bc3 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/BCSS_Binary_Format.txt +239 -0
- data/bin/bc3_merge.rb +155 -0
- data/examples/folder1_2011-01-21.bcss +0 -0
- data/examples/folder2_2011-01-21.bcss +0 -0
- data/examples/test_combine.rb +43 -0
- data/examples/test_filesystem.rb +6 -0
- data/examples/test_hardcoded.rb +15 -0
- data/examples/test_merge.bat +5 -0
- data/examples/test_yaml.rb +188 -0
- data/lib/bc3.rb +89 -0
- data/lib/bc3/file.rb +120 -0
- data/lib/bc3/folder.rb +239 -0
- data/lib/bc3/helper.rb +101 -0
- data/lib/bc3/parse.rb +312 -0
- data/lib/bc3/snapshot.rb +264 -0
- data/lib/bc3/time.rb +1 -0
- data/unittest/unittest_bc3.rb +66 -0
- data/unittest/unittest_bc3_file.rb +35 -0
- data/unittest/unittest_bc3_folder.rb +179 -0
- data/unittest/unittest_bc3_merge.rb +102 -0
- data/unittest/unittest_bc3_snapshot.rb +121 -0
- metadata +119 -0
data/lib/bc3/helper.rb
ADDED
@@ -0,0 +1,101 @@
|
|
1
|
+
module BC3
|
2
|
+
=begin rdoc
|
3
|
+
Define DOS-Attributs.
|
4
|
+
|
5
|
+
http://www.xxcopy.com/xxcopy06.htm
|
6
|
+
Bit 0 Read-Only
|
7
|
+
Bit 1 Hidden
|
8
|
+
Bit 2 System
|
9
|
+
Bit 3 Volume Label
|
10
|
+
Bit 4 Directory
|
11
|
+
Bit 5 Archive
|
12
|
+
|
13
|
+
http://www.computerhope.com/attribhl.htm
|
14
|
+
Bit Positions Hex Description
|
15
|
+
0 0 0 0 0 0 0 1 01h Read Only file
|
16
|
+
0 0 0 0 0 0 1 0 02h Hidden file
|
17
|
+
0 0 0 0 0 1 0 0 04h System file
|
18
|
+
0 0 0 0 1 0 0 0 08h Volume Label
|
19
|
+
0 0 0 1 0 0 0 0 10h Subdirectory
|
20
|
+
0 0 1 0 0 0 0 0 20h Archive
|
21
|
+
0 1 0 0 0 0 0 0 40h Reserved
|
22
|
+
1 0 0 0 0 0 0 0 80h Reserved
|
23
|
+
|
24
|
+
Usage:
|
25
|
+
attrib = Attrib::ReadOnly | Attrib::Hidden
|
26
|
+
=end
|
27
|
+
module Attrib
|
28
|
+
ReadOnly = 1
|
29
|
+
Hidden = 2
|
30
|
+
System = 4
|
31
|
+
VolumeLabel = 8
|
32
|
+
Directory = 16
|
33
|
+
Archive = 32
|
34
|
+
end
|
35
|
+
|
36
|
+
=begin rdoc
|
37
|
+
Helper methods to be included to File and Folder.
|
38
|
+
=end
|
39
|
+
module Helper
|
40
|
+
=begin rdoc
|
41
|
+
Calculate the CRC32 for a string.
|
42
|
+
|
43
|
+
Based on http://www.ruby-forum.com/topic/179452
|
44
|
+
(with minor ruby 1.9 adaptions).
|
45
|
+
=end
|
46
|
+
def self.crc32(c)
|
47
|
+
#Alternative solution:
|
48
|
+
#~ require 'zlib'
|
49
|
+
#~ return Zlib.crc32(c, 0)
|
50
|
+
|
51
|
+
#Encoding of c should be binary or similar.
|
52
|
+
n = c.length
|
53
|
+
r = 0xFFFFFFFF
|
54
|
+
c.each_byte do |i|
|
55
|
+
r ^= i
|
56
|
+
8.times do
|
57
|
+
if (r & 1)!=0
|
58
|
+
r = (r>>1) ^ 0xEDB88320
|
59
|
+
else
|
60
|
+
r >>= 1
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
r ^ 0xFFFFFFFF
|
65
|
+
end
|
66
|
+
|
67
|
+
=begin rdoc
|
68
|
+
Return a Number (e.g. AD epoch) as a binary string with 8 bytes in little endian order.
|
69
|
+
|
70
|
+
With
|
71
|
+
String#<< (Integer)
|
72
|
+
the integer is a codepoint, the character is added.
|
73
|
+
AD's epoch isn't a codepoint, but a binary data.
|
74
|
+
So we need a little conversion, to get
|
75
|
+
AD's epoch (Bignum) as an bit sequence.
|
76
|
+
=end
|
77
|
+
def fixnum2int64( int )
|
78
|
+
bindata = ''.force_encoding('BINARY')
|
79
|
+
#Ugly code, but it works ;-)
|
80
|
+
#.reverse to get "little endian"
|
81
|
+
('%064b' % int).scan(/(\d{8})/).flatten.reverse.each{|b|
|
82
|
+
bindata << b.to_i(2)
|
83
|
+
}
|
84
|
+
raise ArgumentError unless bindata.size == 8 #int was too big
|
85
|
+
bindata
|
86
|
+
end
|
87
|
+
|
88
|
+
=begin rdoc
|
89
|
+
Same as Helper#fixnum2int64, but as 4 bytes string.
|
90
|
+
=end
|
91
|
+
def fixnum2int32( int )
|
92
|
+
bindata = ''.force_encoding('BINARY')
|
93
|
+
('%032b' % int).scan(/(\d{8})/).flatten.reverse.each{|b|
|
94
|
+
bindata << b.to_i(2)
|
95
|
+
}
|
96
|
+
raise ArgumentError unless bindata.size == 4 #int was too big
|
97
|
+
bindata
|
98
|
+
end
|
99
|
+
|
100
|
+
end #Helper
|
101
|
+
end #BC3
|
data/lib/bc3/parse.rb
ADDED
@@ -0,0 +1,312 @@
|
|
1
|
+
=begin rdoc
|
2
|
+
|
3
|
+
=end
|
4
|
+
$:.unshift('..')
|
5
|
+
require 'bc3'
|
6
|
+
|
7
|
+
require "zlib"
|
8
|
+
module BC3
|
9
|
+
=begin rdoc
|
10
|
+
Parser for a given bcss-file.
|
11
|
+
|
12
|
+
=end
|
13
|
+
class SnapshotParser
|
14
|
+
=begin rdoc
|
15
|
+
|
16
|
+
=end
|
17
|
+
def initialize( filename )
|
18
|
+
rawdata = nil
|
19
|
+
@log = $log #fixme replace with sublogger
|
20
|
+
@log.info("Read and parse #{filename}")
|
21
|
+
::File.open( filename, 'rb' ){|f|
|
22
|
+
rawdata = f.read()
|
23
|
+
}
|
24
|
+
|
25
|
+
=begin
|
26
|
+
- HEADER STRUCTURE -
|
27
|
+
[0..3] = 'BCSS'
|
28
|
+
[4] = Major version (UByte)
|
29
|
+
[5] = Minor version (UByte)
|
30
|
+
[6] = Minimum Supported Major Version (UByte)
|
31
|
+
[7] = Minimum Supported Minor Version (UByte)
|
32
|
+
[8..F] = Creation Time (FileTime)
|
33
|
+
[10..11] = Flags (UWord)
|
34
|
+
|
35
|
+
Bit : Meaning
|
36
|
+
0 : Compressed
|
37
|
+
1 : Source Path included
|
38
|
+
2 : Reserved
|
39
|
+
3 : UTF-8
|
40
|
+
4-15 : Reserved
|
41
|
+
|
42
|
+
[12..13] = Path Length (UWord) | Optional
|
43
|
+
[14..N] = Path (char[]) |
|
44
|
+
=end
|
45
|
+
#~ header = rawdata[0..17]
|
46
|
+
@timestamp = Time.now
|
47
|
+
@timestamp, tail = parse_filetime(rawdata[8,8])
|
48
|
+
#Analyse flags - byte position 16/hex10
|
49
|
+
@compressed = rawdata[16].getbyte(0) & 1 != 0
|
50
|
+
@sourcepath = rawdata[16].getbyte(0) & 2 != 0
|
51
|
+
@reserved = rawdata[16].getbyte(0) & 4 != 0
|
52
|
+
@utf = rawdata[16].getbyte(0) & 8 != 0
|
53
|
+
if rawdata[17] != "\x0"
|
54
|
+
@log.warn("2nd flag byte is filled")
|
55
|
+
end
|
56
|
+
|
57
|
+
#Analyse Source path
|
58
|
+
|
59
|
+
#Delete second length parameter for source path
|
60
|
+
if rawdata.slice!(19) != "\x0"
|
61
|
+
@log.warn("Path > 255 not supported")
|
62
|
+
raise "Path > 255 not supported"
|
63
|
+
end
|
64
|
+
path, body = parse_shortstring(rawdata[18..-1])
|
65
|
+
if @compressed
|
66
|
+
=begin
|
67
|
+
Flags:
|
68
|
+
Compressed: If set everything following the header is compressed as a raw
|
69
|
+
deflate stream, as defined by RFC 1951. It is the same compression used by
|
70
|
+
.zip and .gz archives.
|
71
|
+
|
72
|
+
Code from http://www.ruby-forum.com/topic/136825
|
73
|
+
=end
|
74
|
+
@log.debug("uncompress body data")
|
75
|
+
begin
|
76
|
+
body= Zlib::Inflate.inflate(body); #Unclear problem
|
77
|
+
rescue Zlib::DataError
|
78
|
+
@log.debug("Zlib::DataError occured - try with raw deflate")
|
79
|
+
#no luck with Zlib decompression. Let's try with raw deflate,
|
80
|
+
#like some broken browsers do.
|
81
|
+
body= Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate(body)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
@snapshot = Snapshot.new(path, @timestamp)
|
86
|
+
|
87
|
+
parse_body(body)
|
88
|
+
end #initialize
|
89
|
+
#Snapshot-object, result of the parsing.
|
90
|
+
attr_reader :snapshot
|
91
|
+
attr_reader :timestamp
|
92
|
+
|
93
|
+
=begin
|
94
|
+
Parse the body data.
|
95
|
+
|
96
|
+
This method will change the given parameter.
|
97
|
+
=end
|
98
|
+
def parse_body(body)
|
99
|
+
folderstack = [ @snapshot ]
|
100
|
+
while ! body.empty?
|
101
|
+
=begin
|
102
|
+
Each record starts with a single UByte ID value and then the data defined below.
|
103
|
+
=end
|
104
|
+
case last_flag = body.slice!(0)
|
105
|
+
=begin
|
106
|
+
ID_DIRECTORY (0x01)
|
107
|
+
Represents a directory on the system, or an expanded archive file.
|
108
|
+
|
109
|
+
Name : ShortString
|
110
|
+
Last Modified : FileTime
|
111
|
+
DOS Attributes : UInt32
|
112
|
+
=end
|
113
|
+
when "\x01" #folder
|
114
|
+
dirname, tail = parse_shortstring(body)
|
115
|
+
filetime, tail = parse_filetime(tail)
|
116
|
+
attributes, tail = parse_dosattributes(tail)
|
117
|
+
folder = Folder.newh(
|
118
|
+
dirname: dirname,
|
119
|
+
timestamp: filetime,
|
120
|
+
attributes: attributes
|
121
|
+
)
|
122
|
+
folderstack.last << folder
|
123
|
+
folderstack << folder
|
124
|
+
=begin
|
125
|
+
ID_FILE (0x02)
|
126
|
+
Represents a file on the system.
|
127
|
+
|
128
|
+
Name : ShortString
|
129
|
+
Last Modified : FileTime
|
130
|
+
DOS Attributes : UInt32
|
131
|
+
Size : Int32[+Int64]
|
132
|
+
If Size > 2GB, store as Int32(-1) followed by Int64
|
133
|
+
CRC32 : UInt32
|
134
|
+
=end
|
135
|
+
when "\x02" #file
|
136
|
+
filename, tail = parse_shortstring(body)
|
137
|
+
filetime, tail = parse_filetime(tail)
|
138
|
+
attributes, tail = parse_dosattributes(tail)
|
139
|
+
filesize, tail = parse_uint32(tail)
|
140
|
+
crc32, tail = parse_uint32(tail)
|
141
|
+
folderstack.last << File.new(
|
142
|
+
filename: filename,
|
143
|
+
timestamp: filetime,
|
144
|
+
attributes: attributes,
|
145
|
+
filesize: filesize,
|
146
|
+
crc: crc32
|
147
|
+
)
|
148
|
+
=begin
|
149
|
+
ID_FILE_EX (0x03)
|
150
|
+
Represents a file on the system, with extended headers.
|
151
|
+
|
152
|
+
Name..CRC32 is the same as ID_FILE
|
153
|
+
ExtraLen : UInt16
|
154
|
+
ExtraData : Byte[ExtraLen]
|
155
|
+
|
156
|
+
=end
|
157
|
+
when "\x03" #file
|
158
|
+
filename, tail = parse_shortstring(body)
|
159
|
+
filetime, tail = parse_filetime(tail)
|
160
|
+
attributes, tail = parse_dosattributes(tail)
|
161
|
+
filesize, tail = parse_uint32(tail)
|
162
|
+
crc32, tail = parse_uint32(tail)
|
163
|
+
extradata, tail = parse_longstring(tail)
|
164
|
+
extradata = parse_file_extended_headers(extradata)
|
165
|
+
unless extradata #Skip at prob
|
166
|
+
@log.warn("Skip #{filename}")
|
167
|
+
next
|
168
|
+
end
|
169
|
+
folderstack.last << File.new({
|
170
|
+
filename: filename,
|
171
|
+
timestamp: filetime,
|
172
|
+
attributes: attributes,
|
173
|
+
filesize: filesize,
|
174
|
+
crc: crc32,
|
175
|
+
}.merge(extradata)
|
176
|
+
)
|
177
|
+
=begin
|
178
|
+
ID_DIRECTORY_END (0xFF)
|
179
|
+
Represents the end of a directory listing. No data.
|
180
|
+
=end
|
181
|
+
when "\xff" #end of folder
|
182
|
+
folderstack.pop
|
183
|
+
else
|
184
|
+
@log.fatal("Undefined body-parse element #{last_flag.inspect}")
|
185
|
+
p body
|
186
|
+
body.slice!(0..-1) #close further pasring
|
187
|
+
end
|
188
|
+
end
|
189
|
+
if folderstack.size > 1
|
190
|
+
@log.error("Folders in Folderstack not closed correct")
|
191
|
+
p folderstack.size
|
192
|
+
end
|
193
|
+
end
|
194
|
+
=begin rdoc
|
195
|
+
Get a "shortstring".
|
196
|
+
|
197
|
+
1 Byte with length, then the string.
|
198
|
+
|
199
|
+
Return shortstring and rest of string.
|
200
|
+
=end
|
201
|
+
def parse_shortstring( string )
|
202
|
+
#Get length of path
|
203
|
+
pathsize = string.slice!(0).bytes.first
|
204
|
+
# + rawdata[19].bytes.first * 255 #--test it
|
205
|
+
return [string.slice!(0,pathsize), string]
|
206
|
+
end
|
207
|
+
=begin rdoc
|
208
|
+
Get a "longstring".
|
209
|
+
|
210
|
+
2 Bytes with length, then the string.
|
211
|
+
The length is including the 2 bytes for the length
|
212
|
+
|
213
|
+
Return longstring and rest of string.
|
214
|
+
=end
|
215
|
+
def parse_longstring( string )
|
216
|
+
stringsize = string.slice!(0).bytes.first - 2
|
217
|
+
if string.slice!(0) != "\x0"
|
218
|
+
@log.warn("longstring > 255 not supported")
|
219
|
+
raise "longstring > 255 not supported"
|
220
|
+
end
|
221
|
+
return [string.slice!(0,stringsize), string]
|
222
|
+
end
|
223
|
+
=begin rdoc
|
224
|
+
Get Unsigned 32-bit number
|
225
|
+
=end
|
226
|
+
def parse_uint32( string )
|
227
|
+
num = string.slice!(0,4).reverse.each_byte.map{|x| '%08b' % x}.join.to_i(2)
|
228
|
+
return [num, string]
|
229
|
+
end
|
230
|
+
|
231
|
+
=begin rdoc
|
232
|
+
Get a "filetime".
|
233
|
+
|
234
|
+
FileTime:
|
235
|
+
Windows FILETIME structure. 64-bit value representing the number of
|
236
|
+
100-nanosecond intervals since January 1, 1601 UTC. Stored in local time.
|
237
|
+
|
238
|
+
Return time and rest of string.
|
239
|
+
=end
|
240
|
+
def parse_filetime( string )
|
241
|
+
ad_time = string.slice!(0,8) #Integer with filetime
|
242
|
+
time = Time.ad2time(ad_time.reverse.each_byte.map{|x| '%08b' % x}.join.to_i(2))
|
243
|
+
return [time, string]
|
244
|
+
end
|
245
|
+
=begin rdoc
|
246
|
+
Get DOS-attributes.
|
247
|
+
|
248
|
+
=end
|
249
|
+
def parse_dosattributes( string )
|
250
|
+
#Get length of path
|
251
|
+
attributes = string.slice!(0).bytes.first
|
252
|
+
string.slice!(0,3) #skip next 3 bytes
|
253
|
+
return [attributes, string]
|
254
|
+
end
|
255
|
+
=begin
|
256
|
+
=====================
|
257
|
+
File Extended Headers
|
258
|
+
=====================
|
259
|
+
|
260
|
+
Like extended headers, file extended headers should be written in ascending
|
261
|
+
numeric order.
|
262
|
+
|
263
|
+
FILE_EX_VERSION (0x01)
|
264
|
+
String representation of an executable file's Major/Minor/Maint/Build
|
265
|
+
version (e.g., "2.11.28.3542").
|
266
|
+
|
267
|
+
Length : UByte
|
268
|
+
Data : char[Length]
|
269
|
+
|
270
|
+
|
271
|
+
FILE_EX_UTF8 (0x02)
|
272
|
+
UTF-8 encoded filename. Stored as a FileExString. Only used if the UTF-8
|
273
|
+
name doesn't match the ANSI encoded one or if the filename is longer than 255
|
274
|
+
characters.
|
275
|
+
|
276
|
+
|
277
|
+
FILE_EX_LINK_PATH (0x03)
|
278
|
+
UTF-8 encoded symbolic link path. Stored as a FileExString.
|
279
|
+
=end
|
280
|
+
def parse_file_extended_headers(extradata_string)
|
281
|
+
extradata = {}
|
282
|
+
case flag = extradata_string.slice!(0)
|
283
|
+
when "\x01"
|
284
|
+
extradata[:version] = parse_shortstring( extradata_string )
|
285
|
+
when "\x02"
|
286
|
+
@log.warn("Undefined extra data handling for UTF-8 encoded filename")
|
287
|
+
return false #fixme
|
288
|
+
when "\x03"
|
289
|
+
@log.warn("Undefined extra data handling for UTF-8 encoded symbolic")
|
290
|
+
return false #fixme
|
291
|
+
else
|
292
|
+
#fixme handling extradata_string
|
293
|
+
@log.warn("Undefined extra data handling #{flag.inspect} <#{extradata_string.inspect}>")
|
294
|
+
end
|
295
|
+
unless extradata_string.empty?
|
296
|
+
@log.warn("Undefined extra data handling <#{extradata_string.inspect}>")
|
297
|
+
p extradata_string
|
298
|
+
end
|
299
|
+
extradata
|
300
|
+
end
|
301
|
+
end #SnapshotParser
|
302
|
+
def to_hash; @snapshot.to_hash; end
|
303
|
+
end
|
304
|
+
|
305
|
+
if $0 == __FILE__
|
306
|
+
require 'yaml'
|
307
|
+
#~ x = BC3::SnapshotParser.new('../../examples/results/testdir_2011-01-16.bcssx' )
|
308
|
+
x = BC3::SnapshotParser.new('../../examples/results/bc3_2011-01-16.bcss' )
|
309
|
+
#~ x = BC3::SnapshotParser.new('../../Uncompressed Sample/Uncompressed Sample.bcss' )
|
310
|
+
puts x.snapshot.to_hash.to_yaml
|
311
|
+
x.snapshot.save('../../Uncompressed Sample/Uncompressed Sample_reconstructed.bcss')
|
312
|
+
end
|
data/lib/bc3/snapshot.rb
ADDED
@@ -0,0 +1,264 @@
|
|
1
|
+
require 'bc3/helper'
|
2
|
+
module BC3
|
3
|
+
=begin rdoc
|
4
|
+
Container for a snapshot.
|
5
|
+
=end
|
6
|
+
class Snapshot
|
7
|
+
include Helper
|
8
|
+
=begin rdoc
|
9
|
+
=end
|
10
|
+
def initialize( path, timestamp = Time.now )
|
11
|
+
$log.debug("Create Snapshot #{path}")
|
12
|
+
@path = path
|
13
|
+
@timestamp = timestamp || Time.now
|
14
|
+
|
15
|
+
$log.debug("Create base folder for snapshot #{path}")
|
16
|
+
@basefolder = Folder.new('SnapshotRoot', @timestamp)
|
17
|
+
end
|
18
|
+
=begin rdoc
|
19
|
+
Create a snapshot from a hash.
|
20
|
+
|
21
|
+
A snapsot-hash must contain:
|
22
|
+
* snapshot - dirname of the snapshot
|
23
|
+
* content - array of folders (see Folder.newh) and files (File.new)
|
24
|
+
* timestamp (optional)
|
25
|
+
=end
|
26
|
+
def self.newh( data )
|
27
|
+
$log.info("Build Snapshot from hash")
|
28
|
+
raise ArgumentError, "No hash given" unless data.is_a?(Hash)
|
29
|
+
raise ArgumentError, "snapshot name missing" unless data.has_key?(:snapshot)
|
30
|
+
raise ArgumentError, "content missing" unless data.has_key?(:content)
|
31
|
+
raise ArgumentError, "Content is no array" unless data[:content].is_a?(Array)
|
32
|
+
|
33
|
+
snapshot = new( data[:snapshot], data[:timestamp] )
|
34
|
+
data[:content].each{| element |
|
35
|
+
if element.has_key?(:dirname)
|
36
|
+
snapshot << Folder.newh(element)
|
37
|
+
elsif element.has_key?(:filename)
|
38
|
+
snapshot << File.new(element)
|
39
|
+
else
|
40
|
+
raise ArgumentError, "element without dir/filename"
|
41
|
+
end
|
42
|
+
}
|
43
|
+
snapshot
|
44
|
+
end #newh
|
45
|
+
=begin rdoc
|
46
|
+
Create a snapshot from a directory.
|
47
|
+
|
48
|
+
=end
|
49
|
+
def self.newd( dirname )
|
50
|
+
$log.info("Build Snapshot from directory #{dirname}")
|
51
|
+
|
52
|
+
#~ raise ArgumentError, "No hash given" unless data.is_a?(Hash)
|
53
|
+
snapshot = new( ::File.expand_path("./#{dirname}") )
|
54
|
+
Dir.chdir(dirname){
|
55
|
+
Dir['*'].each{|f|
|
56
|
+
if ::File.directory?(f)
|
57
|
+
snapshot << Folder.new_by_dirname(f)
|
58
|
+
elsif ::File.exist?(f)
|
59
|
+
snapshot << File.new_by_filename(f)
|
60
|
+
else
|
61
|
+
raise ArgumentError, "#{f} not found in #{dirname}"
|
62
|
+
end
|
63
|
+
}
|
64
|
+
}
|
65
|
+
snapshot
|
66
|
+
end #newh
|
67
|
+
|
68
|
+
#homepath of the snapshot
|
69
|
+
attr_reader :path
|
70
|
+
#Time stamp from snapshot. Default 'now'
|
71
|
+
attr_reader :timestamp
|
72
|
+
|
73
|
+
=begin rdoc
|
74
|
+
Add content (folders/files) to snapshot.
|
75
|
+
=end
|
76
|
+
def << (content)
|
77
|
+
@basefolder << content
|
78
|
+
end
|
79
|
+
=begin rdoc
|
80
|
+
Loop on content of the folder.
|
81
|
+
|
82
|
+
Options see BC3::Folder#each
|
83
|
+
=end
|
84
|
+
def each(*options)
|
85
|
+
if block_given?
|
86
|
+
@basefolder.each(*options){|key, content| yield key, content }
|
87
|
+
else
|
88
|
+
@basefolder.each
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
=begin rdoc
|
93
|
+
=end
|
94
|
+
def save( filename, compressed = nil )
|
95
|
+
$log.debug("Prepare snapshot for #{filename}")
|
96
|
+
#Check if compressed or uncompressed output wanted
|
97
|
+
compressed = ( filename =~ /\.bcssx/ ) if compressed.nil?
|
98
|
+
#Must be binary, else a \n get's \r\n under windows.
|
99
|
+
::File.open(filename,'wb'){|f|
|
100
|
+
f << bcss( compressed )
|
101
|
+
}
|
102
|
+
$log.info("Saved snapshot as #{filename}")
|
103
|
+
end
|
104
|
+
=begin rdoc
|
105
|
+
Collect the data in a hash.
|
106
|
+
|
107
|
+
Usefull in combination with yaml:
|
108
|
+
require 'bc3'
|
109
|
+
require 'yaml'
|
110
|
+
#...
|
111
|
+
snapshot = snapshot.new(...)
|
112
|
+
#...
|
113
|
+
puts snapshot.to_hash.to_yaml
|
114
|
+
=end
|
115
|
+
def to_hash()
|
116
|
+
{
|
117
|
+
snapshot: @path,
|
118
|
+
timestamp: @timestamp,
|
119
|
+
content: @basefolder.each.values.map{| x | x.to_hash }
|
120
|
+
}
|
121
|
+
end
|
122
|
+
=begin rdoc
|
123
|
+
Prepare a snapshot (bcss-file).
|
124
|
+
|
125
|
+
Only uncompressed structure.
|
126
|
+
|
127
|
+
===Beyond Compare Snapshot Format Version 1.1
|
128
|
+
Beyond Compare snapshots (.bcss) are binary files containing the file metadata
|
129
|
+
(names, sizes, last modified times) of a directory structure without storing
|
130
|
+
any of the file content. They are designed to be read sequentially. File
|
131
|
+
record sizes are variable, so there's no way to seek to arbitrary records
|
132
|
+
without reading all of the records before it.
|
133
|
+
|
134
|
+
=end
|
135
|
+
def bcss( compressed = false )
|
136
|
+
bcss = "".force_encoding('BINARY')
|
137
|
+
bcss << bcss_header( compressed )
|
138
|
+
if compressed
|
139
|
+
$log.debug("Compress bcss-data")
|
140
|
+
$log.fatal("Compress bcss-data not supported - only for test purposes")
|
141
|
+
=begin
|
142
|
+
Flags:
|
143
|
+
Compressed: If set everything following the header is compressed as a raw
|
144
|
+
deflate stream, as defined by RFC 1951. It is the same compression used by
|
145
|
+
.zip and .gz archives.
|
146
|
+
=end
|
147
|
+
#see for truncations http://www.ruby-forum.com/topic/101078
|
148
|
+
# http://ilovett.com/blog/programming/ruby-zlib-deflate
|
149
|
+
#~ puts "%-2i %s" % [ 99, bcss_data.inspect ]
|
150
|
+
-1.upto(9){|i|
|
151
|
+
puts "%-2i %s" % [ i, Zlib::Deflate.deflate( bcss_data, i )[2..-5].inspect ]
|
152
|
+
}
|
153
|
+
bcss << Zlib::Deflate.deflate( bcss_data)[2..-5]
|
154
|
+
#~ bcss << Zlib::Deflate.new(nil, -Zlib::MAX_WBITS).deflate(bcss_data, Zlib::FINISH)
|
155
|
+
else #uncompressed
|
156
|
+
bcss << bcss_data
|
157
|
+
end
|
158
|
+
bcss << 255
|
159
|
+
|
160
|
+
bcss
|
161
|
+
end
|
162
|
+
|
163
|
+
=begin rdoc
|
164
|
+
Create the header data for bcss-file
|
165
|
+
|
166
|
+
Snapshots start with a fixed size header that contains an ID value, version
|
167
|
+
information, a creation date, and various flags, optionally followed by the
|
168
|
+
source folder's path:
|
169
|
+
|
170
|
+
- HEADER STRUCTURE -
|
171
|
+
[0..3] = 'BCSS'
|
172
|
+
[4] = Major version (UByte)
|
173
|
+
[5] = Minor version (UByte)
|
174
|
+
[6] = Minimum Supported Major Version (UByte)
|
175
|
+
[7] = Minimum Supported Minor Version (UByte)
|
176
|
+
[8..F] = Creation Time (FileTime)
|
177
|
+
[10..11] = Flags (UWord)
|
178
|
+
|
179
|
+
Bit : Meaning
|
180
|
+
0 : Compressed
|
181
|
+
1 : Source Path included
|
182
|
+
2 : Reserved
|
183
|
+
3 : UTF-8
|
184
|
+
4-15 : Reserved
|
185
|
+
|
186
|
+
[12..13] = Path Length (UWord) | Optional
|
187
|
+
[14..N] = Path (char[]) |
|
188
|
+
|
189
|
+
Version Information:
|
190
|
+
The first two version bytes represent the actual major and minor versions
|
191
|
+
of the file, and reference a specific version of this specification. The
|
192
|
+
second pair of version bytes represent the minimum snapshot version which must
|
193
|
+
be supported in order to read the snapshot file. Version 1.1 can be read by
|
194
|
+
Version 1.0 applications, so currently Major/Minor should be set to 1.1 and
|
195
|
+
Minimum should be 1.0.
|
196
|
+
|
197
|
+
Flags:
|
198
|
+
Compressed: If set everything following the header is compressed as a raw
|
199
|
+
deflate stream, as defined by RFC 1951. It is the same compression used by
|
200
|
+
.zip and .gz archives.
|
201
|
+
|
202
|
+
Source Path included: If set the original folder's path is included
|
203
|
+
immediately after the header. This is only on part of the file besides the
|
204
|
+
fixed header that is not compressed.
|
205
|
+
|
206
|
+
UTF-8: If set the snapshot was compressed on a system where the default
|
207
|
+
character encoding is UTF-8 (Linux, OS X). Filenames, paths, and link targets
|
208
|
+
will all be stored as UTF-8. If this isn't set the paths are stored using the
|
209
|
+
original OS's ANSI codepage (Windows). In that case any paths may be stored a
|
210
|
+
second time as UTF-8 in extended headers.
|
211
|
+
|
212
|
+
=end
|
213
|
+
def bcss_header( compressed )
|
214
|
+
header = "".force_encoding('BINARY')
|
215
|
+
header << 'BCSS'
|
216
|
+
header << 1 #Major version (UByte)
|
217
|
+
header << 1 #Minor version (UByte)
|
218
|
+
header << 1 #Minimum Supported Major Version (UByte)
|
219
|
+
header << 0 #Minimum Supported Minor Version (UByte)
|
220
|
+
|
221
|
+
#[8..F] = Creation Time (FileTime)
|
222
|
+
#Windows FILETIME structure. 64-bit value representing the number of
|
223
|
+
#100-nanosecond intervals since January 1, 1601 UTC. Stored in local time.
|
224
|
+
#8 Byte long
|
225
|
+
#~ header << "%x" % Time.now.time2ad #-> bignum too big to convert into `unsigned long' (RangeError)
|
226
|
+
header << fixnum2int64(@timestamp.time2ad)
|
227
|
+
#~ header << "\x70\x57\x5C\x25\x69\xB2\xCB\x01" #Data from example
|
228
|
+
|
229
|
+
# [10..11] = Flags (UWord)
|
230
|
+
|
231
|
+
#~ Bit : Meaning
|
232
|
+
#~ 0 : Compressed
|
233
|
+
#~ 1 : Source Path included
|
234
|
+
#~ 2 : Reserved
|
235
|
+
#~ 3 : UTF-8
|
236
|
+
#~ 4-15 : Reserved
|
237
|
+
flag = 0 #no flag set
|
238
|
+
flag += 2 #Source Path included
|
239
|
+
flag += 1 if compressed
|
240
|
+
header << flag
|
241
|
+
header << 0
|
242
|
+
|
243
|
+
# [12..13] = Path Length (UWord) | Optional
|
244
|
+
header << @path.size
|
245
|
+
header << 0 #fixme if path > 255
|
246
|
+
raise "too long path" if @path.size > 155 #fixme
|
247
|
+
# [14..N] = Path (char[]) |
|
248
|
+
header << @path
|
249
|
+
|
250
|
+
header
|
251
|
+
end #header
|
252
|
+
=begin rdoc
|
253
|
+
Return the data part of the snapshot.
|
254
|
+
This part may be packed.
|
255
|
+
=end
|
256
|
+
def bcss_data()
|
257
|
+
data = "".force_encoding('BINARY')
|
258
|
+
@basefolder.each{|key, folder|
|
259
|
+
data << folder.bcss
|
260
|
+
}
|
261
|
+
data
|
262
|
+
end
|
263
|
+
end #Snapshot
|
264
|
+
end #module BC3
|