rzip 2.1.1

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