openlogic-zip 2.0.3

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