zip64writer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|