rubyzip 0.5.7

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rubyzip might be problematic. Click here for more details.

@@ -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,1543 @@
1
+ require 'delegate'
2
+ require 'singleton'
3
+ require 'tempfile'
4
+ require 'ftools'
5
+ require 'zlib'
6
+ require 'zip/stdrubyext'
7
+ require 'zip/ioextras'
8
+
9
+ if Tempfile.superclass == SimpleDelegator
10
+ require 'zip/tempfile_bugfixed'
11
+ Tempfile = BugFix::Tempfile
12
+ end
13
+
14
+ module Zlib #:nodoc:all
15
+ if ! const_defined? :MAX_WBITS
16
+ MAX_WBITS = Zlib::Deflate.MAX_WBITS
17
+ end
18
+ end
19
+
20
+ module Zip
21
+
22
+ VERSION = '0.5.7'
23
+
24
+ RUBY_MINOR_VERSION = RUBY_VERSION.split(".")[1].to_i
25
+
26
+ # Ruby 1.7.x compatibility
27
+ # In ruby 1.6.x and 1.8.0 reading from an empty stream returns
28
+ # an empty string the first time and then nil.
29
+ # not so in 1.7.x
30
+ EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST = RUBY_MINOR_VERSION != 7
31
+
32
+ # ZipInputStream is the basic class for reading zip entries in a
33
+ # zip file. It is possible to create a ZipInputStream object directly,
34
+ # passing the zip file name to the constructor, but more often than not
35
+ # the ZipInputStream will be obtained from a ZipFile (perhaps using the
36
+ # ZipFileSystem interface) object for a particular entry in the zip
37
+ # archive.
38
+ #
39
+ # A ZipInputStream inherits IOExtras::AbstractInputStream in order
40
+ # to provide an IO-like interface for reading from a single zip
41
+ # entry. Beyond methods for mimicking an IO-object it contains
42
+ # the method get_next_entry for iterating through the entries of
43
+ # an archive. get_next_entry returns a ZipEntry object that describes
44
+ # the zip entry the ZipInputStream is currently reading from.
45
+ #
46
+ # Example that creates a zip archive with ZipOutputStream and reads it
47
+ # back again with a ZipInputStream.
48
+ #
49
+ # require 'zip/zip'
50
+ #
51
+ # Zip::ZipOutputStream::open("my.zip") {
52
+ # |io|
53
+ #
54
+ # io.put_next_entry("first_entry.txt")
55
+ # io.write "Hello world!"
56
+ #
57
+ # io.put_next_entry("adir/first_entry.txt")
58
+ # io.write "Hello again!"
59
+ # }
60
+ #
61
+ #
62
+ # Zip::ZipInputStream::open("my.zip") {
63
+ # |io|
64
+ #
65
+ # while (entry = io.get_next_entry)
66
+ # puts "Contents of #{entry.name}: '#{io.read}'"
67
+ # end
68
+ # }
69
+ #
70
+ # java.util.zip.ZipInputStream is the original inspiration for this
71
+ # class.
72
+
73
+ class ZipInputStream
74
+ include IOExtras::AbstractInputStream
75
+
76
+ # Opens the indicated zip file. An exception is thrown
77
+ # if the specified offset in the specified filename is
78
+ # not a local zip entry header.
79
+ def initialize(filename, offset = 0)
80
+ super()
81
+ @archiveIO = File.open(filename, "rb")
82
+ @archiveIO.seek(offset, IO::SEEK_SET)
83
+ @decompressor = NullDecompressor.instance
84
+ @currentEntry = nil
85
+ end
86
+
87
+ def close
88
+ @archiveIO.close
89
+ end
90
+
91
+ # Same as #initialize but if a block is passed the opened
92
+ # stream is passed to the block and closed when the block
93
+ # returns.
94
+ def ZipInputStream.open(filename)
95
+ return new(filename) unless block_given?
96
+
97
+ zio = new(filename)
98
+ yield zio
99
+ ensure
100
+ zio.close if zio
101
+ end
102
+
103
+ # Returns a ZipEntry object. It is necessary to call this
104
+ # method on a newly created ZipInputStream before reading from
105
+ # the first entry in the archive. Returns nil when there are
106
+ # no more entries.
107
+ def get_next_entry
108
+ @archiveIO.seek(@currentEntry.next_header_offset,
109
+ IO::SEEK_SET) if @currentEntry
110
+ open_entry
111
+ end
112
+
113
+ # Rewinds the stream to the beginning of the current entry
114
+ def rewind
115
+ return if @currentEntry.nil?
116
+ @lineno = 0
117
+ @archiveIO.seek(@currentEntry.localHeaderOffset,
118
+ IO::SEEK_SET)
119
+ open_entry
120
+ end
121
+
122
+ # Modeled after IO.read
123
+ def read(numberOfBytes = nil)
124
+ @decompressor.read(numberOfBytes)
125
+ end
126
+
127
+ protected
128
+
129
+ def open_entry
130
+ @currentEntry = ZipEntry.read_local_entry(@archiveIO)
131
+ if (@currentEntry == nil)
132
+ @decompressor = NullDecompressor.instance
133
+ elsif @currentEntry.compression_method == ZipEntry::STORED
134
+ @decompressor = PassThruDecompressor.new(@archiveIO,
135
+ @currentEntry.size)
136
+ elsif @currentEntry.compression_method == ZipEntry::DEFLATED
137
+ @decompressor = Inflater.new(@archiveIO)
138
+ else
139
+ raise ZipCompressionMethodError,
140
+ "Unsupported compression method #{@currentEntry.compression_method}"
141
+ end
142
+ flush
143
+ return @currentEntry
144
+ end
145
+
146
+ def produce_input
147
+ @decompressor.produce_input
148
+ end
149
+
150
+ def input_finished?
151
+ @decompressor.input_finished?
152
+ end
153
+ end
154
+
155
+
156
+
157
+ class Decompressor #:nodoc:all
158
+ CHUNK_SIZE=32768
159
+ def initialize(inputStream)
160
+ super()
161
+ @inputStream=inputStream
162
+ end
163
+ end
164
+
165
+ class Inflater < Decompressor #:nodoc:all
166
+ def initialize(inputStream)
167
+ super
168
+ @zlibInflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
169
+ @outputBuffer=""
170
+ @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
171
+ end
172
+
173
+ def read(numberOfBytes = nil)
174
+ readEverything = (numberOfBytes == nil)
175
+ while (readEverything || @outputBuffer.length < numberOfBytes)
176
+ break if internal_input_finished?
177
+ @outputBuffer << internal_produce_input
178
+ end
179
+ return value_when_finished if @outputBuffer.length==0 && input_finished?
180
+ endIndex= numberOfBytes==nil ? @outputBuffer.length : numberOfBytes
181
+ return @outputBuffer.slice!(0...endIndex)
182
+ end
183
+
184
+ def produce_input
185
+ if (@outputBuffer.empty?)
186
+ return internal_produce_input
187
+ else
188
+ return @outputBuffer.slice!(0...(@outputBuffer.length))
189
+ end
190
+ end
191
+
192
+ # to be used with produce_input, not read (as read may still have more data cached)
193
+ def input_finished?
194
+ @outputBuffer.empty? && internal_input_finished?
195
+ end
196
+
197
+ private
198
+
199
+ def internal_produce_input
200
+ @zlibInflater.inflate(@inputStream.read(Decompressor::CHUNK_SIZE))
201
+ end
202
+
203
+ def internal_input_finished?
204
+ @zlibInflater.finished?
205
+ end
206
+
207
+ # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
208
+ def value_when_finished # mimic behaviour of ruby File object.
209
+ return nil if @hasReturnedEmptyString
210
+ @hasReturnedEmptyString=true
211
+ return ""
212
+ end
213
+ end
214
+
215
+ class PassThruDecompressor < Decompressor #:nodoc:all
216
+ def initialize(inputStream, charsToRead)
217
+ super inputStream
218
+ @charsToRead = charsToRead
219
+ @readSoFar = 0
220
+ @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
221
+ end
222
+
223
+ # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
224
+ def read(numberOfBytes = nil)
225
+ if input_finished?
226
+ hasReturnedEmptyStringVal=@hasReturnedEmptyString
227
+ @hasReturnedEmptyString=true
228
+ return "" unless hasReturnedEmptyStringVal
229
+ return nil
230
+ end
231
+
232
+ if (numberOfBytes == nil || @readSoFar+numberOfBytes > @charsToRead)
233
+ numberOfBytes = @charsToRead-@readSoFar
234
+ end
235
+ @readSoFar += numberOfBytes
236
+ @inputStream.read(numberOfBytes)
237
+ end
238
+
239
+ def produce_input
240
+ read(Decompressor::CHUNK_SIZE)
241
+ end
242
+
243
+ def input_finished?
244
+ (@readSoFar >= @charsToRead)
245
+ end
246
+ end
247
+
248
+ class NullDecompressor #:nodoc:all
249
+ include Singleton
250
+ def read(numberOfBytes = nil)
251
+ nil
252
+ end
253
+
254
+ def produce_input
255
+ nil
256
+ end
257
+
258
+ def input_finished?
259
+ true
260
+ end
261
+ end
262
+
263
+ class NullInputStream < NullDecompressor #:nodoc:all
264
+ include IOExtras::AbstractInputStream
265
+ end
266
+
267
+ class ZipEntry
268
+ STORED = 0
269
+ DEFLATED = 8
270
+
271
+ attr_accessor :comment, :compressed_size, :crc, :extra, :compression_method,
272
+ :name, :size, :localHeaderOffset, :zipfile, :fstype, :externalFileAttributes
273
+
274
+ def initialize(zipfile = "", name = "", comment = "", extra = "",
275
+ compressed_size = 0, crc = 0,
276
+ compression_method = ZipEntry::DEFLATED, size = 0,
277
+ time = Time.now)
278
+ super()
279
+ if name.starts_with("/")
280
+ raise ZipEntryNameError, "Illegal ZipEntry name '#{name}', name must not start with /"
281
+ end
282
+ @localHeaderOffset = 0
283
+ @internalFileAttributes = 1
284
+ @externalFileAttributes = 0
285
+ @version = 52 # this library's version
286
+ @fstype = 0 # default is fat
287
+ @zipfile, @comment, @compressed_size, @crc, @extra, @compression_method,
288
+ @name, @size = zipfile, comment, compressed_size, crc,
289
+ extra, compression_method, name, size
290
+ @time = time
291
+ unless ZipExtraField === @extra
292
+ @extra = ZipExtraField.new(@extra.to_s)
293
+ end
294
+ end
295
+
296
+ def time
297
+ if @extra["UniversalTime"]
298
+ @extra["UniversalTime"].mtime
299
+ else
300
+ # Atandard time field in central directory has local time
301
+ # under archive creator. Then, we can't get timezone.
302
+ @time
303
+ end
304
+ end
305
+ alias :mtime :time
306
+
307
+ def time=(aTime)
308
+ unless @extra.member?("UniversalTime")
309
+ @extra.create("UniversalTime")
310
+ end
311
+ @extra["UniversalTime"].mtime = aTime
312
+ @time = aTime
313
+ end
314
+
315
+ def directory?
316
+ return (%r{\/$} =~ @name) != nil
317
+ end
318
+ alias :is_directory :directory?
319
+
320
+ def file?
321
+ ! directory?
322
+ end
323
+
324
+ def local_entry_offset #:nodoc:all
325
+ localHeaderOffset + local_header_size
326
+ end
327
+
328
+ def local_header_size #:nodoc:all
329
+ LOCAL_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) + (@extra ? @extra.local_size : 0)
330
+ end
331
+
332
+ def cdir_header_size #:nodoc:all
333
+ CDIR_ENTRY_STATIC_HEADER_LENGTH + (@name ? @name.size : 0) +
334
+ (@extra ? @extra.c_dir_size : 0) + (@comment ? @comment.size : 0)
335
+ end
336
+
337
+ def next_header_offset #:nodoc:all
338
+ local_entry_offset + self.compressed_size
339
+ end
340
+
341
+ def to_s
342
+ @name
343
+ end
344
+
345
+ protected
346
+
347
+ def ZipEntry.read_zip_short(io)
348
+ io.read(2).unpack('v')[0]
349
+ end
350
+
351
+ def ZipEntry.read_zip_long(io)
352
+ io.read(4).unpack('V')[0]
353
+ end
354
+ public
355
+
356
+ LOCAL_ENTRY_SIGNATURE = 0x04034b50
357
+ LOCAL_ENTRY_STATIC_HEADER_LENGTH = 30
358
+
359
+ def read_local_entry(io) #:nodoc:all
360
+ @localHeaderOffset = io.tell
361
+ staticSizedFieldsBuf = io.read(LOCAL_ENTRY_STATIC_HEADER_LENGTH)
362
+ unless (staticSizedFieldsBuf.size==LOCAL_ENTRY_STATIC_HEADER_LENGTH)
363
+ raise ZipError, "Premature end of file. Not enough data for zip entry local header"
364
+ end
365
+
366
+ localHeader ,
367
+ @version ,
368
+ @fstype ,
369
+ @gpFlags ,
370
+ @compression_method,
371
+ lastModTime ,
372
+ lastModDate ,
373
+ @crc ,
374
+ @compressed_size ,
375
+ @size ,
376
+ nameLength ,
377
+ extraLength = staticSizedFieldsBuf.unpack('VCCvvvvVVVvv')
378
+
379
+ unless (localHeader == LOCAL_ENTRY_SIGNATURE)
380
+ raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
381
+ end
382
+ set_time(lastModDate, lastModTime)
383
+
384
+ @name = io.read(nameLength)
385
+ extra = io.read(extraLength)
386
+
387
+ if (extra && extra.length != extraLength)
388
+ raise ZipError, "Truncated local zip entry header"
389
+ else
390
+ if ZipExtraField === @extra
391
+ @extra.merge(extra)
392
+ else
393
+ @extra = ZipExtraField.new(extra)
394
+ end
395
+ end
396
+ end
397
+
398
+ def ZipEntry.read_local_entry(io)
399
+ entry = new(io.path)
400
+ entry.read_local_entry(io)
401
+ return entry
402
+ rescue ZipError
403
+ return nil
404
+ end
405
+
406
+ def write_local_entry(io) #:nodoc:all
407
+ @localHeaderOffset = io.tell
408
+
409
+ io <<
410
+ [LOCAL_ENTRY_SIGNATURE ,
411
+ 0 ,
412
+ 0 , # @gpFlags ,
413
+ @compression_method ,
414
+ @time.to_binary_dos_time , # @lastModTime ,
415
+ @time.to_binary_dos_date , # @lastModDate ,
416
+ @crc ,
417
+ @compressed_size ,
418
+ @size ,
419
+ @name ? @name.length : 0,
420
+ @extra? @extra.local_length : 0 ].pack('VvvvvvVVVvv')
421
+ io << @name
422
+ io << (@extra ? @extra.to_local_bin : "")
423
+ end
424
+
425
+ CENTRAL_DIRECTORY_ENTRY_SIGNATURE = 0x02014b50
426
+ CDIR_ENTRY_STATIC_HEADER_LENGTH = 46
427
+
428
+ def read_c_dir_entry(io) #:nodoc:all
429
+ staticSizedFieldsBuf = io.read(CDIR_ENTRY_STATIC_HEADER_LENGTH)
430
+ unless (staticSizedFieldsBuf.size == CDIR_ENTRY_STATIC_HEADER_LENGTH)
431
+ raise ZipError, "Premature end of file. Not enough data for zip cdir entry header"
432
+ end
433
+
434
+ cdirSignature ,
435
+ @version , # version of encoding software
436
+ @fstype , # filesystem type
437
+ @versionNeededToExtract,
438
+ @gpFlags ,
439
+ @compression_method ,
440
+ lastModTime ,
441
+ lastModDate ,
442
+ @crc ,
443
+ @compressed_size ,
444
+ @size ,
445
+ nameLength ,
446
+ extraLength ,
447
+ commentLength ,
448
+ diskNumberStart ,
449
+ @internalFileAttributes,
450
+ @externalFileAttributes,
451
+ @localHeaderOffset ,
452
+ @name ,
453
+ @extra ,
454
+ @comment = staticSizedFieldsBuf.unpack('VCCvvvvvVVVvvvvvVV')
455
+
456
+ unless (cdirSignature == CENTRAL_DIRECTORY_ENTRY_SIGNATURE)
457
+ raise ZipError, "Zip local header magic not found at location '#{localHeaderOffset}'"
458
+ end
459
+ set_time(lastModDate, lastModTime)
460
+
461
+ @name = io.read(nameLength)
462
+ if ZipExtraField === @extra
463
+ @extra.merge(io.read(extraLength))
464
+ else
465
+ @extra = ZipExtraField.new(io.read(extraLength))
466
+ end
467
+ @comment = io.read(commentLength)
468
+ unless (@comment && @comment.length == commentLength)
469
+ raise ZipError, "Truncated cdir zip entry header"
470
+ end
471
+ end
472
+
473
+ def ZipEntry.read_c_dir_entry(io) #:nodoc:all
474
+ entry = new(io.path)
475
+ entry.read_c_dir_entry(io)
476
+ return entry
477
+ rescue ZipError
478
+ return nil
479
+ end
480
+
481
+
482
+ def write_c_dir_entry(io) #:nodoc:all
483
+ io <<
484
+ [CENTRAL_DIRECTORY_ENTRY_SIGNATURE,
485
+ @version , # version of encoding software
486
+ @fstype , # filesystem type
487
+ 0 , # @versionNeededToExtract ,
488
+ 0 , # @gpFlags ,
489
+ @compression_method ,
490
+ @time.to_binary_dos_time , # @lastModTime ,
491
+ @time.to_binary_dos_date , # @lastModDate ,
492
+ @crc ,
493
+ @compressed_size ,
494
+ @size ,
495
+ @name ? @name.length : 0 ,
496
+ @extra ? @extra.c_dir_length : 0 ,
497
+ @comment ? comment.length : 0 ,
498
+ 0 , # disk number start
499
+ @internalFileAttributes , # file type (binary=0, text=1)
500
+ @externalFileAttributes , # native filesystem attributes
501
+ @localHeaderOffset ,
502
+ @name ,
503
+ @extra ,
504
+ @comment ].pack('VCCvvvvvVVVvvvvvVV')
505
+
506
+ io << @name
507
+ io << (@extra ? @extra.to_c_dir_bin : "")
508
+ io << @comment
509
+ end
510
+
511
+ def == (other)
512
+ return false unless other.class == ZipEntry
513
+ # Compares contents of local entry and exposed fields
514
+ (@compression_method == other.compression_method &&
515
+ @crc == other.crc &&
516
+ @compressed_size == other.compressed_size &&
517
+ @size == other.size &&
518
+ @name == other.name &&
519
+ @extra == other.extra &&
520
+ self.time.dos_equals(other.time))
521
+ end
522
+
523
+ def <=> (other)
524
+ return to_s <=> other.to_s
525
+ end
526
+
527
+ def get_input_stream
528
+ zis = ZipInputStream.new(@zipfile, localHeaderOffset)
529
+ zis.get_next_entry
530
+ if block_given?
531
+ begin
532
+ return yield(zis)
533
+ ensure
534
+ zis.close
535
+ end
536
+ else
537
+ return zis
538
+ end
539
+ end
540
+
541
+
542
+ def write_to_zip_output_stream(aZipOutputStream) #:nodoc:all
543
+ aZipOutputStream.copy_raw_entry(self)
544
+ end
545
+
546
+ def parent_as_string
547
+ entry_name = name.chomp("/")
548
+ slash_index = entry_name.rindex("/")
549
+ slash_index ? entry_name.slice(0, slash_index+1) : nil
550
+ end
551
+
552
+ def get_raw_input_stream(&aProc)
553
+ File.open(@zipfile, "rb", &aProc)
554
+ end
555
+
556
+ private
557
+ def set_time(binaryDosDate, binaryDosTime)
558
+ @time = Time.parse_binary_dos_format(binaryDosDate, binaryDosTime)
559
+ rescue ArgumentError
560
+ puts "Invalid date/time in zip entry"
561
+ end
562
+ end
563
+
564
+
565
+ # ZipOutputStream is the basic class for writing zip files. It is
566
+ # possible to create a ZipOutputStream object directly, passing
567
+ # the zip file name to the constructor, but more often than not
568
+ # the ZipOutputStream will be obtained from a ZipFile (perhaps using the
569
+ # ZipFileSystem interface) object for a particular entry in the zip
570
+ # archive.
571
+ #
572
+ # A ZipOutputStream inherits IOExtras::AbstractOutputStream in order
573
+ # to provide an IO-like interface for writing to a single zip
574
+ # entry. Beyond methods for mimicking an IO-object it contains
575
+ # the method put_next_entry that closes the current entry
576
+ # and creates a new.
577
+ #
578
+ # Please refer to ZipInputStream for example code.
579
+ #
580
+ # java.util.zip.ZipOutputStream is the original inspiration for this
581
+ # class.
582
+
583
+ class ZipOutputStream
584
+ include IOExtras::AbstractOutputStream
585
+
586
+ attr_accessor :comment
587
+
588
+ # Opens the indicated zip file. If a file with that name already
589
+ # exists it will be overwritten.
590
+ def initialize(fileName)
591
+ super()
592
+ @fileName = fileName
593
+ @outputStream = File.new(@fileName, "wb")
594
+ @entrySet = ZipEntrySet.new
595
+ @compressor = NullCompressor.instance
596
+ @closed = false
597
+ @currentEntry = nil
598
+ @comment = nil
599
+ end
600
+
601
+ # Same as #initialize but if a block is passed the opened
602
+ # stream is passed to the block and closed when the block
603
+ # returns.
604
+ def ZipOutputStream.open(fileName)
605
+ return new(fileName) unless block_given?
606
+ zos = new(fileName)
607
+ yield zos
608
+ ensure
609
+ zos.close if zos
610
+ end
611
+
612
+ # Closes the stream and writes the central directory to the zip file
613
+ def close
614
+ return if @closed
615
+ finalize_current_entry
616
+ update_local_headers
617
+ write_central_directory
618
+ @outputStream.close
619
+ @closed = true
620
+ end
621
+
622
+ # Closes the current entry and opens a new for writing.
623
+ # +entry+ can be a ZipEntry object or a string.
624
+ def put_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION)
625
+ raise ZipError, "zip stream is closed" if @closed
626
+ newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@fileName, entry.to_s)
627
+ init_next_entry(newEntry)
628
+ @currentEntry=newEntry
629
+ end
630
+
631
+ def copy_raw_entry(entry)
632
+ entry = entry.dup
633
+ raise ZipError, "zip stream is closed" if @closed
634
+ raise ZipError, "entry is not a ZipEntry" if !entry.kind_of?(ZipEntry)
635
+ finalize_current_entry
636
+ @entrySet << entry
637
+ src_pos = entry.local_entry_offset
638
+ entry.write_local_entry(@outputStream)
639
+ @compressor = NullCompressor.instance
640
+ @outputStream << entry.get_raw_input_stream {
641
+ |is|
642
+ is.seek(src_pos, IO::SEEK_SET)
643
+ is.read(entry.compressed_size)
644
+ }
645
+ @compressor = NullCompressor.instance
646
+ @currentEntry = nil
647
+ end
648
+
649
+ private
650
+ def finalize_current_entry
651
+ return unless @currentEntry
652
+ finish
653
+ @currentEntry.compressed_size = @outputStream.tell - @currentEntry.localHeaderOffset -
654
+ @currentEntry.local_header_size
655
+ @currentEntry.size = @compressor.size
656
+ @currentEntry.crc = @compressor.crc
657
+ @currentEntry = nil
658
+ @compressor = NullCompressor.instance
659
+ end
660
+
661
+ def init_next_entry(entry, level = Zlib::DEFAULT_COMPRESSION)
662
+ finalize_current_entry
663
+ @entrySet << entry
664
+ entry.write_local_entry(@outputStream)
665
+ @compressor = get_compressor(entry, level)
666
+ end
667
+
668
+ def get_compressor(entry, level)
669
+ case entry.compression_method
670
+ when ZipEntry::DEFLATED then Deflater.new(@outputStream, level)
671
+ when ZipEntry::STORED then PassThruCompressor.new(@outputStream)
672
+ else raise ZipCompressionMethodError,
673
+ "Invalid compression method: '#{entry.compression_method}'"
674
+ end
675
+ end
676
+
677
+ def update_local_headers
678
+ pos = @outputStream.tell
679
+ @entrySet.each {
680
+ |entry|
681
+ @outputStream.pos = entry.localHeaderOffset
682
+ entry.write_local_entry(@outputStream)
683
+ }
684
+ @outputStream.pos = pos
685
+ end
686
+
687
+ def write_central_directory
688
+ cdir = ZipCentralDirectory.new(@entrySet, @comment)
689
+ cdir.write_to_stream(@outputStream)
690
+ end
691
+
692
+ protected
693
+
694
+ def finish
695
+ @compressor.finish
696
+ end
697
+
698
+ public
699
+ # Modeled after IO.<<
700
+ def << (data)
701
+ @compressor << data
702
+ end
703
+ end
704
+
705
+
706
+ class Compressor #:nodoc:all
707
+ def finish
708
+ end
709
+ end
710
+
711
+ class PassThruCompressor < Compressor #:nodoc:all
712
+ def initialize(outputStream)
713
+ super()
714
+ @outputStream = outputStream
715
+ @crc = Zlib::crc32
716
+ @size = 0
717
+ end
718
+
719
+ def << (data)
720
+ val = data.to_s
721
+ @crc = Zlib::crc32(val, @crc)
722
+ @size += val.size
723
+ @outputStream << val
724
+ end
725
+
726
+ attr_reader :size, :crc
727
+ end
728
+
729
+ class NullCompressor < Compressor #:nodoc:all
730
+ include Singleton
731
+
732
+ def << (data)
733
+ raise IOError, "closed stream"
734
+ end
735
+
736
+ attr_reader :size, :compressed_size
737
+ end
738
+
739
+ class Deflater < Compressor #:nodoc:all
740
+ def initialize(outputStream, level = Zlib::DEFAULT_COMPRESSION)
741
+ super()
742
+ @outputStream = outputStream
743
+ @zlibDeflater = Zlib::Deflate.new(level, -Zlib::MAX_WBITS)
744
+ @size = 0
745
+ @crc = Zlib::crc32
746
+ end
747
+
748
+ def << (data)
749
+ val = data.to_s
750
+ @crc = Zlib::crc32(val, @crc)
751
+ @size += val.size
752
+ @outputStream << @zlibDeflater.deflate(data)
753
+ end
754
+
755
+ def finish
756
+ until @zlibDeflater.finished?
757
+ @outputStream << @zlibDeflater.finish
758
+ end
759
+ end
760
+
761
+ attr_reader :size, :crc
762
+ end
763
+
764
+
765
+ class ZipEntrySet #:nodoc:all
766
+ include Enumerable
767
+
768
+ def initialize(anEnumerable = [])
769
+ super()
770
+ @entrySet = {}
771
+ anEnumerable.each { |o| push(o) }
772
+ end
773
+
774
+ def include?(entry)
775
+ @entrySet.include?(entry.to_s)
776
+ end
777
+
778
+ def <<(entry)
779
+ @entrySet[entry.to_s] = entry
780
+ end
781
+ alias :push :<<
782
+
783
+ def size
784
+ @entrySet.size
785
+ end
786
+ alias :length :size
787
+
788
+ def delete(entry)
789
+ @entrySet.delete(entry.to_s) ? entry : nil
790
+ end
791
+
792
+ def each(&aProc)
793
+ @entrySet.values.each(&aProc)
794
+ end
795
+
796
+ def entries
797
+ @entrySet.values
798
+ end
799
+
800
+ # deep clone
801
+ def dup
802
+ newZipEntrySet = ZipEntrySet.new(@entrySet.values.map { |e| e.dup })
803
+ end
804
+
805
+ def == (other)
806
+ return false unless other.kind_of?(ZipEntrySet)
807
+ return @entrySet == other.entrySet
808
+ end
809
+
810
+ def parent(entry)
811
+ @entrySet[entry.parent_as_string]
812
+ end
813
+
814
+ #TODO attr_accessor :auto_create_directories
815
+ protected
816
+ attr_accessor :entrySet
817
+ end
818
+
819
+
820
+ class ZipCentralDirectory
821
+ include Enumerable
822
+
823
+ END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50
824
+ MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE = 65536 + 18
825
+ STATIC_EOCD_SIZE = 22
826
+
827
+ attr_reader :comment
828
+
829
+ # Returns an Enumerable containing the entries.
830
+ def entries
831
+ @entrySet.entries
832
+ end
833
+
834
+ def initialize(entries = ZipEntrySet.new, comment = "") #:nodoc:
835
+ super()
836
+ @entrySet = entries.kind_of?(ZipEntrySet) ? entries : ZipEntrySet.new(entries)
837
+ @comment = comment
838
+ end
839
+
840
+ def write_to_stream(io) #:nodoc:
841
+ offset = io.tell
842
+ @entrySet.each { |entry| entry.write_c_dir_entry(io) }
843
+ write_e_o_c_d(io, offset)
844
+ end
845
+
846
+ def write_e_o_c_d(io, offset) #:nodoc:
847
+ io <<
848
+ [END_OF_CENTRAL_DIRECTORY_SIGNATURE,
849
+ 0 , # @numberOfThisDisk
850
+ 0 , # @numberOfDiskWithStartOfCDir
851
+ @entrySet? @entrySet.size : 0 ,
852
+ @entrySet? @entrySet.size : 0 ,
853
+ cdir_size ,
854
+ offset ,
855
+ @comment ? @comment.length : 0 ].pack('VvvvvVVv')
856
+ io << @comment
857
+ end
858
+ private :write_e_o_c_d
859
+
860
+ def cdir_size #:nodoc:
861
+ # does not include eocd
862
+ @entrySet.inject(0) { |value, entry| entry.cdir_header_size + value }
863
+ end
864
+ private :cdir_size
865
+
866
+ def read_e_o_c_d(io) #:nodoc:
867
+ buf = get_e_o_c_d(io)
868
+ @numberOfThisDisk = ZipEntry::read_zip_short(buf)
869
+ @numberOfDiskWithStartOfCDir = ZipEntry::read_zip_short(buf)
870
+ @totalNumberOfEntriesInCDirOnThisDisk = ZipEntry::read_zip_short(buf)
871
+ @size = ZipEntry::read_zip_short(buf)
872
+ @sizeInBytes = ZipEntry::read_zip_long(buf)
873
+ @cdirOffset = ZipEntry::read_zip_long(buf)
874
+ commentLength = ZipEntry::read_zip_short(buf)
875
+ @comment = buf.read(commentLength)
876
+ raise ZipError, "Zip consistency problem while reading eocd structure" unless buf.size == 0
877
+ end
878
+
879
+ def read_central_directory_entries(io) #:nodoc:
880
+ begin
881
+ io.seek(@cdirOffset, IO::SEEK_SET)
882
+ rescue Errno::EINVAL
883
+ raise ZipError, "Zip consistency problem while reading central directory entry"
884
+ end
885
+ @entrySet = ZipEntrySet.new
886
+ @size.times {
887
+ @entrySet << ZipEntry.read_c_dir_entry(io)
888
+ }
889
+ end
890
+
891
+ def read_from_stream(io) #:nodoc:
892
+ read_e_o_c_d(io)
893
+ read_central_directory_entries(io)
894
+ end
895
+
896
+ def get_e_o_c_d(io) #:nodoc:
897
+ begin
898
+ io.seek(-MAX_END_OF_CENTRAL_DIRECTORY_STRUCTURE_SIZE, IO::SEEK_END)
899
+ rescue Errno::EINVAL
900
+ io.seek(0, IO::SEEK_SET)
901
+ rescue Errno::EFBIG # FreeBSD 4.9 returns Errno::EFBIG instead of Errno::EINVAL
902
+ io.seek(0, IO::SEEK_SET)
903
+ end
904
+ buf = io.read
905
+ sigIndex = buf.rindex([END_OF_CENTRAL_DIRECTORY_SIGNATURE].pack('V'))
906
+ raise ZipError, "Zip end of central directory signature not found" unless sigIndex
907
+ buf=buf.slice!((sigIndex+4)...(buf.size))
908
+ def buf.read(count)
909
+ slice!(0, count)
910
+ end
911
+ return buf
912
+ end
913
+
914
+ # For iterating over the entries.
915
+ def each(&proc)
916
+ @entrySet.each(&proc)
917
+ end
918
+
919
+ # Returns the number of entries in the central directory (and
920
+ # consequently in the zip archive).
921
+ def size
922
+ @entrySet.size
923
+ end
924
+
925
+ def ZipCentralDirectory.read_from_stream(io) #:nodoc:
926
+ cdir = new
927
+ cdir.read_from_stream(io)
928
+ return cdir
929
+ rescue ZipError
930
+ return nil
931
+ end
932
+
933
+ def == (other) #:nodoc:
934
+ return false unless other.kind_of?(ZipCentralDirectory)
935
+ @entrySet.entries.sort == other.entries.sort && comment == other.comment
936
+ end
937
+ end
938
+
939
+
940
+ class ZipError < StandardError ; end
941
+
942
+ class ZipEntryExistsError < ZipError; end
943
+ class ZipDestinationFileExistsError < ZipError; end
944
+ class ZipCompressionMethodError < ZipError; end
945
+ class ZipEntryNameError < ZipError; end
946
+
947
+ # ZipFile is modeled after java.util.zip.ZipFile from the Java SDK.
948
+ # The most important methods are those inherited from
949
+ # ZipCentralDirectory for accessing information about the entries in
950
+ # the archive and methods such as get_input_stream and
951
+ # get_output_stream for reading from and writing entries to the
952
+ # archive. The class includes a few convenience methods such as
953
+ # #extract for extracting entries to the filesystem, and #remove,
954
+ # #replace, #rename and #mkdir for making simple modifications to
955
+ # the archive.
956
+ #
957
+ # Modifications to a zip archive are not committed until #commit or
958
+ # #close is called. The method #open accepts a block following
959
+ # the pattern from File.open offering a simple way to
960
+ # automatically close the archive when the block returns.
961
+ #
962
+ # The following example opens zip archive <code>my.zip</code>
963
+ # (creating it if it doesn't exist) and adds an entry
964
+ # <code>first.txt</code> and a directory entry <code>a_dir</code>
965
+ # to it.
966
+ #
967
+ # require 'zip/zip'
968
+ #
969
+ # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) {
970
+ # |zipfile|
971
+ # zipfile.get_output_stream("first.txt") { |f| f.puts "Hello from ZipFile" }
972
+ # zipfile.mkdir("a_dir")
973
+ # }
974
+ #
975
+ # The next example reopens <code>my.zip</code> writes the contents of
976
+ # <code>first.txt</code> to standard out and deletes the entry from
977
+ # the archive.
978
+ #
979
+ # require 'zip/zip'
980
+ #
981
+ # Zip::ZipFile.open("my.zip", Zip::ZipFile::CREATE) {
982
+ # |zipfile|
983
+ # puts zipfile.read("first.txt")
984
+ # zipfile.remove("first.txt")
985
+ # }
986
+ #
987
+ # ZipFileSystem offers an alternative API that emulates ruby's
988
+ # interface for accessing the filesystem, ie. the File and Dir classes.
989
+
990
+ class ZipFile < ZipCentralDirectory
991
+
992
+ CREATE = 1
993
+
994
+ attr_reader :name
995
+
996
+ # Opens a zip archive. Pass true as the second parameter to create
997
+ # a new archive if it doesn't exist already.
998
+ def initialize(fileName, create = nil)
999
+ super()
1000
+ @name = fileName
1001
+ @comment = ""
1002
+ if (File.exists?(fileName))
1003
+ File.open(name, "rb") { |f| read_from_stream(f) }
1004
+ elsif (create)
1005
+ @entrySet = ZipEntrySet.new
1006
+ else
1007
+ raise ZipError, "File #{fileName} not found"
1008
+ end
1009
+ @create = create
1010
+ @storedEntries = @entrySet.dup
1011
+ end
1012
+
1013
+ # Same as #new. If a block is passed the ZipFile object is passed
1014
+ # to the block and is automatically closed afterwards just as with
1015
+ # ruby's builtin File.open method.
1016
+ def ZipFile.open(fileName, create = nil)
1017
+ zf = ZipFile.new(fileName, create)
1018
+ if block_given?
1019
+ begin
1020
+ yield zf
1021
+ ensure
1022
+ zf.close
1023
+ end
1024
+ else
1025
+ zf
1026
+ end
1027
+ end
1028
+
1029
+ # Returns the zip files comment, if it has one
1030
+ attr_accessor :comment
1031
+
1032
+ # Iterates over the contents of the ZipFile. This is more efficient
1033
+ # than using a ZipInputStream since this methods simply iterates
1034
+ # through the entries in the central directory structure in the archive
1035
+ # whereas ZipInputStream jumps through the entire archive accessing the
1036
+ # local entry headers (which contain the same information as the
1037
+ # central directory).
1038
+ def ZipFile.foreach(aZipFileName, &block)
1039
+ ZipFile.open(aZipFileName) {
1040
+ |zipFile|
1041
+ zipFile.each(&block)
1042
+ }
1043
+ end
1044
+
1045
+ # Returns an input stream to the specified entry. If a block is passed
1046
+ # the stream object is passed to the block and the stream is automatically
1047
+ # closed afterwards just as with ruby's builtin File.open method.
1048
+ def get_input_stream(entry, &aProc)
1049
+ get_entry(entry).get_input_stream(&aProc)
1050
+ end
1051
+
1052
+ # Returns an output stream to the specified entry. If a block is passed
1053
+ # the stream object is passed to the block and the stream is automatically
1054
+ # closed afterwards just as with ruby's builtin File.open method.
1055
+ def get_output_stream(entry, &aProc)
1056
+ newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s)
1057
+ if newEntry.directory?
1058
+ raise ArgumentError,
1059
+ "cannot open stream to directory entry - '#{newEntry}'"
1060
+ end
1061
+ zipStreamableEntry = ZipStreamableStream.new(newEntry)
1062
+ @entrySet << zipStreamableEntry
1063
+ zipStreamableEntry.get_output_stream(&aProc)
1064
+ end
1065
+
1066
+ # Returns the name of the zip archive
1067
+ def to_s
1068
+ @name
1069
+ end
1070
+
1071
+ # Returns a string containing the contents of the specified entry
1072
+ def read(entry)
1073
+ get_input_stream(entry) { |is| is.read }
1074
+ end
1075
+
1076
+ # Convenience method for adding the contents of a file to the archive
1077
+ def add(entry, srcPath, &continueOnExistsProc)
1078
+ continueOnExistsProc ||= proc { false }
1079
+ check_entry_exists(entry, continueOnExistsProc, "add")
1080
+ newEntry = entry.kind_of?(ZipEntry) ? entry : ZipEntry.new(@name, entry.to_s)
1081
+ if is_directory(newEntry, srcPath)
1082
+ @entrySet << ZipStreamableDirectory.new(newEntry)
1083
+ else
1084
+ @entrySet << ZipStreamableFile.new(newEntry, srcPath)
1085
+ end
1086
+ end
1087
+
1088
+ # Removes the specified entry.
1089
+ def remove(entry)
1090
+ @entrySet.delete(get_entry(entry))
1091
+ end
1092
+
1093
+ # Renames the specified entry.
1094
+ def rename(entry, newName, &continueOnExistsProc)
1095
+ foundEntry = get_entry(entry)
1096
+ check_entry_exists(newName, continueOnExistsProc, "rename")
1097
+ foundEntry.name=newName
1098
+ end
1099
+
1100
+ # Replaces the specified entry with the contents of srcPath (from
1101
+ # the file system).
1102
+ def replace(entry, srcPath)
1103
+ check_file(srcPath)
1104
+ add(remove(entry), srcPath)
1105
+ end
1106
+
1107
+ # Extracts entry to file destPath.
1108
+ def extract(entry, destPath, &onExistsProc)
1109
+ onExistsProc ||= proc { false }
1110
+ foundEntry = get_entry(entry)
1111
+ if foundEntry.is_directory
1112
+ create_directory(foundEntry, destPath, &onExistsProc)
1113
+ else
1114
+ write_file(foundEntry, destPath, &onExistsProc)
1115
+ end
1116
+ end
1117
+
1118
+ # Commits changes that has been made since the previous commit to
1119
+ # the zip archive.
1120
+ def commit
1121
+ return if ! commit_required?
1122
+ on_success_replace(name) {
1123
+ |tmpFile|
1124
+ ZipOutputStream.open(tmpFile) {
1125
+ |zos|
1126
+
1127
+ @entrySet.each { |e| e.write_to_zip_output_stream(zos) }
1128
+ zos.comment = comment
1129
+ }
1130
+ true
1131
+ }
1132
+ initialize(name)
1133
+ end
1134
+
1135
+ # Closes the zip file committing any changes that has been made.
1136
+ def close
1137
+ commit
1138
+ end
1139
+
1140
+ # Returns true if any changes has been made to this archive since
1141
+ # the previous commit
1142
+ def commit_required?
1143
+ return @entrySet != @storedEntries || @create == ZipFile::CREATE
1144
+ end
1145
+
1146
+ # Searches for entry with the specified name. Returns nil if
1147
+ # no entry is found. See also get_entry
1148
+ def find_entry(entry)
1149
+ @entrySet.detect {
1150
+ |e|
1151
+ e.name.sub(/\/$/, "") == entry.to_s.sub(/\/$/, "")
1152
+ }
1153
+ end
1154
+
1155
+ # Searches for an entry just as find_entry, but throws Errno::ENOENT
1156
+ # if no entry is found.
1157
+ def get_entry(entry)
1158
+ selectedEntry = find_entry(entry)
1159
+ unless selectedEntry
1160
+ raise Errno::ENOENT, entry
1161
+ end
1162
+ return selectedEntry
1163
+ end
1164
+
1165
+ # Creates a directory
1166
+ def mkdir(entryName, permissionInt = 0) #permissionInt ignored
1167
+ if find_entry(entryName)
1168
+ raise Errno::EEXIST, "File exists - #{entryName}"
1169
+ end
1170
+ @entrySet << ZipStreamableDirectory.new(ZipEntry.new(name, entryName.to_s.ensure_end("/")))
1171
+ end
1172
+
1173
+ private
1174
+
1175
+ def create_directory(entry, destPath)
1176
+ if File.directory? destPath
1177
+ return
1178
+ elsif File.exists? destPath
1179
+ if block_given? && yield(entry, destPath)
1180
+ File.rm_f destPath
1181
+ else
1182
+ raise ZipDestinationFileExistsError,
1183
+ "Cannot create directory '#{destPath}'. "+
1184
+ "A file already exists with that name"
1185
+ end
1186
+ end
1187
+ Dir.mkdir destPath
1188
+ end
1189
+
1190
+ def is_directory(newEntry, srcPath)
1191
+ srcPathIsDirectory = File.directory?(srcPath)
1192
+ if newEntry.is_directory && ! srcPathIsDirectory
1193
+ raise ArgumentError,
1194
+ "entry name '#{newEntry}' indicates directory entry, but "+
1195
+ "'#{srcPath}' is not a directory"
1196
+ elsif ! newEntry.is_directory && srcPathIsDirectory
1197
+ newEntry.name += "/"
1198
+ end
1199
+ return newEntry.is_directory && srcPathIsDirectory
1200
+ end
1201
+
1202
+ def check_entry_exists(entryName, continueOnExistsProc, procedureName)
1203
+ continueOnExistsProc ||= proc { false }
1204
+ if @entrySet.detect { |e| e.name == entryName }
1205
+ if continueOnExistsProc.call
1206
+ remove get_entry(entryName)
1207
+ else
1208
+ raise ZipEntryExistsError,
1209
+ procedureName+" failed. Entry #{entryName} already exists"
1210
+ end
1211
+ end
1212
+ end
1213
+
1214
+ def write_file(entry, destPath, continueOnExistsProc = proc { false })
1215
+ if File.exists?(destPath) && ! yield(entry, destPath)
1216
+ raise ZipDestinationFileExistsError,
1217
+ "Destination '#{destPath}' already exists"
1218
+ end
1219
+ File.open(destPath, "wb") {
1220
+ |os|
1221
+ entry.get_input_stream { |is| os << is.read }
1222
+ }
1223
+ end
1224
+
1225
+ def check_file(path)
1226
+ unless File.readable? path
1227
+ raise Errno::ENOENT, path
1228
+ end
1229
+ end
1230
+
1231
+ def on_success_replace(aFilename)
1232
+ tmpfile = get_tempfile
1233
+ tmpFilename = tmpfile.path
1234
+ tmpfile.close
1235
+ if yield tmpFilename
1236
+ File.move(tmpFilename, name)
1237
+ end
1238
+ end
1239
+
1240
+ def get_tempfile
1241
+ tempFile = Tempfile.new(File.basename(name), File.dirname(name))
1242
+ tempFile.binmode
1243
+ tempFile
1244
+ end
1245
+
1246
+ end
1247
+
1248
+ class ZipStreamableFile < DelegateClass(ZipEntry) #:nodoc:all
1249
+ def initialize(entry, filepath)
1250
+ super(entry)
1251
+ @delegate = entry
1252
+ @filepath = filepath
1253
+ end
1254
+
1255
+ def get_input_stream(&aProc)
1256
+ File.open(@filepath, "rb", &aProc)
1257
+ end
1258
+
1259
+ def write_to_zip_output_stream(aZipOutputStream)
1260
+ aZipOutputStream.put_next_entry(self)
1261
+ aZipOutputStream << get_input_stream { |is| is.read }
1262
+ end
1263
+
1264
+ def == (other)
1265
+ return false unless other.class == ZipStreamableFile
1266
+ @filepath == other.filepath && super(other.delegate)
1267
+ end
1268
+
1269
+ protected
1270
+ attr_reader :filepath, :delegate
1271
+ end
1272
+
1273
+ class ZipStreamableDirectory < DelegateClass(ZipEntry) #:nodoc:all
1274
+ def initialize(entry)
1275
+ super(entry)
1276
+ end
1277
+
1278
+ def get_input_stream(&aProc)
1279
+ return yield(NullInputStream.instance) if block_given?
1280
+ NullInputStream.instance
1281
+ end
1282
+
1283
+ def write_to_zip_output_stream(aZipOutputStream)
1284
+ aZipOutputStream.put_next_entry(self)
1285
+ end
1286
+ end
1287
+
1288
+ class ZipStreamableStream < DelegateClass(ZipEntry) #nodoc:all
1289
+ def initialize(entry)
1290
+ super(entry)
1291
+ @tempFile = Tempfile.new(File.basename(name), File.dirname(zipfile))
1292
+ @tempFile.binmode
1293
+ end
1294
+
1295
+ def get_output_stream
1296
+ if block_given?
1297
+ begin
1298
+ yield(@tempFile)
1299
+ ensure
1300
+ @tempFile.close
1301
+ end
1302
+ else
1303
+ @tempFile
1304
+ end
1305
+ end
1306
+
1307
+ def get_input_stream
1308
+ if ! @tempFile.closed?
1309
+ raise StandardError, "cannot open entry for reading while its open for writing - #{name}"
1310
+ end
1311
+ @tempFile.open # reopens tempfile from top
1312
+ if block_given?
1313
+ begin
1314
+ yield(@tempFile)
1315
+ ensure
1316
+ @tempFile.close
1317
+ end
1318
+ else
1319
+ @tempFile
1320
+ end
1321
+ end
1322
+
1323
+ def write_to_zip_output_stream(aZipOutputStream)
1324
+ aZipOutputStream.put_next_entry(self)
1325
+ aZipOutputStream << get_input_stream { |is| is.read }
1326
+ end
1327
+ end
1328
+
1329
+ class ZipExtraField < Hash
1330
+ ID_MAP = {}
1331
+
1332
+ # Meta class for extra fields
1333
+ class Generic
1334
+ def self.register_map
1335
+ if self.const_defined?(:HEADER_ID)
1336
+ ID_MAP[self.const_get(:HEADER_ID)] = self
1337
+ end
1338
+ end
1339
+
1340
+ def self.name
1341
+ self.to_s.split("::")[-1]
1342
+ end
1343
+
1344
+ # return field [size, content] or false
1345
+ def initial_parse(binstr)
1346
+ if ! binstr
1347
+ # If nil, start with empty.
1348
+ return false
1349
+ elsif binstr[0,2] != self.class.const_get(:HEADER_ID)
1350
+ $stderr.puts "Warning: weired extra feild header ID. skip parsing"
1351
+ return false
1352
+ end
1353
+ [binstr[2,2].unpack("v")[0], binstr[4..-1]]
1354
+ end
1355
+
1356
+ def ==(other)
1357
+ self.class != other.class and return false
1358
+ each { |k, v|
1359
+ v != other[k] and return false
1360
+ }
1361
+ true
1362
+ end
1363
+
1364
+ def to_local_bin
1365
+ s = pack_for_local
1366
+ self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s
1367
+ end
1368
+
1369
+ def to_c_dir_bin
1370
+ s = pack_for_c_dir
1371
+ self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s
1372
+ end
1373
+ end
1374
+
1375
+ # Info-ZIP Additional timestamp field
1376
+ class UniversalTime < Generic
1377
+ HEADER_ID = "UT"
1378
+ register_map
1379
+
1380
+ def initialize(binstr = nil)
1381
+ @ctime = nil
1382
+ @mtime = nil
1383
+ @atime = nil
1384
+ @flag = nil
1385
+ binstr and merge(binstr)
1386
+ end
1387
+ attr_accessor :atime, :ctime, :mtime, :flag
1388
+
1389
+ def merge(binstr)
1390
+ binstr == "" and return
1391
+ size, content = initial_parse(binstr)
1392
+ size or return
1393
+ @flag, mtime, atime, ctime = content.unpack("CVVV")
1394
+ mtime and @mtime ||= Time.at(mtime)
1395
+ atime and @atime ||= Time.at(atime)
1396
+ ctime and @ctime ||= Time.at(ctime)
1397
+ end
1398
+
1399
+ def ==(other)
1400
+ @mtime == other.mtime &&
1401
+ @atime == other.atime &&
1402
+ @ctime == other.ctime
1403
+ end
1404
+
1405
+ def pack_for_local
1406
+ s = [@flag].pack("C")
1407
+ @flag & 1 != 0 and s << [@mtime.to_i].pack("V")
1408
+ @flag & 2 != 0 and s << [@atime.to_i].pack("V")
1409
+ @flag & 4 != 0 and s << [@ctime.to_i].pack("V")
1410
+ s
1411
+ end
1412
+
1413
+ def pack_for_c_dir
1414
+ s = [@flag].pack("C")
1415
+ @flag & 1 == 1 and s << [@mtime.to_i].pack("V")
1416
+ s
1417
+ end
1418
+ end
1419
+
1420
+ # Info-ZIP Extra for UNIX uid/gid
1421
+ class IUnix < Generic
1422
+ HEADER_ID = "Ux"
1423
+ register_map
1424
+
1425
+ def initialize(binstr = nil)
1426
+ @uid = 0
1427
+ @gid = 0
1428
+ binstr and merge(binstr)
1429
+ end
1430
+ attr_accessor :uid, :gid
1431
+
1432
+ def merge(binstr)
1433
+ binstr == "" and return
1434
+ size, content = initial_parse(binstr)
1435
+ # size: 0 for central direcotry. 4 for local header
1436
+ return if(! size || size == 0)
1437
+ uid, gid = content.unpack("vv")
1438
+ @uid ||= uid
1439
+ @gid ||= gid
1440
+ end
1441
+
1442
+ def ==(other)
1443
+ @uid == other.uid &&
1444
+ @gid == other.gid
1445
+ end
1446
+
1447
+ def pack_for_local
1448
+ [@uid, @gid].pack("vv")
1449
+ end
1450
+
1451
+ def pack_for_c_dir
1452
+ ""
1453
+ end
1454
+ end
1455
+
1456
+ ## start main of ZipExtraField < Hash
1457
+ def initialize(binstr = nil)
1458
+ binstr and merge(binstr)
1459
+ end
1460
+
1461
+ def merge(binstr)
1462
+ binstr == "" and return
1463
+ i = 0
1464
+ while i < binstr.length
1465
+ id = binstr[i,2]
1466
+ len = binstr[i+2,2].to_s.unpack("v")[0]
1467
+ if id && ID_MAP.member?(id)
1468
+ field_name = ID_MAP[id].name
1469
+ if self.member?(field_name)
1470
+ self[field_name].mergea(binstr[i, len+4])
1471
+ else
1472
+ field_obj = ID_MAP[id].new(binstr[i, len+4])
1473
+ self[field_name] = field_obj
1474
+ end
1475
+ elsif id
1476
+ unless self["Unknown"]
1477
+ s = ""
1478
+ class << s
1479
+ alias_method :to_c_dir_bin, :to_s
1480
+ alias_method :to_local_bin, :to_s
1481
+ end
1482
+ self["Unknown"] = s
1483
+ end
1484
+ if ! len || len+4 > binstr[i..-1].length
1485
+ self["Unknown"] << binstr[i..-1]
1486
+ break;
1487
+ end
1488
+ self["Unknown"] << binstr[i, len+4]
1489
+ end
1490
+ i += len+4
1491
+ end
1492
+ end
1493
+
1494
+ def create(name)
1495
+ field_class = nil
1496
+ ID_MAP.each { |id, klass|
1497
+ if klass.name == name
1498
+ field_class = klass
1499
+ break
1500
+ end
1501
+ }
1502
+ if ! field_class
1503
+ raise ZipError, "Unknown extra field '#{name}'"
1504
+ end
1505
+ self[name] = field_class.new()
1506
+ end
1507
+
1508
+ def to_local_bin
1509
+ s = ""
1510
+ each { |k, v|
1511
+ s << v.to_local_bin
1512
+ }
1513
+ s
1514
+ end
1515
+ alias :to_s :to_local_bin
1516
+
1517
+ def to_c_dir_bin
1518
+ s = ""
1519
+ each { |k, v|
1520
+ s << v.to_c_dir_bin
1521
+ }
1522
+ s
1523
+ end
1524
+
1525
+ def c_dir_length
1526
+ to_c_dir_bin.length
1527
+ end
1528
+ def local_length
1529
+ to_local_bin.length
1530
+ end
1531
+ alias :c_dir_size :c_dir_length
1532
+ alias :local_size :local_length
1533
+ alias :length :local_length
1534
+ alias :size :local_length
1535
+ end # end ZipExtraField
1536
+
1537
+ end # Zip namespace module
1538
+
1539
+
1540
+
1541
+ # Copyright (C) 2002, 2003 Thomas Sondergaard
1542
+ # rubyzip is free software; you can redistribute it and/or
1543
+ # modify it under the terms of the ruby license.