zip64writer 0.0.1
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/Rakefile +35 -0
- data/lib/zip64/debug.rb +82 -0
- data/lib/zip64/structures.rb +443 -0
- data/lib/zip64/version.rb +6 -0
- data/lib/zip64/writer.rb +398 -0
- data/test/zip64_test.rb +28 -0
- metadata +73 -0
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
Rake::TestTask.new do |t|
|
4
|
+
t.test_files = FileList['test/*_test.rb']
|
5
|
+
t.verbose = true
|
6
|
+
end
|
7
|
+
|
8
|
+
require 'rubygems/package_task'
|
9
|
+
|
10
|
+
$: << 'lib'
|
11
|
+
|
12
|
+
require 'zip64/version'
|
13
|
+
|
14
|
+
spec = Gem::Specification.new do |s|
|
15
|
+
s.platform = Gem::Platform::RUBY
|
16
|
+
s.summary = "Zip64 Output Library"
|
17
|
+
s.authors = ["Geoff Youngs"]
|
18
|
+
s.email = 'git@intersect-uk.co.uk'
|
19
|
+
s.homepage = 'http://github.com/geoffyoungs/zip64writer'
|
20
|
+
s.name = 'zip64writer'
|
21
|
+
s.version = Zip64::VERSION.to_s
|
22
|
+
s.requirements << 'none'
|
23
|
+
s.require_path = 'lib'
|
24
|
+
s.files = FileList['lib/zip64/*.rb']
|
25
|
+
s.test_files = FileList['test/*_test.rb'] + ['Rakefile']
|
26
|
+
s.description = <<EOF
|
27
|
+
A simple library to output Zip64 zip files from pure ruby.
|
28
|
+
EOF
|
29
|
+
end
|
30
|
+
|
31
|
+
Gem::PackageTask.new(spec) do |pkg|
|
32
|
+
pkg.need_zip = true
|
33
|
+
pkg.need_tar = true
|
34
|
+
end
|
35
|
+
|
data/lib/zip64/debug.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
$: << File.dirname(__FILE__)+"/../"
|
2
|
+
require 'zip64/structures'
|
3
|
+
require 'stringio'
|
4
|
+
def find_block_type sig
|
5
|
+
Zip64.constants.each do |nam|
|
6
|
+
klass = Zip64.const_get(nam)
|
7
|
+
if klass.respond_to?(:constants) && klass.constants.include?('SIG')
|
8
|
+
if sig == klass::SIG
|
9
|
+
return klass
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def zip_debug(arg)
|
17
|
+
|
18
|
+
File.open(arg, "rb") do |fp|
|
19
|
+
until fp.eof?
|
20
|
+
until fp.eof? or (fp.tell%2).zero?
|
21
|
+
fp.getc
|
22
|
+
end
|
23
|
+
bs = fp.tell
|
24
|
+
STDOUT.write(sprintf(".%-16i", bs))
|
25
|
+
sig = fp.read(4).unpack('V').first
|
26
|
+
klass = find_block_type sig
|
27
|
+
|
28
|
+
if klass
|
29
|
+
o = klass.read_from(fp, 1)
|
30
|
+
o.signature = sig
|
31
|
+
o.describe(STDOUT)
|
32
|
+
|
33
|
+
case o
|
34
|
+
when Zip64::LocalFileHeader
|
35
|
+
filename = fp.read(o.filename_len)
|
36
|
+
extra = fp.read(o.extra_field_len)
|
37
|
+
|
38
|
+
data_len = o.data_len
|
39
|
+
|
40
|
+
unless extra.empty?
|
41
|
+
extra_fp = StringIO.new(extra)
|
42
|
+
until extra_fp.eof?
|
43
|
+
esig = extra_fp.read(2).unpack('v').first
|
44
|
+
if eklass = find_block_type(esig)
|
45
|
+
e = eklass.read_from(extra_fp, 1)
|
46
|
+
e.signature = sig
|
47
|
+
e.describe(STDOUT)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
#if o.data_len == Zip64::LEN64
|
51
|
+
# info = Zip64::Zip64ExtraField.read_from(StringIO.new(extra), 0)
|
52
|
+
# info.describe(STDOUT)
|
53
|
+
# data_len = info.data_len
|
54
|
+
#end
|
55
|
+
end
|
56
|
+
|
57
|
+
data = fp.read(data_len)
|
58
|
+
p [:filename, filename]
|
59
|
+
p [:extra, extra]
|
60
|
+
if data.size > 50
|
61
|
+
data = data[0..50]+"(#{data.size} bytes, truncated)"
|
62
|
+
end
|
63
|
+
p [:data, data]
|
64
|
+
when Zip64::CDFileHeader
|
65
|
+
filename = fp.read(o.filename_len)
|
66
|
+
extra = fp.read(o.extra_field_len)
|
67
|
+
comment = fp.read(o.file_comment_len)
|
68
|
+
p [:filename, filename]
|
69
|
+
p [:extra, extra]
|
70
|
+
p [:comment, comment]
|
71
|
+
end
|
72
|
+
else
|
73
|
+
puts sprintf('%16s %16i', sig.to_s(16), sig) if sig
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
if __FILE__.eql?($0)
|
81
|
+
zip_debug ARGV[0]
|
82
|
+
end
|
@@ -0,0 +1,443 @@
|
|
1
|
+
#/usr/bin/env ruby
|
2
|
+
|
3
|
+
class Block
|
4
|
+
class FieldFormat
|
5
|
+
def self.from(line)
|
6
|
+
case line
|
7
|
+
when Array
|
8
|
+
new(*line)
|
9
|
+
when self
|
10
|
+
line
|
11
|
+
else
|
12
|
+
raise "#{line.inspect} is not a valid field"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :type, :name
|
17
|
+
def initialize(type, name, options = {})
|
18
|
+
if options.is_a?(Hash)
|
19
|
+
else
|
20
|
+
val, options = options, {}
|
21
|
+
options[:default] = val
|
22
|
+
end
|
23
|
+
@type, @name, @options = type, name, options
|
24
|
+
end
|
25
|
+
|
26
|
+
def encode(object, value)
|
27
|
+
val = value || @options[:default]
|
28
|
+
[val].pack(@type).force_encoding("ASCII-8BIT")
|
29
|
+
rescue
|
30
|
+
raise "#{value.inspect} (#{val.inspect}?) is not a valid type for #{self.inspect}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
class << self
|
34
|
+
# Reader mode:
|
35
|
+
# With no arguments returns the fields defined for this class
|
36
|
+
#
|
37
|
+
# Writer mode:
|
38
|
+
# If args is not empty then each argument is passed to FieldFormat.from(arg)
|
39
|
+
# to convert to a FieldFormat object.
|
40
|
+
#
|
41
|
+
# Also accessors are defined
|
42
|
+
def fields *args
|
43
|
+
if args.empty?
|
44
|
+
@fields
|
45
|
+
else
|
46
|
+
@fields = args.map { |arg| FieldFormat.from(arg) }
|
47
|
+
@fields.each { |f| attr_accessor(f.name) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def fields
|
53
|
+
self.class.fields()
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.base_size
|
57
|
+
fields.inject(0) { |t, f| size_of(f.type) + t }
|
58
|
+
end
|
59
|
+
|
60
|
+
def read_field_from(fp, field)
|
61
|
+
if (sz = size_of(field.type)).nonzero?
|
62
|
+
val = fp.read(sz).unpack(field.type).first
|
63
|
+
send("#{field.name}=", val)
|
64
|
+
else
|
65
|
+
fn = "#{field.name}_len"
|
66
|
+
if field.type == "A*" && o.respond_to?(fn) && o.send(fn)
|
67
|
+
send("#{field.name}=", fp.read(o.send(fn)))
|
68
|
+
else
|
69
|
+
puts "#{field.name} un-fetchable"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.read_from(fp, offset)
|
75
|
+
o = new()
|
76
|
+
fields[offset..-1].each do |field|
|
77
|
+
next if o.respond_to?(:skip_read?) && o.skip_read?(field)
|
78
|
+
o.read_field_from(fp, field)
|
79
|
+
end
|
80
|
+
o
|
81
|
+
end
|
82
|
+
def describe(io)
|
83
|
+
io.puts "---- #{self.class.name}"
|
84
|
+
fields.each do |field|
|
85
|
+
val = send(field.name)
|
86
|
+
if val.nil?
|
87
|
+
io.puts sprintf("%33s %s", 'NULL', field.name)
|
88
|
+
else
|
89
|
+
io.puts sprintf("%16s %16i %2i %s", val.to_s(16), val,
|
90
|
+
size_of(field.type), field.name)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
def self.size_of(type)
|
97
|
+
case type
|
98
|
+
when 'C', 'c'
|
99
|
+
1
|
100
|
+
when 'v'
|
101
|
+
2
|
102
|
+
when 'V'
|
103
|
+
4
|
104
|
+
when 'Q'
|
105
|
+
8
|
106
|
+
else
|
107
|
+
0
|
108
|
+
end
|
109
|
+
end
|
110
|
+
def size_of(type)
|
111
|
+
self.class.size_of(type)
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_string
|
115
|
+
buf = ''.force_encoding("ASCII-8BIT")
|
116
|
+
fields.each do |f|
|
117
|
+
buf << pack_field(f)
|
118
|
+
end
|
119
|
+
buf
|
120
|
+
end
|
121
|
+
alias :to_s :to_string
|
122
|
+
|
123
|
+
def pack_field(field)
|
124
|
+
field.encode(self, send(field.name))
|
125
|
+
end
|
126
|
+
|
127
|
+
def size
|
128
|
+
to_string.size
|
129
|
+
end
|
130
|
+
|
131
|
+
def initialize(options={})
|
132
|
+
options.each do |key,val|
|
133
|
+
send("#{key}=", val) if respond_to?("#{key}") && respond_to?("#{key}=")
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
module Zip64
|
139
|
+
|
140
|
+
LEN64 = 0xFFFFFFFF
|
141
|
+
module Flags
|
142
|
+
ENCRYPTED = 1 << 0
|
143
|
+
CRC_IN_CD = 1 << 3
|
144
|
+
UTF8 = 1 << 11
|
145
|
+
end
|
146
|
+
module Compression
|
147
|
+
NONE = 0
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
class Zip64ExtraField < Block
|
152
|
+
ID = 0x0001
|
153
|
+
fields ['v', :header_id],
|
154
|
+
['v', :header_len],
|
155
|
+
['Q', :raw_data_len],
|
156
|
+
['Q', :data_len]
|
157
|
+
def to_s; to_string; end
|
158
|
+
end
|
159
|
+
|
160
|
+
class ::String
|
161
|
+
if RUBY_VERSION <= "1.8.7"
|
162
|
+
def force_encoding(what)
|
163
|
+
self
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
class LocalFileHeader < Block
|
169
|
+
SIG = 0x04034b50
|
170
|
+
|
171
|
+
fields ['V', :signature, SIG],
|
172
|
+
['v', :version, 45],
|
173
|
+
['v', :flags, 0],
|
174
|
+
['v', :compression, 0],
|
175
|
+
['v', :last_mod_file_time],
|
176
|
+
['v', :last_mod_file_date],
|
177
|
+
['V', :crc32],
|
178
|
+
['V', :data_len],
|
179
|
+
['V', :raw_data_len],
|
180
|
+
['v', :filename_len],
|
181
|
+
['v', :extra_field_len]
|
182
|
+
|
183
|
+
def zip64?
|
184
|
+
data_len == LEN64 && raw_data_len == LEN64
|
185
|
+
end
|
186
|
+
|
187
|
+
def version
|
188
|
+
zip64? ? 45 : 10
|
189
|
+
end
|
190
|
+
|
191
|
+
attr_reader :filename
|
192
|
+
def to_string
|
193
|
+
extra = extra_field_str
|
194
|
+
self.extra_field_len = extra.size
|
195
|
+
super + "#{@filename}#{extra}"
|
196
|
+
end
|
197
|
+
def to_s
|
198
|
+
to_string
|
199
|
+
end
|
200
|
+
def filename=(str)
|
201
|
+
str = str.dup.force_encoding('ASCII-8BIT')
|
202
|
+
self.filename_len = str.size
|
203
|
+
@filename = str
|
204
|
+
end
|
205
|
+
def extra_field=(str)
|
206
|
+
case str
|
207
|
+
when Array
|
208
|
+
@extra_field = str
|
209
|
+
else
|
210
|
+
@extra_field = [str]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
def extra_field
|
214
|
+
(@extra_field ||= [])
|
215
|
+
end
|
216
|
+
def extra_field_str
|
217
|
+
buf = ''.force_encoding('ASCII-8BIT')
|
218
|
+
extra_field.each do |field|
|
219
|
+
if field.respond_to?(:to_string)
|
220
|
+
buf << field.to_string.force_encoding('ASCII-8BIT')
|
221
|
+
else
|
222
|
+
buf << field.to_s.force_encoding('ASCII-8BIT')
|
223
|
+
end
|
224
|
+
end
|
225
|
+
buf
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
class CDFileHeader < Block
|
230
|
+
SIG = 0x02014b50
|
231
|
+
fields ['V', :signature, SIG],
|
232
|
+
['v', :made_by],
|
233
|
+
['v', :version, 45],
|
234
|
+
['v', :flags, 0],
|
235
|
+
['v', :compression, 0],
|
236
|
+
['v', :last_mod_file_time],
|
237
|
+
['v', :last_mod_file_date],
|
238
|
+
['V', :crc32, 0],
|
239
|
+
['V', :data_len],
|
240
|
+
['V', :raw_data_len],
|
241
|
+
['v', :filename_len], # auto
|
242
|
+
['v', :extra_field_len], # auto
|
243
|
+
['v', :file_comment_len], #
|
244
|
+
['v', :disk_no],
|
245
|
+
['v', :internal_file_attributes],
|
246
|
+
['V', :external_file_attributes],
|
247
|
+
['V', :rel_offset_of_local_header]
|
248
|
+
|
249
|
+
def zip64?
|
250
|
+
data_len == LEN64 && raw_data_len == LEN64
|
251
|
+
end
|
252
|
+
|
253
|
+
def version
|
254
|
+
zip64? ? 45 : 10
|
255
|
+
end
|
256
|
+
|
257
|
+
attr_reader :filename, :extra_field, :file_comment
|
258
|
+
def to_string
|
259
|
+
super + "#{@filename}#{extra_field}#{file_comment}"
|
260
|
+
end
|
261
|
+
def file_comment=(str)
|
262
|
+
str = str.force_encoding('ASCII-8BIT')
|
263
|
+
self.file_comment_len = str.size
|
264
|
+
@file_comment = str
|
265
|
+
end
|
266
|
+
def filename=(str)
|
267
|
+
str = str.force_encoding('ASCII-8BIT')
|
268
|
+
self.filename_len = str.size
|
269
|
+
@filename = str
|
270
|
+
end
|
271
|
+
def extra_field=(str)
|
272
|
+
str = str.force_encoding('ASCII-8BIT')
|
273
|
+
self.extra_field_len = str.size
|
274
|
+
@extra_field = str
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
class Zip64CDExtraField < Block
|
279
|
+
SIG = 0x0001
|
280
|
+
fields ['v', :signature, SIG],
|
281
|
+
['v', :size],
|
282
|
+
['Q', :raw_data_len],
|
283
|
+
['Q', :data_len],
|
284
|
+
['Q', :relative_offset],
|
285
|
+
['V', :disk_no]
|
286
|
+
def to_string
|
287
|
+
@size = 0
|
288
|
+
fields.each { |field| @size += size_of(field.type) }
|
289
|
+
@size -= 4
|
290
|
+
super
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
class UnixExtraField < Block
|
295
|
+
SIG = 0x000d
|
296
|
+
fields ['v', :signature, SIG],
|
297
|
+
['v', :size],
|
298
|
+
['V', :atime],
|
299
|
+
['V', :mtime],
|
300
|
+
['v', :uid],
|
301
|
+
['v', :gid],
|
302
|
+
['A*', :data, '']
|
303
|
+
|
304
|
+
def to_string
|
305
|
+
@size = @data.to_s.size
|
306
|
+
fields.each { |field| @size += size_of(field.type) }
|
307
|
+
@size -= 4
|
308
|
+
super
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
class ExtendedTimestampField < Block
|
313
|
+
SIG = 0x5455
|
314
|
+
fields ['v', :signature, SIG],
|
315
|
+
['v', :size],
|
316
|
+
['C', :info_bits],
|
317
|
+
['V', :mtime],
|
318
|
+
['V', :atime],
|
319
|
+
['V', :ctime]
|
320
|
+
def skip_read?(field)
|
321
|
+
case field.name
|
322
|
+
when :mtime
|
323
|
+
(@info_bits&1).zero?
|
324
|
+
when :atime
|
325
|
+
(@info_bits&2).zero?
|
326
|
+
when :ctime
|
327
|
+
(@info_bits&4).zero?
|
328
|
+
else
|
329
|
+
false
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
class InfoZipNewUnixExtraField < Block
|
335
|
+
SIG = 0x7875
|
336
|
+
fields ['v', :signature, SIG],
|
337
|
+
['v', :size],
|
338
|
+
['C', :version],
|
339
|
+
['C', :uid_size],
|
340
|
+
['V', :uid],
|
341
|
+
['C', :gid_size],
|
342
|
+
['V', :gid]
|
343
|
+
MAP = { 1 => 'C', 2 => 'v', 4 => 'V'}
|
344
|
+
|
345
|
+
def pack_field(field)
|
346
|
+
case field.name
|
347
|
+
when :uid
|
348
|
+
type = MAP[@uid_size]
|
349
|
+
[@uid].pack(type).force_encoding('ASCII-8BIT')
|
350
|
+
when :gid
|
351
|
+
type = MAP[@gid_size]
|
352
|
+
[@gid].pack(type).force_encoding('ASCII-8BIT')
|
353
|
+
else
|
354
|
+
super
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def read_field_from(fp, field)
|
359
|
+
case field.name
|
360
|
+
when :uid
|
361
|
+
type = MAP[@uid_size]
|
362
|
+
@uid = fp.read(@uid_size).unpack(type).first
|
363
|
+
when :gid
|
364
|
+
type = MAP[@gid_size]
|
365
|
+
@gid = fp.read(@gid_size).unpack(type).first
|
366
|
+
else
|
367
|
+
super
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
class DigSig < Block
|
373
|
+
SIG = 0x05054b50
|
374
|
+
fields ['V', :signature, SIG],
|
375
|
+
['v', :size],
|
376
|
+
['A*', :data]
|
377
|
+
def to_string
|
378
|
+
@size = @data.size
|
379
|
+
super
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
class DD64 < Block
|
384
|
+
SIG = 0x08074b50
|
385
|
+
fields ['V', :signature, SIG],
|
386
|
+
['V', :crc32, 0],
|
387
|
+
['Q', :data_len],
|
388
|
+
['Q', :raw_data_len]
|
389
|
+
end
|
390
|
+
|
391
|
+
class Zip64EOCDR < Block
|
392
|
+
# SIG = 0x02014b50
|
393
|
+
SIG = 0x06064b50
|
394
|
+
fields ['V', :signature, SIG],
|
395
|
+
['Q', :record_len],
|
396
|
+
['v', :made_by],
|
397
|
+
['v', :version, 45],
|
398
|
+
['V', :this_disk_no],
|
399
|
+
['V', :disk_with_cd_no],
|
400
|
+
['Q', :total_no_entries_on_this_disk],
|
401
|
+
['Q', :total_no_entries],
|
402
|
+
['Q', :size_of_cd],
|
403
|
+
['Q', :offset_of_cd_wrt_disk_no]
|
404
|
+
['A*', :data, '']
|
405
|
+
def to_string
|
406
|
+
@record_len = @data.to_s.size - 12
|
407
|
+
fields.each { |field| @record_len += size_of(field.type) }
|
408
|
+
super
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
412
|
+
class Zip64EOCDL < Block
|
413
|
+
SIG = 0x07064b50
|
414
|
+
fields ['V', :signature, SIG],
|
415
|
+
['V', :disk_with_z64_eocdr],
|
416
|
+
['Q', :relative_offset],
|
417
|
+
['V', :no_disks]
|
418
|
+
end
|
419
|
+
|
420
|
+
class EOCDR < Block
|
421
|
+
SIG = 0x06054b50
|
422
|
+
fields ['V', :signature, SIG],
|
423
|
+
['v', :disk_no],
|
424
|
+
['v', :disk_with_cd_no],
|
425
|
+
['v', :total_entries_in_local_cd],
|
426
|
+
['v', :total_entries],
|
427
|
+
['V', :cd_size],
|
428
|
+
['V', :offset_to_cd_start],
|
429
|
+
['v', :file_comment_len]
|
430
|
+
attr_reader :file_comment
|
431
|
+
def to_string
|
432
|
+
super + "#{@file_comment}"
|
433
|
+
end
|
434
|
+
def file_comment=(str)
|
435
|
+
str = str.force_encoding('ASCII-8BIT')
|
436
|
+
@file_comment = str
|
437
|
+
self.file_comment_len = str.size
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
end
|
442
|
+
|
443
|
+
|
data/lib/zip64/writer.rb
ADDED
@@ -0,0 +1,398 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
require 'zip64/structures'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
module Zip64
|
6
|
+
module_function
|
7
|
+
def time_to_msdos_time(time)
|
8
|
+
mt = 0
|
9
|
+
|
10
|
+
mt |= time.sec
|
11
|
+
mt |= time.min << 5
|
12
|
+
mt |= time.hour << 11
|
13
|
+
|
14
|
+
mt
|
15
|
+
end
|
16
|
+
def date_to_msdos_date(date)
|
17
|
+
md = 0
|
18
|
+
|
19
|
+
md |= date.day
|
20
|
+
md |= date.month << 5
|
21
|
+
md |= (date.year - 1980) << 9
|
22
|
+
|
23
|
+
md
|
24
|
+
end
|
25
|
+
|
26
|
+
class ZipWriter
|
27
|
+
def initialize(io)
|
28
|
+
@io, @offset = io, 0
|
29
|
+
@dir_entries = []
|
30
|
+
if block_given?
|
31
|
+
yield(self)
|
32
|
+
close
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def ensure_metadata(io, info)
|
37
|
+
info = info.dup
|
38
|
+
|
39
|
+
info[:mtime] ||= Time.now
|
40
|
+
info[:gid] ||= Process.gid
|
41
|
+
info[:uid] ||= Process.uid
|
42
|
+
|
43
|
+
info[:name] ||= File.basename(io.path) if io.respond_to?(:path) && io.path
|
44
|
+
info[:name] ||= "file-#{@dir_entries.size+2}.dat"
|
45
|
+
|
46
|
+
info
|
47
|
+
end
|
48
|
+
|
49
|
+
def make_entry32(info, data, crc)
|
50
|
+
header = LocalFileHeader.new(
|
51
|
+
:flags => (1<<11),
|
52
|
+
:compression => 0,
|
53
|
+
:last_mod_file_time => Zip64.time_to_msdos_time(info[:mtime]),
|
54
|
+
:last_mod_file_date => Zip64.date_to_msdos_date(info[:mtime]),
|
55
|
+
:crc32 => crc,
|
56
|
+
:data_len => data.size,
|
57
|
+
:raw_data_len => data.size,
|
58
|
+
:filename => info[:name],
|
59
|
+
:extra_field => '')
|
60
|
+
|
61
|
+
header
|
62
|
+
end
|
63
|
+
|
64
|
+
def make_entry64(info, data, crc)
|
65
|
+
header = LocalFileHeader.new(
|
66
|
+
:flags => 0,
|
67
|
+
:last_mod_file_time => Zip64.time_to_msdos_time(info[:mtime]),
|
68
|
+
:last_mod_file_date => Zip64.date_to_msdos_date(info[:mtime]),
|
69
|
+
:crc32 => crc,
|
70
|
+
:data_len => LEN64,
|
71
|
+
:raw_data_len => LEN64,
|
72
|
+
:filename => info[:name],
|
73
|
+
:extra_field => Zip64ExtraField.new(
|
74
|
+
:header_id => Zip64ExtraField::ID,
|
75
|
+
:header_len => 16,
|
76
|
+
:raw_data_len => data.size,
|
77
|
+
:data_len => data.size)
|
78
|
+
)
|
79
|
+
|
80
|
+
header
|
81
|
+
end
|
82
|
+
|
83
|
+
def make_entry(info, data, crc)
|
84
|
+
if info[:use] == 64 || @offset + data.size > self.threshold
|
85
|
+
header = make_entry64(info, data, crc)
|
86
|
+
else
|
87
|
+
header = make_entry32(info, data, crc)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def add_link(target, info)
|
92
|
+
info = ensure_metadata(nil, info)
|
93
|
+
crc = Zlib.crc32('', 0)
|
94
|
+
make_entry(info, '', crc)
|
95
|
+
|
96
|
+
entry = { :offset => @offset, :len => 0 }
|
97
|
+
@dir_entries << entry
|
98
|
+
|
99
|
+
header = make_entry(info, '', crc)
|
100
|
+
header.extra_field.push(UnixExtraField.new(:atime => info[:mtime].to_i,
|
101
|
+
:mtime => info[:mtime].to_i,
|
102
|
+
:uid => info[:uid].to_i,
|
103
|
+
:gid => info[:gid].to_i,
|
104
|
+
:data => target))
|
105
|
+
|
106
|
+
entry[:zip64] = header.zip64?
|
107
|
+
entry[:local_header] = header
|
108
|
+
|
109
|
+
# write output
|
110
|
+
write header.to_string
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_entry(io, info)
|
114
|
+
info = ensure_metadata(io, info)
|
115
|
+
|
116
|
+
data = io.read.to_s
|
117
|
+
crc = Zlib.crc32(data, 0)
|
118
|
+
|
119
|
+
# XXX: this doesn't fit well with the planned usage :(
|
120
|
+
entry = { :offset => @offset, :len => data.size }
|
121
|
+
@dir_entries << entry
|
122
|
+
|
123
|
+
header = make_entry(info, data, crc)
|
124
|
+
|
125
|
+
entry[:zip64] = header.zip64?
|
126
|
+
|
127
|
+
if info[:russiandolls]
|
128
|
+
first_header = header
|
129
|
+
last_header = header
|
130
|
+
|
131
|
+
info[:russiandolls].each_with_index do |doll,index|
|
132
|
+
io.rewind
|
133
|
+
doll_header = make_entry(info.merge(doll), data, crc)
|
134
|
+
doll_prefix = [0x4343, doll_header.size].pack('vv')
|
135
|
+
|
136
|
+
if (doll_header.to_string.size + doll_prefix.size) +
|
137
|
+
first_header.to_string.size > local_header_max
|
138
|
+
STDERR.puts "Can't add any more dolls! Dolls #{index}-#{dolls.size-1} omitted."
|
139
|
+
else
|
140
|
+
offset = @offset + first_header.to_string.size + doll_prefix.size
|
141
|
+
last_header.extra_field << doll_prefix << doll_header
|
142
|
+
@dir_entries << {
|
143
|
+
:offset => offset,
|
144
|
+
:len => data.size,
|
145
|
+
:local_header => doll_header,
|
146
|
+
:zip64 => doll_header.zip64?
|
147
|
+
}
|
148
|
+
last_header = doll_header
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
entry[:local_header] = header
|
154
|
+
|
155
|
+
# write output
|
156
|
+
write header.to_string
|
157
|
+
|
158
|
+
# write data (& any compression?)
|
159
|
+
write data
|
160
|
+
|
161
|
+
|
162
|
+
# write descriptor - not need with current strategy
|
163
|
+
# write DD64.new(:crc32 => crc,
|
164
|
+
# :data_len => data.size,
|
165
|
+
# :raw_data_len => data.size).to_string
|
166
|
+
end
|
167
|
+
|
168
|
+
def local_header_max
|
169
|
+
1024 * 64
|
170
|
+
end
|
171
|
+
|
172
|
+
def threshold
|
173
|
+
1024 * # kb
|
174
|
+
1024 * # mb
|
175
|
+
1024 * # gb
|
176
|
+
2 - local_header_max
|
177
|
+
end
|
178
|
+
|
179
|
+
def write_central_directory
|
180
|
+
align
|
181
|
+
@central_directory_offset = @offset
|
182
|
+
|
183
|
+
@dir_entries.each do |entry|
|
184
|
+
offset = entry[:offset]
|
185
|
+
len = entry[:len]
|
186
|
+
header = entry[:local_header]
|
187
|
+
|
188
|
+
extra_field = Zip64CDExtraField.new(
|
189
|
+
:data_len => len,
|
190
|
+
:raw_data_len => len,
|
191
|
+
:relative_offset => offset,
|
192
|
+
:disk_no => 0
|
193
|
+
)
|
194
|
+
write CDFileHeader.new(
|
195
|
+
:flags => header.flags,
|
196
|
+
:compression => header.compression,
|
197
|
+
:made_by => (3 << 8),
|
198
|
+
:last_mod_file_time => header.last_mod_file_time,
|
199
|
+
:last_mod_file_date => header.last_mod_file_date,
|
200
|
+
:crc32 => header.crc32,
|
201
|
+
:data_len => entry[:zip64] ? LEN64 : len,
|
202
|
+
:raw_data_len => entry[:zip64] ? LEN64 : len,
|
203
|
+
:filename => header.filename,
|
204
|
+
:extra_field => entry[:zip64] ? extra_field.to_string : '',
|
205
|
+
:file_comment => '',
|
206
|
+
:disk_no => entry[:zip64] ? 0xffff : 0,
|
207
|
+
:internal_file_attributes => 0,
|
208
|
+
:external_file_attributes => 0,
|
209
|
+
:rel_offset_of_local_header => entry[:zip64] ? LEN64 : offset
|
210
|
+
).to_string
|
211
|
+
end
|
212
|
+
|
213
|
+
# Central Directory Size
|
214
|
+
@central_directory_size = @offset - @central_directory_offset
|
215
|
+
end
|
216
|
+
|
217
|
+
def write_zip64_end_of_central_directory
|
218
|
+
#align
|
219
|
+
@zip64_central_directory_offset = @offset
|
220
|
+
|
221
|
+
write Zip64EOCDR.new(
|
222
|
+
:made_by => 3 << 8,
|
223
|
+
:this_disk_no => 0,
|
224
|
+
:disk_with_cd_no => 0,
|
225
|
+
:total_no_entries_on_this_disk => @dir_entries.size,
|
226
|
+
:total_no_entries => @dir_entries.size,
|
227
|
+
:size_of_cd => @central_directory_size,
|
228
|
+
:offset_of_cd_wrt_disk_no => @central_directory_offset,
|
229
|
+
:data => ''
|
230
|
+
).to_string
|
231
|
+
end
|
232
|
+
|
233
|
+
def write_zip64_end_of_central_directory_locator
|
234
|
+
#align
|
235
|
+
write Zip64EOCDL.new(
|
236
|
+
:disk_with_z64_eocdr => 0,
|
237
|
+
# Assume relative offset is relative to disk, as
|
238
|
+
# is case elsewhere in Zip spec
|
239
|
+
:relative_offset => @zip64_central_directory_offset,
|
240
|
+
:no_disks => 1
|
241
|
+
).to_string
|
242
|
+
end
|
243
|
+
|
244
|
+
def write_end_of_central_directory_record
|
245
|
+
#align
|
246
|
+
write EOCDR.new(
|
247
|
+
:disk_no => 0,
|
248
|
+
:disk_with_cd_no => 0,
|
249
|
+
:total_entries_in_local_cd => @dir_entries.size,
|
250
|
+
:total_entries => @dir_entries.size,
|
251
|
+
:cd_size => @central_directory_size,
|
252
|
+
:offset_to_cd_start => @central_directory_offset,
|
253
|
+
:file_comment => ''
|
254
|
+
).to_string
|
255
|
+
end
|
256
|
+
|
257
|
+
def close
|
258
|
+
# [archive decryption header]
|
259
|
+
# ignore
|
260
|
+
# [archive extra data record]
|
261
|
+
# ignore
|
262
|
+
|
263
|
+
# [central directory]
|
264
|
+
write_central_directory()
|
265
|
+
|
266
|
+
if @dir_entries.any? { |entry| entry[:zip64] }
|
267
|
+
# [zip64 end of central directory record]
|
268
|
+
write_zip64_end_of_central_directory()
|
269
|
+
|
270
|
+
# [zip64 end of central directory locator]
|
271
|
+
write_zip64_end_of_central_directory_locator()
|
272
|
+
end
|
273
|
+
|
274
|
+
# [end of central directory record]
|
275
|
+
write_end_of_central_directory_record()
|
276
|
+
|
277
|
+
write_last()
|
278
|
+
end
|
279
|
+
|
280
|
+
def self.get_io_size(io)
|
281
|
+
if io.respond_to?(:stat)
|
282
|
+
size = io.stat.size
|
283
|
+
elsif io.respond_to?(:size)
|
284
|
+
size = io.size
|
285
|
+
else
|
286
|
+
pos = io.tell
|
287
|
+
io.seek(0, IO::SEEK_END)
|
288
|
+
size = io.tell
|
289
|
+
io.seek(pos, IO::SEEK_START)
|
290
|
+
end
|
291
|
+
size
|
292
|
+
end
|
293
|
+
|
294
|
+
def self.predict_size64(files)
|
295
|
+
file_overhead = LocalFileHeader.base_size +
|
296
|
+
Zip64ExtraField.base_size
|
297
|
+
|
298
|
+
cd_overhead = CDFileHeader.base_size +
|
299
|
+
Zip64CDExtraField.base_size
|
300
|
+
|
301
|
+
total = 0
|
302
|
+
names_size = 0
|
303
|
+
dolls = 0
|
304
|
+
doll_names_size = 0
|
305
|
+
files.each do |file|
|
306
|
+
names_size += file[:name].size
|
307
|
+
total += get_io_size(file[:io]) + file[:name].size + file_overhead
|
308
|
+
|
309
|
+
if file[:russiandolls]
|
310
|
+
file[:russiandolls].each do |doll|
|
311
|
+
total += doll[:name].size + file_overhead + 4
|
312
|
+
dolls += 1
|
313
|
+
doll_names_size += doll[:name].size
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
until (total%4).zero?
|
319
|
+
total += 1
|
320
|
+
end
|
321
|
+
|
322
|
+
total += files.size * cd_overhead
|
323
|
+
total += names_size
|
324
|
+
|
325
|
+
total += dolls * cd_overhead
|
326
|
+
total += doll_names_size
|
327
|
+
|
328
|
+
total += EOCDR.base_size + Zip64EOCDL.base_size + Zip64EOCDR.base_size
|
329
|
+
|
330
|
+
total
|
331
|
+
end
|
332
|
+
|
333
|
+
protected
|
334
|
+
def align
|
335
|
+
until (@offset % 4).zero?
|
336
|
+
write_raw "\0"
|
337
|
+
end
|
338
|
+
end
|
339
|
+
def write(bytes)
|
340
|
+
bytes = bytes.to_string unless bytes.is_a?(String)
|
341
|
+
write_raw(bytes)
|
342
|
+
end
|
343
|
+
def write_raw(bytes)
|
344
|
+
#STDERR.puts "Write: #{'%8i' % bytes.size} @#{'%08i' % @offset}"
|
345
|
+
@io << bytes
|
346
|
+
@offset += bytes.size
|
347
|
+
end
|
348
|
+
def write_last(bytes=nil)
|
349
|
+
bytes = bytes.to_string if bytes.respond_to?(:to_string)
|
350
|
+
write_raw(bytes) unless bytes.nil? or bytes.empty?
|
351
|
+
end
|
352
|
+
|
353
|
+
def self.test
|
354
|
+
files = []
|
355
|
+
15.times do |x|
|
356
|
+
files << { :name => ("foo-%02i.txt" % x), :io => StringIO.new("Foo is #{x} and #{x * x}") }
|
357
|
+
info = files.last
|
358
|
+
(x*500).to_i.times do |y|
|
359
|
+
files.last[:io].puts "And some more lines about blah - we are foo #{x}"
|
360
|
+
end
|
361
|
+
files.last[:io].rewind
|
362
|
+
|
363
|
+
(rand()*15).to_i.times do |n|
|
364
|
+
info[:russiandolls] ||= []
|
365
|
+
info[:russiandolls] << { :name => ("%s-doll%02i.txt" % [info[:name],n]) }
|
366
|
+
end #if (i % 2).zero?
|
367
|
+
end
|
368
|
+
|
369
|
+
x = predict_size64(files)
|
370
|
+
File.open("test.zip", "w") do |fp|
|
371
|
+
ZipWriter.new(fp) do |writer|
|
372
|
+
i = 0
|
373
|
+
files.each do |info|
|
374
|
+
info = {:mtime => Time.now, :use => (i < 3 ? 32 : 64)}.merge(info)
|
375
|
+
|
376
|
+
writer.add_entry(info[:io], info)
|
377
|
+
#exit
|
378
|
+
#p info
|
379
|
+
i += 1
|
380
|
+
end
|
381
|
+
end
|
382
|
+
p [:guess, x, :actual, fp.tell, :diff, fp.tell - x]
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
class EventMachineWriter < ZipWriter
|
387
|
+
def write_raw(bytes)
|
388
|
+
@io.send_data(bytes)
|
389
|
+
@offset += bytes.size
|
390
|
+
end
|
391
|
+
def write_last(bytes=nil)
|
392
|
+
bytes = bytes.to_string if bytes.respond_to?(:to_string)
|
393
|
+
write_raw(bytes) unless bytes.nil? or bytes.empty?
|
394
|
+
@io.close_connection_after_writing
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
data/test/zip64_test.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'stringio'
|
3
|
+
$: << File.join(File.dirname(__FILE__), '../lib')
|
4
|
+
require 'zip64/writer'
|
5
|
+
|
6
|
+
class Zip64Test < Test::Unit::TestCase
|
7
|
+
def test_small_std_zip
|
8
|
+
io = StringIO.new
|
9
|
+
|
10
|
+
Zip64::ZipWriter.new(io) { |zip| zip.add_entry(StringIO.new("Foo"), :name => 'bar.txt') }
|
11
|
+
|
12
|
+
assert_equal 115, io.string.size
|
13
|
+
assert_match /^PK/, io.string
|
14
|
+
assert_match /bar.txtFoo/, io.string
|
15
|
+
end
|
16
|
+
def test_small_z64_zip
|
17
|
+
io = StringIO.new
|
18
|
+
|
19
|
+
Zip64::ZipWriter.new(io) { |zip| zip.add_entry(StringIO.new("Foo"), :name => 'bar.txt', :use => 64) }
|
20
|
+
|
21
|
+
assert_equal 243, io.string.size
|
22
|
+
assert_match /^PK/, io.string
|
23
|
+
assert_match /bar.txt.*Foo/, io.string
|
24
|
+
|
25
|
+
assert io.string.include?([Zip64::Zip64EOCDR::SIG].pack('V'))
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zip64writer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Geoff Youngs
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-10-11 00:00:00 Z
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: |
|
22
|
+
A simple library to output Zip64 zip files from pure ruby.
|
23
|
+
|
24
|
+
email: git@intersect-uk.co.uk
|
25
|
+
executables: []
|
26
|
+
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files: []
|
30
|
+
|
31
|
+
files:
|
32
|
+
- lib/zip64/writer.rb
|
33
|
+
- lib/zip64/debug.rb
|
34
|
+
- lib/zip64/structures.rb
|
35
|
+
- lib/zip64/version.rb
|
36
|
+
- test/zip64_test.rb
|
37
|
+
- Rakefile
|
38
|
+
homepage: http://github.com/geoffyoungs/zip64writer
|
39
|
+
licenses: []
|
40
|
+
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options: []
|
43
|
+
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
none: false
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
hash: 3
|
52
|
+
segments:
|
53
|
+
- 0
|
54
|
+
version: "0"
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 3
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
requirements:
|
65
|
+
- none
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.8.10
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: Zip64 Output Library
|
71
|
+
test_files:
|
72
|
+
- test/zip64_test.rb
|
73
|
+
- Rakefile
|