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