zip64writer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+
@@ -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
+
@@ -0,0 +1,6 @@
1
+ module Zip64
2
+ VERSION = [0,0,1]
3
+ def VERSION.to_s
4
+ map { |v| v.to_s }.join('.')
5
+ end
6
+ end
@@ -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
+
@@ -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