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