zip 2.0.2

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.
@@ -0,0 +1,111 @@
1
+ unless Enumerable.method_defined?(:inject)
2
+ module Enumerable #:nodoc:all
3
+ def inject(n = 0)
4
+ each { |value| n = yield(n, value) }
5
+ n
6
+ end
7
+ end
8
+ end
9
+
10
+ module Enumerable #:nodoc:all
11
+ # returns a new array of all the return values not equal to nil
12
+ # This implementation could be faster
13
+ def select_map(&aProc)
14
+ map(&aProc).reject { |e| e.nil? }
15
+ end
16
+ end
17
+
18
+ unless Object.method_defined?(:object_id)
19
+ class Object #:nodoc:all
20
+ # Using object_id which is the new thing, so we need
21
+ # to make that work in versions prior to 1.8.0
22
+ alias object_id id
23
+ end
24
+ end
25
+
26
+ unless File.respond_to?(:read)
27
+ class File # :nodoc:all
28
+ # singleton method read does not exist in 1.6.x
29
+ def self.read(fileName)
30
+ open(fileName) { |f| f.read }
31
+ end
32
+ end
33
+ end
34
+
35
+ class String #:nodoc:all
36
+ def starts_with(aString)
37
+ rindex(aString, 0) == 0
38
+ end
39
+
40
+ def ends_with(aString)
41
+ index(aString, -aString.size)
42
+ end
43
+
44
+ def ensure_end(aString)
45
+ ends_with(aString) ? self : self + aString
46
+ end
47
+
48
+ def lchop
49
+ slice(1, length)
50
+ end
51
+ end
52
+
53
+ class Time #:nodoc:all
54
+
55
+ #MS-DOS File Date and Time format as used in Interrupt 21H Function 57H:
56
+ #
57
+ # Register CX, the Time:
58
+ # Bits 0-4 2 second increments (0-29)
59
+ # Bits 5-10 minutes (0-59)
60
+ # bits 11-15 hours (0-24)
61
+ #
62
+ # Register DX, the Date:
63
+ # Bits 0-4 day (1-31)
64
+ # bits 5-8 month (1-12)
65
+ # bits 9-15 year (four digit year minus 1980)
66
+
67
+
68
+ def to_binary_dos_time
69
+ (sec/2) +
70
+ (min << 5) +
71
+ (hour << 11)
72
+ end
73
+
74
+ def to_binary_dos_date
75
+ (day) +
76
+ (month << 5) +
77
+ ((year - 1980) << 9)
78
+ end
79
+
80
+ # Dos time is only stored with two seconds accuracy
81
+ def dos_equals(other)
82
+ to_i/2 == other.to_i/2
83
+ end
84
+
85
+ def self.parse_binary_dos_format(binaryDosDate, binaryDosTime)
86
+ second = 2 * ( 0b11111 & binaryDosTime)
87
+ minute = ( 0b11111100000 & binaryDosTime) >> 5
88
+ hour = (0b1111100000000000 & binaryDosTime) >> 11
89
+ day = ( 0b11111 & binaryDosDate)
90
+ month = ( 0b111100000 & binaryDosDate) >> 5
91
+ year = ((0b1111111000000000 & binaryDosDate) >> 9) + 1980
92
+ begin
93
+ return Time.local(year, month, day, hour, minute, second)
94
+ end
95
+ end
96
+ end
97
+
98
+ class Module #:nodoc:all
99
+ def forward_message(forwarder, *messagesToForward)
100
+ methodDefs = messagesToForward.map {
101
+ |msg|
102
+ "def #{msg}; #{forwarder}(:#{msg}); end"
103
+ }
104
+ module_eval(methodDefs.join("\n"))
105
+ end
106
+ end
107
+
108
+
109
+ # Copyright (C) 2002, 2003 Thomas Sondergaard
110
+ # rubyzip is free software; you can redistribute it and/or
111
+ # modify it under the terms of the ruby license.
@@ -0,0 +1,195 @@
1
+ #
2
+ # tempfile - manipulates temporary files
3
+ #
4
+ # $Id: tempfile_bugfixed.rb,v 1.2 2005/02/19 20:30:33 thomas Exp $
5
+ #
6
+
7
+ require 'delegate'
8
+ require 'tmpdir'
9
+
10
+ module BugFix #:nodoc:all
11
+
12
+ # A class for managing temporary files. This library is written to be
13
+ # thread safe.
14
+ class Tempfile < DelegateClass(File)
15
+ MAX_TRY = 10
16
+ @@cleanlist = []
17
+
18
+ # Creates a temporary file of mode 0600 in the temporary directory
19
+ # whose name is basename.pid.n and opens with mode "w+". A Tempfile
20
+ # object works just like a File object.
21
+ #
22
+ # If tmpdir is omitted, the temporary directory is determined by
23
+ # Dir::tmpdir provided by 'tmpdir.rb'.
24
+ # When $SAFE > 0 and the given tmpdir is tainted, it uses
25
+ # /tmp. (Note that ENV values are tainted by default)
26
+ def initialize(basename, tmpdir=Dir::tmpdir)
27
+ if $SAFE > 0 and tmpdir.tainted?
28
+ tmpdir = '/tmp'
29
+ end
30
+
31
+ lock = nil
32
+ n = failure = 0
33
+
34
+ begin
35
+ Thread.critical = true
36
+
37
+ begin
38
+ tmpname = sprintf('%s/%s%d.%d', tmpdir, basename, $$, n)
39
+ lock = tmpname + '.lock'
40
+ n += 1
41
+ end while @@cleanlist.include?(tmpname) or
42
+ File.exist?(lock) or File.exist?(tmpname)
43
+
44
+ Dir.mkdir(lock)
45
+ rescue
46
+ failure += 1
47
+ retry if failure < MAX_TRY
48
+ raise "cannot generate tempfile `%s'" % tmpname
49
+ ensure
50
+ Thread.critical = false
51
+ end
52
+
53
+ @data = [tmpname]
54
+ @clean_proc = Tempfile.callback(@data)
55
+ ObjectSpace.define_finalizer(self, @clean_proc)
56
+
57
+ @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600)
58
+ @tmpname = tmpname
59
+ @@cleanlist << @tmpname
60
+ @data[1] = @tmpfile
61
+ @data[2] = @@cleanlist
62
+
63
+ super(@tmpfile)
64
+
65
+ # Now we have all the File/IO methods defined, you must not
66
+ # carelessly put bare puts(), etc. after this.
67
+
68
+ Dir.rmdir(lock)
69
+ end
70
+
71
+ # Opens or reopens the file with mode "r+".
72
+ def open
73
+ @tmpfile.close if @tmpfile
74
+ @tmpfile = File.open(@tmpname, 'r+')
75
+ @data[1] = @tmpfile
76
+ __setobj__(@tmpfile)
77
+ end
78
+
79
+ def _close # :nodoc:
80
+ @tmpfile.close if @tmpfile
81
+ @data[1] = @tmpfile = nil
82
+ end
83
+ protected :_close
84
+
85
+ # Closes the file. If the optional flag is true, unlinks the file
86
+ # after closing.
87
+ #
88
+ # If you don't explicitly unlink the temporary file, the removal
89
+ # will be delayed until the object is finalized.
90
+ def close(unlink_now=false)
91
+ if unlink_now
92
+ close!
93
+ else
94
+ _close
95
+ end
96
+ end
97
+
98
+ # Closes and unlinks the file.
99
+ def close!
100
+ _close
101
+ @clean_proc.call
102
+ ObjectSpace.undefine_finalizer(self)
103
+ end
104
+
105
+ # Unlinks the file. On UNIX-like systems, it is often a good idea
106
+ # to unlink a temporary file immediately after creating and opening
107
+ # it, because it leaves other programs zero chance to access the
108
+ # file.
109
+ def unlink
110
+ # keep this order for thread safeness
111
+ File.unlink(@tmpname) if File.exist?(@tmpname)
112
+ @@cleanlist.delete(@tmpname) if @@cleanlist
113
+ end
114
+ alias delete unlink
115
+
116
+ if RUBY_VERSION > '1.8.0'
117
+ def __setobj__(obj)
118
+ @_dc_obj = obj
119
+ end
120
+ else
121
+ def __setobj__(obj)
122
+ @obj = obj
123
+ end
124
+ end
125
+
126
+ # Returns the full path name of the temporary file.
127
+ def path
128
+ @tmpname
129
+ end
130
+
131
+ # Returns the size of the temporary file. As a side effect, the IO
132
+ # buffer is flushed before determining the size.
133
+ def size
134
+ if @tmpfile
135
+ @tmpfile.flush
136
+ @tmpfile.stat.size
137
+ else
138
+ 0
139
+ end
140
+ end
141
+ alias length size
142
+
143
+ class << self
144
+ def callback(data) # :nodoc:
145
+ pid = $$
146
+ lambda{
147
+ if pid == $$
148
+ path, tmpfile, cleanlist = *data
149
+
150
+ print "removing ", path, "..." if $DEBUG
151
+
152
+ tmpfile.close if tmpfile
153
+
154
+ # keep this order for thread safeness
155
+ File.unlink(path) if File.exist?(path)
156
+ cleanlist.delete(path) if cleanlist
157
+
158
+ print "done\n" if $DEBUG
159
+ end
160
+ }
161
+ end
162
+
163
+ # If no block is given, this is a synonym for new().
164
+ #
165
+ # If a block is given, it will be passed tempfile as an argument,
166
+ # and the tempfile will automatically be closed when the block
167
+ # terminates. In this case, open() returns nil.
168
+ def open(*args)
169
+ tempfile = new(*args)
170
+
171
+ if block_given?
172
+ begin
173
+ yield(tempfile)
174
+ ensure
175
+ tempfile.close
176
+ end
177
+
178
+ nil
179
+ else
180
+ tempfile
181
+ end
182
+ end
183
+ end
184
+ end
185
+
186
+ end # module BugFix
187
+ if __FILE__ == $0
188
+ # $DEBUG = true
189
+ f = Tempfile.new("foo")
190
+ f.print("foo\n")
191
+ f.close
192
+ f.open
193
+ p f.gets # => "foo\n"
194
+ f.close!
195
+ end
@@ -0,0 +1,3 @@
1
+ module Zip
2
+ VERSION = '2.0.2'
3
+ end
@@ -0,0 +1,1853 @@
1
+ require 'delegate'
2
+ require 'singleton'
3
+ require 'tempfile'
4
+ require 'fileutils'
5
+ require 'stringio'
6
+ require 'zlib'
7
+ require 'zip/stdrubyext'
8
+ require 'zip/ioextras'
9
+ require 'zip/version'
10
+
11
+ if Tempfile.superclass == SimpleDelegator
12
+ require 'zip/tempfile_bugfixed'
13
+ Tempfile = BugFix::Tempfile
14
+ end
15
+
16
+ module Zlib #:nodoc:all
17
+ if ! const_defined? :MAX_WBITS
18
+ MAX_WBITS = Zlib::Deflate.MAX_WBITS
19
+ end
20
+ end
21
+
22
+ module Zip
23
+
24
+ RUBY_MINOR_VERSION = RUBY_VERSION.split(".")[1].to_i
25
+
26
+ RUNNING_ON_WINDOWS = /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM
27
+
28
+ # Ruby 1.7.x compatibility
29
+ # In ruby 1.6.x and 1.8.0 reading from an empty stream returns
30
+ # an empty string the first time and then nil.
31
+ # not so in 1.7.x
32
+ EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST = RUBY_MINOR_VERSION != 7
33
+
34
+ # ZipInputStream is the basic class for reading zip entries in a
35
+ # zip file. It is possible to create a ZipInputStream object directly,
36
+ # passing the zip file name to the constructor, but more often than not
37
+ # the ZipInputStream will be obtained from a ZipFile (perhaps using the
38
+ # ZipFileSystem interface) object for a particular entry in the zip
39
+ # archive.
40
+ #
41
+ # A ZipInputStream inherits IOExtras::AbstractInputStream in order
42
+ # to provide an IO-like interface for reading from a single zip
43
+ # entry. Beyond methods for mimicking an IO-object it contains
44
+ # the method get_next_entry for iterating through the entries of
45
+ # an archive. get_next_entry returns a ZipEntry object that describes
46
+ # the zip entry the ZipInputStream is currently reading from.
47
+ #
48
+ # Example that creates a zip archive with ZipOutputStream and reads it
49
+ # back again with a ZipInputStream.
50
+ #
51
+ # require 'zip/zip'
52
+ #
53
+ # Zip::ZipOutputStream::open("my.zip") {
54
+ # |io|
55
+ #
56
+ # io.put_next_entry("first_entry.txt")
57
+ # io.write "Hello world!"
58
+ #
59
+ # io.put_next_entry("adir/first_entry.txt")
60
+ # io.write "Hello again!"
61
+ # }
62
+ #
63
+ #
64
+ # Zip::ZipInputStream::open("my.zip") {
65
+ # |io|
66
+ #
67
+ # while (entry = io.get_next_entry)
68
+ # puts "Contents of #{entry.name}: '#{io.read}'"
69
+ # end
70
+ # }
71
+ #
72
+ # java.util.zip.ZipInputStream is the original inspiration for this
73
+ # class.
74
+
75
+ class ZipInputStream
76
+ include IOExtras::AbstractInputStream
77
+
78
+ # Opens the indicated zip file. An exception is thrown
79
+ # if the specified offset in the specified filename is
80
+ # not a local zip entry header.
81
+ def initialize(filename, offset = 0)
82
+ super()
83
+ @archiveIO = filename.class == StringIO ? filename : File.open(filename, "rb")
84
+ # @archiveIO = File.open(filename, "rb")
85
+ @archiveIO.seek(offset, IO::SEEK_SET)
86
+ @decompressor = NullDecompressor.instance
87
+ @currentEntry = nil
88
+ end
89
+
90
+ def close
91
+ @archiveIO.close
92
+ end
93
+
94
+ # Same as #initialize but if a block is passed the opened
95
+ # stream is passed to the block and closed when the block
96
+ # returns.
97
+ def ZipInputStream.open(filename)
98
+ return new(filename) unless block_given?
99
+
100
+ zio = new(filename)
101
+ yield zio
102
+ ensure
103
+ zio.close if zio
104
+ end
105
+
106
+ # Returns a ZipEntry object. It is necessary to call this
107
+ # method on a newly created ZipInputStream before reading from
108
+ # the first entry in the archive. Returns nil when there are
109
+ # no more entries.
110
+
111
+ def get_next_entry
112
+ @archiveIO.seek(@currentEntry.next_header_offset,
113
+ IO::SEEK_SET) if @currentEntry
114
+ open_entry
115
+ end
116
+
117
+ # Rewinds the stream to the beginning of the current entry
118
+ def rewind
119
+ return if @currentEntry.nil?
120
+ @lineno = 0
121
+ @archiveIO.seek(@currentEntry.localHeaderOffset,
122
+ IO::SEEK_SET)
123
+ open_entry
124
+ end
125
+
126
+ # Modeled after IO.sysread
127
+ def sysread(numberOfBytes = nil, buf = nil)
128
+ @decompressor.sysread(numberOfBytes, buf)
129
+ end
130
+
131
+ def eof
132
+ @outputBuffer.empty? && @decompressor.eof
133
+ end
134
+ alias :eof? :eof
135
+
136
+ protected
137
+
138
+ def open_entry
139
+ @currentEntry = ZipEntry.read_local_entry(@archiveIO)
140
+ if (@currentEntry == nil)
141
+ @decompressor = NullDecompressor.instance
142
+ elsif @currentEntry.compression_method == ZipEntry::STORED
143
+ @decompressor = PassThruDecompressor.new(@archiveIO,
144
+ @currentEntry.size)
145
+ elsif @currentEntry.compression_method == ZipEntry::DEFLATED
146
+ @decompressor = Inflater.new(@archiveIO)
147
+ else
148
+ raise ZipCompressionMethodError,
149
+ "Unsupported compression method #{@currentEntry.compression_method}"
150
+ end
151
+ flush
152
+ return @currentEntry
153
+ end
154
+
155
+ def produce_input
156
+ @decompressor.produce_input
157
+ end
158
+
159
+ def input_finished?
160
+ @decompressor.input_finished?
161
+ end
162
+ end
163
+
164
+
165
+
166
+ class Decompressor #:nodoc:all
167
+ CHUNK_SIZE=32768
168
+ def initialize(inputStream)
169
+ super()
170
+ @inputStream=inputStream
171
+ end
172
+ end
173
+
174
+ class Inflater < Decompressor #:nodoc:all
175
+ def initialize(inputStream)
176
+ super
177
+ @zlibInflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
178
+ @outputBuffer=""
179
+ @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
180
+ end
181
+
182
+ def sysread(numberOfBytes = nil, buf = nil)
183
+ readEverything = (numberOfBytes == nil)
184
+ while (readEverything || @outputBuffer.length < numberOfBytes)
185
+ break if internal_input_finished?
186
+ @outputBuffer << internal_produce_input(buf)
187
+ end
188
+ return value_when_finished if @outputBuffer.length==0 && input_finished?
189
+ endIndex= numberOfBytes==nil ? @outputBuffer.length : numberOfBytes
190
+ return @outputBuffer.slice!(0...endIndex)
191
+ end
192
+
193
+ def produce_input
194
+ if (@outputBuffer.empty?)
195
+ return internal_produce_input
196
+ else
197
+ return @outputBuffer.slice!(0...(@outputBuffer.length))
198
+ end
199
+ end
200
+
201
+ # to be used with produce_input, not read (as read may still have more data cached)
202
+ # is data cached anywhere other than @outputBuffer? the comment above may be wrong
203
+ def input_finished?
204
+ @outputBuffer.empty? && internal_input_finished?
205
+ end
206
+ alias :eof :input_finished?
207
+ alias :eof? :input_finished?
208
+
209
+ private
210
+
211
+ def internal_produce_input(buf = nil)
212
+ retried = 0
213
+ begin
214
+ s = buf ? @inputStream.read(Decompressor::CHUNK_SIZE, buf) : @inputStream.read(Decompressor::CHUNK_SIZE)
215
+ @zlibInflater.inflate(s)
216
+ rescue Zlib::BufError
217
+ raise if (retried >= 5) # how many times should we retry?
218
+ retried += 1
219
+ retry
220
+ end
221
+ end
222
+
223
+ def internal_input_finished?
224
+ @zlibInflater.finished?
225
+ end
226
+
227
+ # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
228
+ def value_when_finished # mimic behaviour of ruby File object.
229
+ return nil if @hasReturnedEmptyString
230
+ @hasReturnedEmptyString=true
231
+ return ""
232
+ end
233
+ end
234
+
235
+ class PassThruDecompressor < Decompressor #:nodoc:all
236
+ def initialize(inputStream, charsToRead)
237
+ super inputStream
238
+ @charsToRead = charsToRead
239
+ @readSoFar = 0
240
+ @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
241
+ end
242
+
243
+ # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
244
+ def sysread(numberOfBytes = nil, buf = nil)
245
+ if input_finished?
246
+ hasReturnedEmptyStringVal=@hasReturnedEmptyString
247
+ @hasReturnedEmptyString=true
248
+ return "" unless hasReturnedEmptyStringVal
249
+ return nil
250
+ end
251
+
252
+ if (numberOfBytes == nil || @readSoFar+numberOfBytes > @charsToRead)
253
+ numberOfBytes = @charsToRead-@readSoFar
254
+ end
255
+ @readSoFar += numberOfBytes
256
+ if buf
257
+ @inputStream.read(numberOfBytes, buf)
258
+ else
259
+ @inputStream.read(numberOfBytes)
260
+ end
261
+ end
262
+
263
+ def produce_input
264
+ sysread(Decompressor::CHUNK_SIZE)
265
+ end
266
+
267
+ def input_finished?
268
+ (@readSoFar >= @charsToRead)
269
+ end
270
+ alias :eof :input_finished?
271
+ alias :eof? :input_finished?
272
+ end
273
+
274
+ class NullDecompressor #:nodoc:all
275
+ include Singleton
276
+ def sysread(numberOfBytes = nil, buf = nil)
277
+ nil
278
+ end
279
+
280
+ def produce_input
281
+ nil
282
+ end
283
+
284
+ def input_finished?
285
+ true
286
+ end
287
+
288
+ def eof
289
+ true
290
+ end
291
+ alias :eof? :eof
292
+ end
293
+
294
+ class NullInputStream < NullDecompressor #:nodoc:all
295
+ include IOExtras::AbstractInputStream
296
+ end
297
+
298
+ class ZipEntry
299
+ STORED = 0
300
+ DEFLATED = 8
301
+
302
+ FSTYPE_FAT = 0
303
+ FSTYPE_AMIGA = 1
304
+ FSTYPE_VMS = 2
305
+ FSTYPE_UNIX = 3
306
+ FSTYPE_VM_CMS = 4
307
+ FSTYPE_ATARI = 5
308
+ FSTYPE_HPFS = 6
309
+ FSTYPE_MAC = 7
310
+ FSTYPE_Z_SYSTEM = 8
311
+ FSTYPE_CPM = 9
312
+ FSTYPE_TOPS20 = 10
313
+ FSTYPE_NTFS = 11
314
+ FSTYPE_QDOS = 12
315
+ FSTYPE_ACORN = 13
316
+ FSTYPE_VFAT = 14
317
+ FSTYPE_MVS = 15
318
+ FSTYPE_BEOS = 16
319
+ FSTYPE_TANDEM = 17
320
+ FSTYPE_THEOS = 18
321
+ FSTYPE_MAC_OSX = 19
322
+ FSTYPE_ATHEOS = 30
323
+
324
+ FSTYPES = {
325
+ FSTYPE_FAT => 'FAT'.freeze,
326
+ FSTYPE_AMIGA => 'Amiga'.freeze,
327
+ FSTYPE_VMS => 'VMS (Vax or Alpha AXP)'.freeze,
328
+ FSTYPE_UNIX => 'Unix'.freeze,
329
+ FSTYPE_VM_CMS => 'VM/CMS'.freeze,
330
+ FSTYPE_ATARI => 'Atari ST'.freeze,
331
+ FSTYPE_HPFS => 'OS/2 or NT HPFS'.freeze,
332
+ FSTYPE_MAC => 'Macintosh'.freeze,
333
+ FSTYPE_Z_SYSTEM => 'Z-System'.freeze,
334
+ FSTYPE_CPM => 'CP/M'.freeze,
335
+ FSTYPE_TOPS20 => 'TOPS-20'.freeze,
336
+ FSTYPE_NTFS => 'NTFS'.freeze,
337
+ FSTYPE_QDOS => 'SMS/QDOS'.freeze,
338
+ FSTYPE_ACORN => 'Acorn RISC OS'.freeze,
339
+ FSTYPE_VFAT => 'Win32 VFAT'.freeze,
340
+ FSTYPE_MVS => 'MVS'.freeze,
341
+ FSTYPE_BEOS => 'BeOS'.freeze,
342
+ FSTYPE_TANDEM => 'Tandem NSK'.freeze,
343
+ FSTYPE_THEOS => 'Theos'.freeze,
344
+ FSTYPE_MAC_OSX => 'Mac OS/X (Darwin)'.freeze,
345
+ FSTYPE_ATHEOS => 'AtheOS'.freeze,
346
+ }.freeze
347
+
348
+ attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method,
349
+ :name, :size, :localHeaderOffset, :zipfile, :fstype, :externalFileAttributes, :gp_flags, :header_signature
350
+
351
+ attr_accessor :follow_symlinks
352
+ attr_accessor :restore_times, :restore_permissions, :restore_ownership
353
+ attr_accessor :unix_uid, :unix_gid, :unix_perms
354
+
355
+ attr_reader :ftype, :filepath # :nodoc:
356
+
357
+ def initialize(zipfile = "", name = "", comment = "", extra = "",
358
+ compressed_size = 0, crc = 0,
359
+ compression_method = ZipEntry::DEFLATED, size = 0,
360
+ time = Time.now)
361
+ super()
362
+ if name.starts_with("/")
363
+ raise ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
364
+ end
365
+ @localHeaderOffset = 0
366
+ @internalFileAttributes = 1
367
+ @externalFileAttributes = 0
368
+ @version = 52 # this library's version
369
+ @ftype = nil # unspecified or unknown
370
+ @filepath = nil
371
+ if Zip::RUNNING_ON_WINDOWS
372
+ @fstype = FSTYPE_FAT
373
+ else
374
+ @fstype = FSTYPE_UNIX
375
+ end
376
+ @zipfile, @comment, @compressed_size, @crc, @extra, @compression_method,
377
+ @name, @size = zipfile, comment, compressed_size, crc,
378
+ extra, compression_method, name, size
379
+ @time = time
380
+
381
+ @follow_symlinks = false
382
+
383
+ @restore_times = true
384
+ @restore_permissions = false
385
+ @restore_ownership = false
386
+
387
+ # BUG: need an extra field to support uid/gid's
388
+ @unix_uid = nil
389
+ @unix_gid = nil
390
+ @unix_perms = nil
391
+ # @posix_acl = nil
392
+ # @ntfs_acl = nil
393
+
394
+ if name_is_directory?
395
+ @ftype = :directory
396
+ else
397
+ @ftype = :file
398
+ end
399
+
400
+ unless ZipExtraField === @extra
401
+ @extra = ZipExtraField.new(@extra.to_s)
402
+ end
403
+ end
404
+
405
+ def time
406
+ if @extra["UniversalTime"]
407
+ @extra["UniversalTime"].mtime
408
+ else
409
+ # Atandard time field in central directory has local time
410
+ # under archive creator. Then, we can't get timezone.
411
+ @time
412
+ end
413
+ end
414
+ alias :mtime :time
415
+
416
+ def time=(aTime)
417
+ unless @extra.member?("UniversalTime")
418
+ @extra.create("UniversalTime")
419
+ end
420
+ @extra["UniversalTime"].mtime = aTime
421
+ @time = aTime
422
+ end
423
+
424
+ # Returns +true+ if the entry is a directory.
425
+ def directory?
426
+ raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype
427
+ @ftype == :directory
428
+ end
429
+ alias :is_directory :directory?
430
+
431
+ # Returns +true+ if the entry is a file.
432
+ def file?
433
+ raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype
434
+ @ftype == :file
435
+ end
436
+
437
+ # Returns +true+ if the entry is a symlink.
438
+ def symlink?
439
+ raise ZipInternalError, "current filetype is unknown: #{self.inspect}" unless @ftype
440
+ @ftype == :link
441
+ end
442
+
443
+ def name_is_directory? #:nodoc:all
444
+ (%r{\/$} =~ @name) != nil
445
+ end
446
+
447
+ def local_entry_offset #:nodoc:all
448
+ localHeaderOffset + local_header_size
449
+ end
450
+
451
+ def local_header_size #:nodoc:all
452
+ LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + (@extra ? @extra.local_size : 0)
453
+ end
454
+
455
+ def cdir_header_size #:nodoc:all
456
+ CDIR_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) +
457
+ (@extra ? @extra.c_dir_size : 0) + (@comment ? @comment.size : 0)
458
+ end
459
+
460
+ def next_header_offset #:nodoc:all
461
+ local_entry_offset + self.compressed_size
462
+ end
463
+
464
+ # Extracts entry to file destPath (defaults to @name).
465
+ def extract(destPath = @name, &onExistsProc)
466
+ onExistsProc ||= proc { false }
467
+
468
+ if directory?
469
+ create_directory(destPath, &onExistsProc)
470
+ elsif file?
471
+ write_file(destPath, &onExistsProc)
472
+ elsif symlink?
473
+ create_symlink(destPath, &onExistsProc)
474
+ else
475
+ raise RuntimeError, "unknown file type #{self.inspect}"
476
+ end
477
+
478
+ self
479
+ end
480
+
481
+ def to_s
482
+ @name
483
+ end
484
+
485
+ protected
486
+
487
+ def ZipEntry.read_zip_short(io) # :nodoc:
488
+ io.read(2).unpack('v')[0]
489
+ end
490
+
491
+ def ZipEntry.read_zip_long(io) # :nodoc:
492
+ io.read(4).unpack('V')[0]
493
+ end
494
+ public
495
+
496
+ LOCAL_ENTRY_SIGNATURE = 0x04034b50
497
+ LOCAL_ENTRY_STATIC_HEADER_LENGTH = 30
498
+ LOCAL_ENTRY_TRAILING_DESCRIPTOR_LENGTH = 4+4+4
499
+
500
+ def read_local_entry(io) #:nodoc:all
501
+ @localHeaderOffset = io.tell
502
+ staticSizedFieldsBuf = io.read(LOCAL_ENTRY_STATIC_HEADER_LENGTH)
503
+ unless (staticSizedFieldsBuf.size==LOCAL_ENTRY_STATIC_HEADER_LENGTH)
504
+ raise ZipError, "Premature end of file. Not enough data for zip entry local header"
505
+ end
506
+
507
+ @header_signature ,
508
+ @version ,
509
+ @fstype ,
510
+ @gp_flags ,
511
+ @compression_method,
512
+ lastModTime ,
513
+ lastModDate ,
514
+ @crc ,
515
+ @compressed_size ,
516
+ @size ,
517
+ nameLength ,
518
+ extraLength = staticSizedFieldsBuf.unpack('VCCvvvvVVVvv')
519
+
520
+ unless (@header_signature == LOCAL_ENTRY_SIGNATURE)
521
+ raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
522
+ end
523
+ set_time(lastModDate, lastModTime)
524
+
525
+ @name = io.read(nameLength)
526
+ extra = io.read(extraLength)
527
+
528
+ if (extra && extra.length != extraLength)
529
+ raise ZipError, "Truncated local zip entry header"
530
+ else
531
+ if ZipExtraField === @extra
532
+ @extra.merge(extra)
533
+ else
534
+ @extra = ZipExtraField.new(extra)
535
+ end
536
+ end
537
+ end
538
+
539
+ def ZipEntry.read_local_entry(io)
540
+ entry = new(io.path)
541
+ entry.read_local_entry(io)
542
+ return entry
543
+ rescue ZipError
544
+ return nil
545
+ end
546
+
547
+ def write_local_entry(io) #:nodoc:all
548
+ @localHeaderOffset = io.tell
549
+
550
+ io <<
551
+ [LOCAL_ENTRY_SIGNATURE ,
552
+ 0 ,
553
+ 0 , # @gp_flags ,
554
+ @compression_method ,
555
+ @time.to_binary_dos_time , # @lastModTime ,
556
+ @time.to_binary_dos_date , # @lastModDate ,
557
+ @crc ,
558
+ @compressed_size ,
559
+ @size ,
560
+ @name ? @name.length : 0,
561
+ @extra? @extra.local_length : 0 ].pack('VvvvvvVVVvv')
562
+ io << @name
563
+ io << (@extra ? @extra.to_local_bin : "")
564
+ end
565
+
566
+ CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
567
+ CDIR_ENTRY_STATIC_HEADER_LENGTH = 46
568
+
569
+ def read_c_dir_entry(io) #:nodoc:all
570
+ staticSizedFieldsBuf = io.read(CDIR_ENTRY_STATIC_HEADER_LENGTH)
571
+ unless (staticSizedFieldsBuf.size == CDIR_ENTRY_STATIC_HEADER_LENGTH)
572
+ raise ZipError, "Premature end of file. Not enough data for zip cdir entry header"
573
+ end
574
+
575
+ @header_signature ,
576
+ @version , # version of encoding software
577
+ @fstype , # filesystem type
578
+ @versionNeededToExtract,
579
+ @gp_flags ,
580
+ @compression_method ,
581
+ lastModTime ,
582
+ lastModDate ,
583
+ @crc ,
584
+ @compressed_size ,
585
+ @size ,
586
+ nameLength ,
587
+ extraLength ,
588
+ commentLength ,
589
+ diskNumberStart ,
590
+ @internalFileAttributes,
591
+ @externalFileAttributes,
592
+ @localHeaderOffset ,
593
+ @name ,
594
+ @extra ,
595
+ @comment = staticSizedFieldsBuf.unpack('VCCvvvvvVVVvvvvvVV')
596
+
597
+ unless (@header_signature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE)
598
+ raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
599
+ end
600
+ set_time(lastModDate, lastModTime)
601
+
602
+ @name = io.read(nameLength)
603
+ if ZipExtraField === @extra
604
+ @extra.merge(io.read(extraLength))
605
+ else
606
+ @extra = ZipExtraField.new(io.read(extraLength))
607
+ end
608
+ @comment = io.read(commentLength)
609
+ unless (@comment && @comment.length == commentLength)
610
+ raise ZipError, "Truncated cdir zip entry header"
611
+ end
612
+
613
+ case @fstype
614
+ when FSTYPE_UNIX
615
+ @unix_perms = (@externalFileAttributes >> 16) & 07777
616
+
617
+ case (@externalFileAttributes >> 28)
618
+ when 04
619
+ @ftype = :directory
620
+ when 010
621
+ @ftype = :file
622
+ when 012
623
+ @ftype = :link
624
+ else
625
+ raise ZipInternalError, "unknown file type #{'0%o' % (@externalFileAttributes >> 28)}"
626
+ end
627
+ else
628
+ if name_is_directory?
629
+ @ftype = :directory
630
+ else
631
+ @ftype = :file
632
+ end
633
+ end
634
+ end
635
+
636
+ def ZipEntry.read_c_dir_entry(io) #:nodoc:all
637
+ entry = new(io.path)
638
+ entry.read_c_dir_entry(io)
639
+ return entry
640
+ rescue ZipError
641
+ return nil
642
+ end
643
+
644
+ def file_stat(path) # :nodoc:
645
+ if @follow_symlinks
646
+ return File::stat(path)
647
+ else
648
+ return File::lstat(path)
649
+ end
650
+ end
651
+
652
+ def get_extra_attributes_from_path(path) # :nodoc:
653
+ unless Zip::RUNNING_ON_WINDOWS
654
+ stat = file_stat(path)
655
+ @unix_uid = stat.uid
656
+ @unix_gid = stat.gid
657
+ @unix_perms = stat.mode & 07777
658
+ end
659
+ end
660
+
661
+ def set_extra_attributes_on_path(destPath) # :nodoc:
662
+ return unless (file? or directory?)
663
+
664
+ case @fstype
665
+ when FSTYPE_UNIX
666
+ # BUG: does not update timestamps into account
667
+ # ignore setuid/setgid bits by default. honor if @restore_ownership
668
+ unix_perms_mask = 01777
669
+ unix_perms_mask = 07777 if (@restore_ownership)
670
+ File::chmod(@unix_perms & unix_perms_mask, destPath) if (@restore_permissions && @unix_perms)
671
+ File::chown(@unix_uid, @unix_gid, destPath) if (@restore_ownership && @unix_uid && @unix_gid && Process::egid == 0)
672
+ # File::utimes()
673
+ end
674
+ end
675
+
676
+ def write_c_dir_entry(io) #:nodoc:all
677
+ case @fstype
678
+ when FSTYPE_UNIX
679
+ ft = nil
680
+ case @ftype
681
+ when :file
682
+ ft = 010
683
+ @unix_perms ||= 0644
684
+ when :directory
685
+ ft = 004
686
+ @unix_perms ||= 0755
687
+ when :symlink
688
+ ft = 012
689
+ @unix_perms ||= 0755
690
+ else
691
+ raise ZipInternalError, "unknown file type #{self.inspect}"
692
+ end
693
+
694
+ @externalFileAttributes = (ft << 12 | (@unix_perms & 07777)) << 16
695
+ end
696
+
697
+ io <<
698
+ [CENTRAL_DIRECTORY_ENTRY_SIGNATURE,
699
+ @version , # version of encoding software
700
+ @fstype , # filesystem type
701
+ 0 , # @versionNeededToExtract ,
702
+ 0 , # @gp_flags ,
703
+ @compression_method ,
704
+ @time.to_binary_dos_time , # @lastModTime ,
705
+ @time.to_binary_dos_date , # @lastModDate ,
706
+ @crc ,
707
+ @compressed_size ,
708
+ @size ,
709
+ @name ? @name.length : 0 ,
710
+ @extra ? @extra.c_dir_length : 0 ,
711
+ @comment ? comment.length : 0 ,
712
+ 0 , # disk number start
713
+ @internalFileAttributes , # file type (binary=0, text=1)
714
+ @externalFileAttributes , # native filesystem attributes
715
+ @localHeaderOffset ,
716
+ @name ,
717
+ @extra ,
718
+ @comment ].pack('VCCvvvvvVVVvvvvvVV')
719
+
720
+ io << @name
721
+ io << (@extra ? @extra.to_c_dir_bin : "")
722
+ io << @comment
723
+ end
724
+
725
+ def == (other)
726
+ return false unless other.class == self.class
727
+ # Compares contents of local entry and exposed fields
728
+ (@compression_method == other.compression_method &&
729
+ @crc == other.crc &&
730
+ @compressed_size == other.compressed_size &&
731
+ @size == other.size &&
732
+ @name == other.name &&
733
+ @extra == other.extra &&
734
+ @filepath == other.filepath &&
735
+ self.time.dos_equals(other.time))
736
+ end
737
+
738
+ def <=> (other)
739
+ return to_s <=> other.to_s
740
+ end
741
+
742
+ # Returns an IO like object for the given ZipEntry.
743
+ # Warning: may behave weird with symlinks.
744
+ def get_input_stream(&aProc)
745
+ if @ftype == :directory
746
+ return yield(NullInputStream.instance) if block_given?
747
+ return NullInputStream.instance
748
+ elsif @filepath
749
+ case @ftype
750
+ when :file
751
+ return File.open(@filepath, "rb", &aProc)
752
+
753
+ when :symlink
754
+ linkpath = File::readlink(@filepath)
755
+ stringio = StringIO.new(linkpath)
756
+ return yield(stringio) if block_given?
757
+ return stringio
758
+ else
759
+ raise "unknown @ftype #{@ftype}"
760
+ end
761
+ else
762
+ zis = ZipInputStream.new(@zipfile, localHeaderOffset)
763
+ zis.get_next_entry
764
+ if block_given?
765
+ begin
766
+ return yield(zis)
767
+ ensure
768
+ zis.close
769
+ end
770
+ else
771
+ return zis
772
+ end
773
+ end
774
+ end
775
+
776
+ def gather_fileinfo_from_srcpath(srcPath) # :nodoc:
777
+ stat = file_stat(srcPath)
778
+ case stat.ftype
779
+ when 'file'
780
+ if name_is_directory?
781
+ raise ArgumentError,
782
+ "entry name '#{newEntry}' indicates directory entry, but "+
783
+ "'#{srcPath}' is not a directory"
784
+ end
785
+ @ftype = :file
786
+ when 'directory'
787
+ if ! name_is_directory?
788
+ @name += "/"
789
+ end
790
+ @ftype = :directory
791
+ when 'link'
792
+ if name_is_directory?
793
+ raise ArgumentError,
794
+ "entry name '#{newEntry}' indicates directory entry, but "+
795
+ "'#{srcPath}' is not a directory"
796
+ end
797
+ @ftype = :symlink
798
+ else
799
+ raise RuntimeError, "unknown file type: #{srcPath.inspect} #{stat.inspect}"
800
+ end
801
+
802
+ @filepath = srcPath
803
+ get_extra_attributes_from_path(@filepath)
804
+ end
805
+
806
+ def write_to_zip_output_stream(aZipOutputStream) #:nodoc:all
807
+ if @ftype == :directory
808
+ aZipOutputStream.put_next_entry(self)
809
+ elsif @filepath
810
+ aZipOutputStream.put_next_entry(self)
811
+ get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) }
812
+ else
813
+ aZipOutputStream.copy_raw_entry(self)
814
+ end
815
+ end
816
+
817
+ def parent_as_string
818
+ entry_name = name.chomp("/")
819
+ slash_index = entry_name.rindex("/")
820
+ slash_index ? entry_name.slice(0, slash_index+1) : nil
821
+ end
822
+
823
+ def get_raw_input_stream(&aProc)
824
+ File.open(@zipfile, "rb", &aProc)
825
+ end
826
+
827
+ private
828
+
829
+ def set_time(binaryDosDate, binaryDosTime)
830
+ @time = Time.parse_binary_dos_format(binaryDosDate, binaryDosTime)
831
+ rescue ArgumentError
832
+ puts "Invalid date/time in zip entry"
833
+ end
834
+
835
+ def write_file(destPath, continueOnExistsProc = proc { false })
836
+ if File.exists?(destPath) && ! yield(self, destPath)
837
+ raise ZipDestinationFileExistsError,
838
+ "Destination '#{destPath}' already exists"
839
+ end
840
+ File.open(destPath, "wb") do |os|
841
+ get_input_stream do |is|
842
+ set_extra_attributes_on_path(destPath)
843
+
844
+ buf = ''
845
+ while buf = is.sysread(Decompressor::CHUNK_SIZE, buf)
846
+ os << buf
847
+ end
848
+ end
849
+ end
850
+ end
851
+
852
+ def create_directory(destPath)
853
+ if File.directory? destPath
854
+ return
855
+ elsif File.exists? destPath
856
+ if block_given? && yield(self, destPath)
857
+ FileUtils.rm(destPath)
858
+ else
859
+ raise ZipDestinationFileExistsError,
860
+ "Cannot create directory '#{destPath}'. "+
861
+ "A file already exists with that name"
862
+ end
863
+ end
864
+ Dir.mkdir destPath
865
+ set_extra_attributes_on_path(destPath)
866
+ end
867
+
868
+ # BUG: create_symlink() does not use &onExistsProc
869
+ def create_symlink(destPath)
870
+ stat = nil
871
+ begin
872
+ stat = File::lstat(destPath)
873
+ rescue Errno::ENOENT
874
+ end
875
+
876
+ io = get_input_stream
877
+ linkto = io.read
878
+
879
+ if stat
880
+ if stat.symlink?
881
+ if File::readlink(destPath) == linkto
882
+ return
883
+ else
884
+ raise ZipDestinationFileExistsError,
885
+ "Cannot create symlink '#{destPath}'. "+
886
+ "A symlink already exists with that name"
887
+ end
888
+ else
889
+ raise ZipDestinationFileExistsError,
890
+ "Cannot create symlink '#{destPath}'. "+
891
+ "A file already exists with that name"
892
+ end
893
+ end
894
+
895
+ File::symlink(linkto, destPath)
896
+ end
897
+ end
898
+
899
+
900
+ # ZipOutputStream is the basic class for writing zip files. It is
901
+ # possible to create a ZipOutputStream object directly, passing
902
+ # the zip file name to the constructor, but more often than not
903
+ # the ZipOutputStream will be obtained from a ZipFile (perhaps using the
904
+ # ZipFileSystem interface) object for a particular entry in the zip
905
+ # archive.
906
+ #
907
+ # A ZipOutputStream inherits IOExtras::AbstractOutputStream in order
908
+ # to provide an IO-like interface for writing to a single zip
909
+ # entry. Beyond methods for mimicking an IO-object it contains
910
+ # the method put_next_entry that closes the current entry
911
+ # and creates a new.
912
+ #
913
+ # Please refer to ZipInputStream for example code.
914
+ #
915
+ # java.util.zip.ZipOutputStream is the original inspiration for this
916
+ # class.
917
+
918
+ class ZipOutputStream
919
+ include IOExtras::AbstractOutputStream
920
+
921
+ attr_accessor :comment
922
+
923
+ # Opens the indicated zip file. If a file with that name already
924
+ # exists it will be overwritten.
925
+ def initialize(fileName)
926
+ super()
927
+ @fileName = fileName
928
+ @outputStream = File.new(@fileName, "wb")
929
+ @entrySet = ZipEntrySet.new
930
+ @compressor = NullCompressor.instance
931
+ @closed = false
932
+ @currentEntry = nil
933
+ @comment = nil
934
+ end
935
+
936
+ # Same as #initialize but if a block is passed the opened
937
+ # stream is passed to the block and closed when the block
938
+ # returns.
939
+ def ZipOutputStream.open(fileName)
940
+ return new(fileName) unless block_given?
941
+ zos = new(fileName)
942
+ yield zos
943
+ ensure
944
+ zos.close if zos
945
+ end
946
+
947
+ # Closes the stream and writes the central directory to the zip file
948
+ def close
949
+ return if @closed
950
+ finalize_current_entry
951
+ update_local_headers
952
+ write_central_directory
953
+ @outputStream.close
954
+ @closed = true
955
+ end
956
+
957
+ # Closes the current entry and opens a new for writing.
958
+ # +entry+ can be a ZipEntry object or a string.
959
+ def put_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION)
960
+ raise ZipError, "zip stream is closed" if @closed
961
+ newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@fileName, entry.to_s)
962
+ init_next_entry(newEntry, level)
963
+ @currentEntry=newEntry
964
+ end
965
+
966
+ def copy_raw_entry(entry)
967
+ entry = entry.dup
968
+ raise ZipError, "zip stream is closed" if @closed
969
+ raise ZipError, "entry is not a ZipEntry" if !entry.kind_of?(ZipEntry)
970
+ finalize_current_entry
971
+ @entrySet << entry
972
+ src_pos = entry.local_entry_offset
973
+ entry.write_local_entry(@outputStream)
974
+ @compressor = NullCompressor.instance
975
+ @outputStream << entry.get_raw_input_stream {
976
+ |is|
977
+ is.seek(src_pos, IO::SEEK_SET)
978
+ is.read(entry.compressed_size)
979
+ }
980
+ @compressor = NullCompressor.instance
981
+ @currentEntry = nil
982
+ end
983
+
984
+ private
985
+ def finalize_current_entry
986
+ return unless @currentEntry
987
+ finish
988
+ @currentEntry.compressed_size = @outputStream.tell - @currentEntry.localHeaderOffset -
989
+ @currentEntry.local_header_size
990
+ @currentEntry.size = @compressor.size
991
+ @currentEntry.crc = @compressor.crc
992
+ @currentEntry = nil
993
+ @compressor = NullCompressor.instance
994
+ end
995
+
996
+ def init_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION)
997
+ finalize_current_entry
998
+ @entrySet << entry
999
+ entry.write_local_entry(@outputStream)
1000
+ @compressor = get_compressor(entry, level)
1001
+ end
1002
+
1003
+ def get_compressor(entry, level)
1004
+ case entry.compression_method
1005
+ when ZipEntry::DEFLATED then Deflater.new(@outputStream, level)
1006
+ when ZipEntry::STORED then PassThruCompressor.new(@outputStream)
1007
+ else raise ZipCompressionMethodError,
1008
+ "Invalid compression method: '#{entry.compression_method}'"
1009
+ end
1010
+ end
1011
+
1012
+ def update_local_headers
1013
+ pos = @outputStream.tell
1014
+ @entrySet.each {
1015
+ |entry|
1016
+ @outputStream.pos = entry.localHeaderOffset
1017
+ entry.write_local_entry(@outputStream)
1018
+ }
1019
+ @outputStream.pos = pos
1020
+ end
1021
+
1022
+ def write_central_directory
1023
+ cdir = ZipCentralDirectory.new(@entrySet, @comment)
1024
+ cdir.write_to_stream(@outputStream)
1025
+ end
1026
+
1027
+ protected
1028
+
1029
+ def finish
1030
+ @compressor.finish
1031
+ end
1032
+
1033
+ public
1034
+ # Modeled after IO.<<
1035
+ def << (data)
1036
+ @compressor << data
1037
+ end
1038
+ end
1039
+
1040
+
1041
+ class Compressor #:nodoc:all
1042
+ def finish
1043
+ end
1044
+ end
1045
+
1046
+ class PassThruCompressor < Compressor #:nodoc:all
1047
+ def initialize(outputStream)
1048
+ super()
1049
+ @outputStream = outputStream
1050
+ @crc = Zlib::crc32
1051
+ @size = 0
1052
+ end
1053
+
1054
+ def << (data)
1055
+ val = data.to_s
1056
+ @crc = Zlib::crc32(val, @crc)
1057
+ @size += val.size
1058
+ @outputStream << val
1059
+ end
1060
+
1061
+ attr_reader :size, :crc
1062
+ end
1063
+
1064
+ class NullCompressor < Compressor #:nodoc:all
1065
+ include Singleton
1066
+
1067
+ def << (data)
1068
+ raise IOError, "closed stream"
1069
+ end
1070
+
1071
+ attr_reader :size, :compressed_size
1072
+ end
1073
+
1074
+ class Deflater < Compressor #:nodoc:all
1075
+ def initialize(outputStream, level = Zlib::DEFAULT_COMPRESSION)
1076
+ super()
1077
+ @outputStream = outputStream
1078
+ @zlibDeflater = Zlib::Deflate.new(level, -Zlib::MAX_WBITS)
1079
+ @size = 0
1080
+ @crc = Zlib::crc32
1081
+ end
1082
+
1083
+ def << (data)
1084
+ val = data.to_s
1085
+ @crc = Zlib::crc32(val, @crc)
1086
+ @size += val.size
1087
+ @outputStream << @zlibDeflater.deflate(data)
1088
+ end
1089
+
1090
+ def finish
1091
+ until @zlibDeflater.finished?
1092
+ @outputStream << @zlibDeflater.finish
1093
+ end
1094
+ end
1095
+
1096
+ attr_reader :size, :crc
1097
+ end
1098
+
1099
+
1100
+ class ZipEntrySet #:nodoc:all
1101
+ include Enumerable
1102
+
1103
+ def initialize(anEnumerable = [])
1104
+ super()
1105
+ @entrySet = {}
1106
+ anEnumerable.each { |o| push(o) }
1107
+ end
1108
+
1109
+ def include?(entry)
1110
+ @entrySet.include?(entry.to_s)
1111
+ end
1112
+
1113
+ def <<(entry)
1114
+ @entrySet[entry.to_s] = entry
1115
+ end
1116
+ alias :push :<<
1117
+
1118
+ def size
1119
+ @entrySet.size
1120
+ end
1121
+ alias :length :size
1122
+
1123
+ def delete(entry)
1124
+ @entrySet.delete(entry.to_s) ? entry : nil
1125
+ end
1126
+
1127
+ def each(&aProc)
1128
+ @entrySet.values.each(&aProc)
1129
+ end
1130
+
1131
+ def entries
1132
+ @entrySet.values
1133
+ end
1134
+
1135
+ # deep clone
1136
+ def dup
1137
+ newZipEntrySet = ZipEntrySet.new(@entrySet.values.map { |e| e.dup })
1138
+ end
1139
+
1140
+ def == (other)
1141
+ return false unless other.kind_of?(ZipEntrySet)
1142
+ return @entrySet == other.entrySet
1143
+ end
1144
+
1145
+ def parent(entry)
1146
+ @entrySet[entry.parent_as_string]
1147
+ end
1148
+
1149
+ def glob(pattern, flags = File::FNM_PATHNAME|File::FNM_DOTMATCH)
1150
+ entries.select {
1151
+ |entry|
1152
+ File.fnmatch(pattern, entry.name.chomp('/'), flags)
1153
+ }
1154
+ end
1155
+
1156
+ #TODO attr_accessor :auto_create_directories
1157
+ protected
1158
+ attr_accessor :entrySet
1159
+ end
1160
+
1161
+
1162
+ class ZipCentralDirectory
1163
+ include Enumerable
1164
+
1165
+ END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50
1166
+ MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE = 65536 + 18
1167
+ STATIC_EOCD_SIZE = 22
1168
+
1169
+ attr_reader :comment
1170
+
1171
+ # Returns an Enumerable containing the entries.
1172
+ def entries
1173
+ @entrySet.entries
1174
+ end
1175
+
1176
+ def initialize(entries = ZipEntrySet.new, comment = "") #:nodoc:
1177
+ super()
1178
+ @entrySet = entries.kind_of?(ZipEntrySet) ? entries : ZipEntrySet.new(entries)
1179
+ @comment = comment
1180
+ end
1181
+
1182
+ def write_to_stream(io) #:nodoc:
1183
+ offset = io.tell
1184
+ @entrySet.each { |entry| entry.write_c_dir_entry(io) }
1185
+ write_e_o_c_d(io, offset)
1186
+ end
1187
+
1188
+ def write_e_o_c_d(io, offset) #:nodoc:
1189
+ io <<
1190
+ [END_OF_CENTRAL_DIRECTORY_SIGNATURE,
1191
+ 0 , # @numberOfThisDisk
1192
+ 0 , # @numberOfDiskWithStartOfCDir
1193
+ @entrySet? @entrySet.size : 0 ,
1194
+ @entrySet? @entrySet.size : 0 ,
1195
+ cdir_size ,
1196
+ offset ,
1197
+ @comment ? @comment.length : 0 ].pack('VvvvvVVv')
1198
+ io << @comment
1199
+ end
1200
+ private :write_e_o_c_d
1201
+
1202
+ def cdir_size #:nodoc:
1203
+ # does not include eocd
1204
+ @entrySet.inject(0) { |value, entry| entry.cdir_header_size + value }
1205
+ end
1206
+ private :cdir_size
1207
+
1208
+ def read_e_o_c_d(io) #:nodoc:
1209
+ buf = get_e_o_c_d(io)
1210
+ @numberOfThisDisk = ZipEntry::read_zip_short(buf)
1211
+ @numberOfDiskWithStartOfCDir = ZipEntry::read_zip_short(buf)
1212
+ @totalNumberOfEntriesInCDirOnThisDisk = ZipEntry::read_zip_short(buf)
1213
+ @size = ZipEntry::read_zip_short(buf)
1214
+ @sizeInBytes = ZipEntry::read_zip_long(buf)
1215
+ @cdirOffset = ZipEntry::read_zip_long(buf)
1216
+ commentLength = ZipEntry::read_zip_short(buf)
1217
+ @comment = buf.read(commentLength)
1218
+ raise ZipError, "Zip consistency problem while reading eocd structure" unless buf.size == 0
1219
+ end
1220
+
1221
+ def read_central_directory_entries(io) #:nodoc:
1222
+ begin
1223
+ io.seek(@cdirOffset, IO::SEEK_SET)
1224
+ rescue Errno::EINVAL
1225
+ raise ZipError, "Zip consistency problem while reading central directory entry"
1226
+ end
1227
+ @entrySet = ZipEntrySet.new
1228
+ @size.times {
1229
+ @entrySet << ZipEntry.read_c_dir_entry(io)
1230
+ }
1231
+ end
1232
+
1233
+ def read_from_stream(io) #:nodoc:
1234
+ read_e_o_c_d(io)
1235
+ read_central_directory_entries(io)
1236
+ end
1237
+
1238
+ def get_e_o_c_d(io) #:nodoc:
1239
+ begin
1240
+ io.seek(-MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE, IO::SEEK_END)
1241
+ rescue Errno::EINVAL
1242
+ io.seek(0, IO::SEEK_SET)
1243
+ rescue Errno::EFBIG # FreeBSD 4.9 raise Errno::EFBIG instead of Errno::EINVAL
1244
+ io.seek(0, IO::SEEK_SET)
1245
+ end
1246
+
1247
+ # 'buf = io.read' substituted with lump of code to work around FreeBSD 4.5 issue
1248
+ retried = false
1249
+ buf = nil
1250
+ begin
1251
+ buf = io.read
1252
+ rescue Errno::EFBIG # FreeBSD 4.5 may raise Errno::EFBIG
1253
+ raise if (retried)
1254
+ retried = true
1255
+
1256
+ io.seek(0, IO::SEEK_SET)
1257
+ retry
1258
+ end
1259
+
1260
+ sigIndex = buf.rindex([END_OF_CENTRAL_DIRECTORY_SIGNATURE].pack('V'))
1261
+ raise ZipError, "Zip end of central directory signature not found" unless sigIndex
1262
+ buf=buf.slice!((sigIndex+4)...(buf.size))
1263
+ def buf.read(count)
1264
+ slice!(0, count)
1265
+ end
1266
+ return buf
1267
+ end
1268
+
1269
+ # For iterating over the entries.
1270
+ def each(&proc)
1271
+ @entrySet.each(&proc)
1272
+ end
1273
+
1274
+ # Returns the number of entries in the central directory (and
1275
+ # consequently in the zip archive).
1276
+ def size
1277
+ @entrySet.size
1278
+ end
1279
+
1280
+ def ZipCentralDirectory.read_from_stream(io) #:nodoc:
1281
+ cdir = new
1282
+ cdir.read_from_stream(io)
1283
+ return cdir
1284
+ rescue ZipError
1285
+ return nil
1286
+ end
1287
+
1288
+ def == (other) #:nodoc:
1289
+ return false unless other.kind_of?(ZipCentralDirectory)
1290
+ @entrySet.entries.sort == other.entries.sort && comment == other.comment
1291
+ end
1292
+ end
1293
+
1294
+
1295
+ class ZipError < StandardError ; end
1296
+
1297
+ class ZipEntryExistsError < ZipError; end
1298
+ class ZipDestinationFileExistsError < ZipError; end
1299
+ class ZipCompressionMethodError < ZipError; end
1300
+ class ZipEntryNameError < ZipError; end
1301
+ class ZipInternalError < ZipError; end
1302
+
1303
+ # ZipFile is modeled after java.util.zip.ZipFile from the Java SDK.
1304
+ # The most important methods are those inherited from
1305
+ # ZipCentralDirectory for accessing information about the entries in
1306
+ # the archive and methods such as get_input_stream and
1307
+ # get_output_stream for reading from and writing entries to the
1308
+ # archive. The class includes a few convenience methods such as
1309
+ # #extract for extracting entries to the filesystem, and #remove,
1310
+ # #replace, #rename and #mkdir for making simple modifications to
1311
+ # the archive.
1312
+ #
1313
+ # Modifications to a zip archive are not committed until #commit or
1314
+ # #close is called. The method #open accepts a block following
1315
+ # the pattern from File.open offering a simple way to
1316
+ # automatically close the archive when the block returns.
1317
+ #
1318
+ # The following example opens zip archive <code>my.zip</code>
1319
+ # (creating it if it doesn't exist) and adds an entry
1320
+ # <code>first.txt</code> and a directory entry <code>a_dir</code>
1321
+ # to it.
1322
+ #
1323
+ # require 'zip/zip'
1324
+ #
1325
+ # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) {
1326
+ # |zipfile|
1327
+ # zipfile.get_output_stream("first.txt") { |f| f.puts "Hello from ZipFile" }
1328
+ # zipfile.mkdir("a_dir")
1329
+ # }
1330
+ #
1331
+ # The next example reopens <code>my.zip</code> writes the contents of
1332
+ # <code>first.txt</code> to standard out and deletes the entry from
1333
+ # the archive.
1334
+ #
1335
+ # require 'zip/zip'
1336
+ #
1337
+ # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) {
1338
+ # |zipfile|
1339
+ # puts zipfile.read("first.txt")
1340
+ # zipfile.remove("first.txt")
1341
+ # }
1342
+ #
1343
+ # ZipFileSystem offers an alternative API that emulates ruby's
1344
+ # interface for accessing the filesystem, ie. the File and Dir classes.
1345
+
1346
+ class ZipFile < ZipCentralDirectory
1347
+
1348
+ CREATE = 1
1349
+
1350
+ attr_reader :name
1351
+
1352
+ # default -> false
1353
+ attr_accessor :restore_ownership
1354
+ # default -> false
1355
+ attr_accessor :restore_permissions
1356
+ # default -> true
1357
+ attr_accessor :restore_times
1358
+
1359
+ # Opens a zip archive. Pass true as the second parameter to create
1360
+ # a new archive if it doesn't exist already.
1361
+ def initialize(fileName, create = nil)
1362
+ super()
1363
+ @name = fileName
1364
+ @comment = ""
1365
+ if (File.exists?(fileName))
1366
+ File.open(name, "rb") { |f| read_from_stream(f) }
1367
+ elsif (create)
1368
+ @entrySet = ZipEntrySet.new
1369
+ else
1370
+ raise ZipError, "File #{fileName} not found"
1371
+ end
1372
+ @create = create
1373
+ @storedEntries = @entrySet.dup
1374
+
1375
+ @restore_ownership = false
1376
+ @restore_permissions = false
1377
+ @restore_times = true
1378
+ end
1379
+
1380
+ # Same as #new. If a block is passed the ZipFile object is passed
1381
+ # to the block and is automatically closed afterwards just as with
1382
+ # ruby's builtin File.open method.
1383
+ def ZipFile.open(fileName, create = nil)
1384
+ zf = ZipFile.new(fileName, create)
1385
+ if block_given?
1386
+ begin
1387
+ yield zf
1388
+ ensure
1389
+ zf.close
1390
+ end
1391
+ else
1392
+ zf
1393
+ end
1394
+ end
1395
+
1396
+ # Returns the zip files comment, if it has one
1397
+ attr_accessor :comment
1398
+
1399
+ # Iterates over the contents of the ZipFile. This is more efficient
1400
+ # than using a ZipInputStream since this methods simply iterates
1401
+ # through the entries in the central directory structure in the archive
1402
+ # whereas ZipInputStream jumps through the entire archive accessing the
1403
+ # local entry headers (which contain the same information as the
1404
+ # central directory).
1405
+ def ZipFile.foreach(aZipFileName, &block)
1406
+ ZipFile.open(aZipFileName) {
1407
+ |zipFile|
1408
+ zipFile.each(&block)
1409
+ }
1410
+ end
1411
+
1412
+ # Returns an input stream to the specified entry. If a block is passed
1413
+ # the stream object is passed to the block and the stream is automatically
1414
+ # closed afterwards just as with ruby's builtin File.open method.
1415
+ def get_input_stream(entry, &aProc)
1416
+ get_entry(entry).get_input_stream(&aProc)
1417
+ end
1418
+
1419
+ # Returns an output stream to the specified entry. If a block is passed
1420
+ # the stream object is passed to the block and the stream is automatically
1421
+ # closed afterwards just as with ruby's builtin File.open method.
1422
+ def get_output_stream(entry, &aProc)
1423
+ newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s)
1424
+ if newEntry.directory?
1425
+ raise ArgumentError,
1426
+ "cannot open stream to directory entry - '#{newEntry}'"
1427
+ end
1428
+ zipStreamableEntry = ZipStreamableStream.new(newEntry)
1429
+ @entrySet << zipStreamableEntry
1430
+ zipStreamableEntry.get_output_stream(&aProc)
1431
+ end
1432
+
1433
+ # Returns the name of the zip archive
1434
+ def to_s
1435
+ @name
1436
+ end
1437
+
1438
+ # Returns a string containing the contents of the specified entry
1439
+ def read(entry)
1440
+ get_input_stream(entry) { |is| is.read }
1441
+ end
1442
+
1443
+ # Convenience method for adding the contents of a file to the archive
1444
+ def add(entry, srcPath, &continueOnExistsProc)
1445
+ continueOnExistsProc ||= proc { false }
1446
+ check_entry_exists(entry, continueOnExistsProc, "add")
1447
+ newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s)
1448
+ newEntry.gather_fileinfo_from_srcpath(srcPath)
1449
+ @entrySet << newEntry
1450
+ end
1451
+
1452
+ # Removes the specified entry.
1453
+ def remove(entry)
1454
+ @entrySet.delete(get_entry(entry))
1455
+ end
1456
+
1457
+ # Renames the specified entry.
1458
+ def rename(entry, newName, &continueOnExistsProc)
1459
+ foundEntry = get_entry(entry)
1460
+ check_entry_exists(newName, continueOnExistsProc, "rename")
1461
+ get_output_stream(newName) { |os| os.write(read(foundEntry)) }
1462
+ remove(foundEntry)
1463
+ end
1464
+
1465
+ # Replaces the specified entry with the contents of srcPath (from
1466
+ # the file system).
1467
+ def replace(entry, srcPath)
1468
+ check_file(srcPath)
1469
+ add(remove(entry), srcPath)
1470
+ end
1471
+
1472
+ # Extracts entry to file destPath.
1473
+ def extract(entry, destPath, &onExistsProc)
1474
+ onExistsProc ||= proc { false }
1475
+ foundEntry = get_entry(entry)
1476
+ foundEntry.extract(destPath, &onExistsProc)
1477
+ end
1478
+
1479
+ # Commits changes that has been made since the previous commit to
1480
+ # the zip archive.
1481
+ def commit
1482
+ return if ! commit_required?
1483
+ on_success_replace(name) {
1484
+ |tmpFile|
1485
+ ZipOutputStream.open(tmpFile) {
1486
+ |zos|
1487
+
1488
+ @entrySet.each { |e| e.write_to_zip_output_stream(zos) }
1489
+ zos.comment = comment
1490
+ }
1491
+ true
1492
+ }
1493
+ initialize(name)
1494
+ end
1495
+
1496
+ # Closes the zip file committing any changes that has been made.
1497
+ def close
1498
+ commit
1499
+ end
1500
+
1501
+ # Returns true if any changes has been made to this archive since
1502
+ # the previous commit
1503
+ def commit_required?
1504
+ return @entrySet != @storedEntries || @create == ZipFile::CREATE
1505
+ end
1506
+
1507
+ # Searches for entry with the specified name. Returns nil if
1508
+ # no entry is found. See also get_entry
1509
+ def find_entry(entry)
1510
+ @entrySet.detect {
1511
+ |e|
1512
+ e.name.sub(/\/$/, "") == entry.to_s.sub(/\/$/, "")
1513
+ }
1514
+ end
1515
+
1516
+ # Searches for an entry just as find_entry, but throws Errno::ENOENT
1517
+ # if no entry is found.
1518
+ def get_entry(entry)
1519
+ selectedEntry = find_entry(entry)
1520
+ unless selectedEntry
1521
+ raise Errno::ENOENT, entry
1522
+ end
1523
+ selectedEntry.restore_ownership = @restore_ownership
1524
+ selectedEntry.restore_permissions = @restore_permissions
1525
+ selectedEntry.restore_times = @restore_times
1526
+
1527
+ return selectedEntry
1528
+ end
1529
+
1530
+ # Creates a directory
1531
+ def mkdir(entryName, permissionInt = 0755)
1532
+ if find_entry(entryName)
1533
+ raise Errno::EEXIST, "File exists - #{entryName}"
1534
+ end
1535
+ @entrySet << ZipStreamableDirectory.new(@name, entryName.to_s.ensure_end("/"), nil, permissionInt)
1536
+ end
1537
+
1538
+ private
1539
+
1540
+ def is_directory(newEntry, srcPath)
1541
+ srcPathIsDirectory = File.directory?(srcPath)
1542
+ if newEntry.is_directory && ! srcPathIsDirectory
1543
+ raise ArgumentError,
1544
+ "entry name '#{newEntry}' indicates directory entry, but "+
1545
+ "'#{srcPath}' is not a directory"
1546
+ elsif ! newEntry.is_directory && srcPathIsDirectory
1547
+ newEntry.name += "/"
1548
+ end
1549
+ return newEntry.is_directory && srcPathIsDirectory
1550
+ end
1551
+
1552
+ def check_entry_exists(entryName, continueOnExistsProc, procedureName)
1553
+ continueOnExistsProc ||= proc { false }
1554
+ if @entrySet.detect { |e| e.name == entryName }
1555
+ if continueOnExistsProc.call
1556
+ remove get_entry(entryName)
1557
+ else
1558
+ raise ZipEntryExistsError,
1559
+ procedureName+" failed. Entry #{entryName} already exists"
1560
+ end
1561
+ end
1562
+ end
1563
+
1564
+ def check_file(path)
1565
+ unless File.readable? path
1566
+ raise Errno::ENOENT, path
1567
+ end
1568
+ end
1569
+
1570
+ def on_success_replace(aFilename)
1571
+ tmpfile = get_tempfile
1572
+ tmpFilename = tmpfile.path
1573
+ tmpfile.close
1574
+ if yield tmpFilename
1575
+ FileUtils.mv(tmpFilename, name)
1576
+ end
1577
+ end
1578
+
1579
+ def get_tempfile
1580
+ tempFile = Tempfile.new(File.basename(name), File.dirname(name))
1581
+ tempFile.binmode
1582
+ tempFile
1583
+ end
1584
+
1585
+ end
1586
+
1587
+ class ZipStreamableDirectory < ZipEntry
1588
+ def initialize(zipfile, entry, srcPath = nil, permissionInt = nil)
1589
+ super(zipfile, entry)
1590
+
1591
+ @ftype = :directory
1592
+ entry.get_extra_attributes_from_path(srcPath) if (srcPath)
1593
+ @unix_perms = permissionInt if (permissionInt)
1594
+ end
1595
+ end
1596
+
1597
+ class ZipStreamableStream < DelegateClass(ZipEntry) #nodoc:all
1598
+ def initialize(entry)
1599
+ super(entry)
1600
+ @tempFile = Tempfile.new(File.basename(name), File.dirname(zipfile))
1601
+ @tempFile.binmode
1602
+ end
1603
+
1604
+ def get_output_stream
1605
+ if block_given?
1606
+ begin
1607
+ yield(@tempFile)
1608
+ ensure
1609
+ @tempFile.close
1610
+ end
1611
+ else
1612
+ @tempFile
1613
+ end
1614
+ end
1615
+
1616
+ def get_input_stream
1617
+ if ! @tempFile.closed?
1618
+ raise StandardError, "cannot open entry for reading while its open for writing - #{name}"
1619
+ end
1620
+ @tempFile.open # reopens tempfile from top
1621
+ @tempFile.binmode
1622
+ if block_given?
1623
+ begin
1624
+ yield(@tempFile)
1625
+ ensure
1626
+ @tempFile.close
1627
+ end
1628
+ else
1629
+ @tempFile
1630
+ end
1631
+ end
1632
+
1633
+ def write_to_zip_output_stream(aZipOutputStream)
1634
+ aZipOutputStream.put_next_entry(self)
1635
+ get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) }
1636
+ end
1637
+ end
1638
+
1639
+ class ZipExtraField < Hash
1640
+ ID_MAP = {}
1641
+
1642
+ # Meta class for extra fields
1643
+ class Generic
1644
+ def self.register_map
1645
+ if self.const_defined?(:HEADER_ID)
1646
+ ID_MAP[self.const_get(:HEADER_ID)] = self
1647
+ end
1648
+ end
1649
+
1650
+ def self.name
1651
+ self.to_s.split("::")[-1]
1652
+ end
1653
+
1654
+ # return field [size, content] or false
1655
+ def initial_parse(binstr)
1656
+ if ! binstr
1657
+ # If nil, start with empty.
1658
+ return false
1659
+ elsif binstr[0,2] != self.class.const_get(:HEADER_ID)
1660
+ $stderr.puts "Warning: weired extra feild header ID. skip parsing"
1661
+ return false
1662
+ end
1663
+ [binstr[2,2].unpack("v")[0], binstr[4..-1]]
1664
+ end
1665
+
1666
+ def ==(other)
1667
+ self.class != other.class and return false
1668
+ each { |k, v|
1669
+ v != other[k] and return false
1670
+ }
1671
+ true
1672
+ end
1673
+
1674
+ def to_local_bin
1675
+ s = pack_for_local
1676
+ self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s
1677
+ end
1678
+
1679
+ def to_c_dir_bin
1680
+ s = pack_for_c_dir
1681
+ self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s
1682
+ end
1683
+ end
1684
+
1685
+ # Info-ZIP Additional timestamp field
1686
+ class UniversalTime < Generic
1687
+ HEADER_ID = "UT"
1688
+ register_map
1689
+
1690
+ def initialize(binstr = nil)
1691
+ @ctime = nil
1692
+ @mtime = nil
1693
+ @atime = nil
1694
+ @flag = nil
1695
+ binstr and merge(binstr)
1696
+ end
1697
+ attr_accessor :atime, :ctime, :mtime, :flag
1698
+
1699
+ def merge(binstr)
1700
+ binstr == "" and return
1701
+ size, content = initial_parse(binstr)
1702
+ size or return
1703
+ @flag, mtime, atime, ctime = content.unpack("CVVV")
1704
+ mtime and @mtime ||= Time.at(mtime)
1705
+ atime and @atime ||= Time.at(atime)
1706
+ ctime and @ctime ||= Time.at(ctime)
1707
+ end
1708
+
1709
+ def ==(other)
1710
+ @mtime == other.mtime &&
1711
+ @atime == other.atime &&
1712
+ @ctime == other.ctime
1713
+ end
1714
+
1715
+ def pack_for_local
1716
+ s = [@flag].pack("C")
1717
+ @flag & 1 != 0 and s << [@mtime.to_i].pack("V")
1718
+ @flag & 2 != 0 and s << [@atime.to_i].pack("V")
1719
+ @flag & 4 != 0 and s << [@ctime.to_i].pack("V")
1720
+ s
1721
+ end
1722
+
1723
+ def pack_for_c_dir
1724
+ s = [@flag].pack("C")
1725
+ @flag & 1 == 1 and s << [@mtime.to_i].pack("V")
1726
+ s
1727
+ end
1728
+ end
1729
+
1730
+ # Info-ZIP Extra for UNIX uid/gid
1731
+ class IUnix < Generic
1732
+ HEADER_ID = "Ux"
1733
+ register_map
1734
+
1735
+ def initialize(binstr = nil)
1736
+ @uid = 0
1737
+ @gid = 0
1738
+ binstr and merge(binstr)
1739
+ end
1740
+ attr_accessor :uid, :gid
1741
+
1742
+ def merge(binstr)
1743
+ binstr == "" and return
1744
+ size, content = initial_parse(binstr)
1745
+ # size: 0 for central direcotry. 4 for local header
1746
+ return if(! size || size == 0)
1747
+ uid, gid = content.unpack("vv")
1748
+ @uid ||= uid
1749
+ @gid ||= gid
1750
+ end
1751
+
1752
+ def ==(other)
1753
+ @uid == other.uid &&
1754
+ @gid == other.gid
1755
+ end
1756
+
1757
+ def pack_for_local
1758
+ [@uid, @gid].pack("vv")
1759
+ end
1760
+
1761
+ def pack_for_c_dir
1762
+ ""
1763
+ end
1764
+ end
1765
+
1766
+ ## start main of ZipExtraField < Hash
1767
+ def initialize(binstr = nil)
1768
+ binstr and merge(binstr)
1769
+ end
1770
+
1771
+ def merge(binstr)
1772
+ binstr == "" and return
1773
+ i = 0
1774
+ while i < binstr.length
1775
+ id = binstr[i,2]
1776
+ len = binstr[i+2,2].to_s.unpack("v")[0]
1777
+ if id && ID_MAP.member?(id)
1778
+ field_name = ID_MAP[id].name
1779
+ if self.member?(field_name)
1780
+ self[field_name].mergea(binstr[i, len+4])
1781
+ else
1782
+ field_obj = ID_MAP[id].new(binstr[i, len+4])
1783
+ self[field_name] = field_obj
1784
+ end
1785
+ elsif id
1786
+ unless self["Unknown"]
1787
+ s = ""
1788
+ class << s
1789
+ alias_method :to_c_dir_bin, :to_s
1790
+ alias_method :to_local_bin, :to_s
1791
+ end
1792
+ self["Unknown"] = s
1793
+ end
1794
+ if ! len || len+4 > binstr[i..-1].length
1795
+ self["Unknown"] << binstr[i..-1]
1796
+ break;
1797
+ end
1798
+ self["Unknown"] << binstr[i, len+4]
1799
+ end
1800
+ i += len+4
1801
+ end
1802
+ end
1803
+
1804
+ def create(name)
1805
+ field_class = nil
1806
+ ID_MAP.each { |id, klass|
1807
+ if klass.name == name
1808
+ field_class = klass
1809
+ break
1810
+ end
1811
+ }
1812
+ if ! field_class
1813
+ raise ZipError, "Unknown extra field '#{name}'"
1814
+ end
1815
+ self[name] = field_class.new()
1816
+ end
1817
+
1818
+ def to_local_bin
1819
+ s = ""
1820
+ each { |k, v|
1821
+ s << v.to_local_bin
1822
+ }
1823
+ s
1824
+ end
1825
+ alias :to_s :to_local_bin
1826
+
1827
+ def to_c_dir_bin
1828
+ s = ""
1829
+ each { |k, v|
1830
+ s << v.to_c_dir_bin
1831
+ }
1832
+ s
1833
+ end
1834
+
1835
+ def c_dir_length
1836
+ to_c_dir_bin.length
1837
+ end
1838
+ def local_length
1839
+ to_local_bin.length
1840
+ end
1841
+ alias :c_dir_size :c_dir_length
1842
+ alias :local_size :local_length
1843
+ alias :length :local_length
1844
+ alias :size :local_length
1845
+ end # end ZipExtraField
1846
+
1847
+ end # Zip namespace module
1848
+
1849
+
1850
+
1851
+ # Copyright (C) 2002, 2003 Thomas Sondergaard
1852
+ # rubyzip is free software; you can redistribute it and/or
1853
+ # modify it under the terms of the ruby license.