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