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 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