viva-rubyzip 0.9.1.1

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