IronDigital-rubyzip 0.9.2

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,1857 @@
1
+ require 'delegate'
2
+ require 'singleton'
3
+ require 'tempfile'
4
+
5
+ begin
6
+ require 'ftools'
7
+ rescue LoadError
8
+ require 'fileutils'
9
+ end
10
+
11
+ require 'stringio'
12
+ require 'zlib'
13
+ require 'zip/stdrubyext'
14
+ require 'zip/ioextras'
15
+
16
+ if Tempfile.superclass == SimpleDelegator
17
+ require 'zip/tempfile_bugfixed'
18
+ Tempfile = BugFix::Tempfile
19
+ end
20
+
21
+ module Zlib #:nodoc:all
22
+ if ! const_defined? :MAX_WBITS
23
+ MAX_WBITS = Zlib::Deflate.MAX_WBITS
24
+ end
25
+ end
26
+
27
+ module Zip
28
+
29
+ VERSION = '0.9.2'
30
+
31
+ RUBY_MINOR_VERSION = RUBY_VERSION.split(".")[1].to_i
32
+
33
+ RUNNING_ON_WINDOWS = /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM
34
+
35
+ # Ruby 1.7.x compatibility
36
+ # In ruby 1.6.x and 1.8.0 reading from an empty stream returns
37
+ # an empty string the first time and then nil.
38
+ # not so in 1.7.x
39
+ EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST = RUBY_MINOR_VERSION != 7
40
+
41
+ # ZipInputStream is the basic class for reading zip entries in a
42
+ # zip file. It is possible to create a ZipInputStream object directly,
43
+ # passing the zip file name to the constructor, but more often than not
44
+ # the ZipInputStream will be obtained from a ZipFile (perhaps using the
45
+ # ZipFileSystem interface) object for a particular entry in the zip
46
+ # archive.
47
+ #
48
+ # A ZipInputStream inherits IOExtras::AbstractInputStream in order
49
+ # to provide an IO-like interface for reading from a single zip
50
+ # entry. Beyond methods for mimicking an IO-object it contains
51
+ # the method get_next_entry for iterating through the entries of
52
+ # an archive. get_next_entry returns a ZipEntry object that describes
53
+ # the zip entry the ZipInputStream is currently reading from.
54
+ #
55
+ # Example that creates a zip archive with ZipOutputStream and reads it
56
+ # back again with a ZipInputStream.
57
+ #
58
+ # require 'zip/zip'
59
+ #
60
+ # Zip::ZipOutputStream::open("my.zip") {
61
+ # |io|
62
+ #
63
+ # io.put_next_entry("first_entry.txt")
64
+ # io.write "Hello world!"
65
+ #
66
+ # io.put_next_entry("adir/first_entry.txt")
67
+ # io.write "Hello again!"
68
+ # }
69
+ #
70
+ #
71
+ # Zip::ZipInputStream::open("my.zip") {
72
+ # |io|
73
+ #
74
+ # while (entry = io.get_next_entry)
75
+ # puts "Contents of #{entry.name}: '#{io.read}'"
76
+ # end
77
+ # }
78
+ #
79
+ # java.util.zip.ZipInputStream is the original inspiration for this
80
+ # class.
81
+
82
+ class ZipInputStream
83
+ include IOExtras::AbstractInputStream
84
+
85
+ # Opens the indicated zip file. An exception is thrown
86
+ # if the specified offset in the specified filename is
87
+ # not a local zip entry header.
88
+ def initialize(filename, offset = 0)
89
+ super()
90
+ @archiveIO = File.open(filename, "rb")
91
+ @archiveIO.seek(offset, IO::SEEK_SET)
92
+ @decompressor = NullDecompressor.instance
93
+ @currentEntry = nil
94
+ end
95
+
96
+ def close
97
+ @archiveIO.close
98
+ end
99
+
100
+ # Same as #initialize but if a block is passed the opened
101
+ # stream is passed to the block and closed when the block
102
+ # returns.
103
+ def ZipInputStream.open(filename)
104
+ return new(filename) unless block_given?
105
+
106
+ zio = new(filename)
107
+ yield zio
108
+ ensure
109
+ zio.close if zio
110
+ end
111
+
112
+ # Returns a ZipEntry object. It is necessary to call this
113
+ # method on a newly created ZipInputStream before reading from
114
+ # the first entry in the archive. Returns nil when there are
115
+ # no more entries.
116
+
117
+ def get_next_entry
118
+ @archiveIO.seek(@currentEntry.next_header_offset,
119
+ IO::SEEK_SET) if @currentEntry
120
+ open_entry
121
+ end
122
+
123
+ # Rewinds the stream to the beginning of the current entry
124
+ def rewind
125
+ return if @currentEntry.nil?
126
+ @lineno = 0
127
+ @archiveIO.seek(@currentEntry.localHeaderOffset,
128
+ IO::SEEK_SET)
129
+ open_entry
130
+ end
131
+
132
+ # Modeled after IO.sysread
133
+ def sysread(numberOfBytes = nil, buf = nil)
134
+ @decompressor.sysread(numberOfBytes, buf)
135
+ end
136
+
137
+ def eof
138
+ @outputBuffer.empty? && @decompressor.eof
139
+ end
140
+ alias :eof? :eof
141
+
142
+ protected
143
+
144
+ def open_entry
145
+ @currentEntry = ZipEntry.read_local_entry(@archiveIO)
146
+ if (@currentEntry == nil)
147
+ @decompressor = NullDecompressor.instance
148
+ elsif @currentEntry.compression_method == ZipEntry::STORED
149
+ @decompressor = PassThruDecompressor.new(@archiveIO,
150
+ @currentEntry.size)
151
+ elsif @currentEntry.compression_method == ZipEntry::DEFLATED
152
+ @decompressor = Inflater.new(@archiveIO)
153
+ else
154
+ raise ZipCompressionMethodError,
155
+ "Unsupported compression method #{@currentEntry.compression_method}"
156
+ end
157
+ flush
158
+ return @currentEntry
159
+ end
160
+
161
+ def produce_input
162
+ @decompressor.produce_input
163
+ end
164
+
165
+ def input_finished?
166
+ @decompressor.input_finished?
167
+ end
168
+ end
169
+
170
+
171
+
172
+ class Decompressor #:nodoc:all
173
+ CHUNK_SIZE=32768
174
+ def initialize(inputStream)
175
+ super()
176
+ @inputStream=inputStream
177
+ end
178
+ end
179
+
180
+ class Inflater < Decompressor #:nodoc:all
181
+ def initialize(inputStream)
182
+ super
183
+ @zlibInflater = Zlib::Inflate.new(-Zlib::MAX_WBITS)
184
+ @outputBuffer=""
185
+ @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
186
+ end
187
+
188
+ def sysread(numberOfBytes = nil, buf = nil)
189
+ readEverything = (numberOfBytes == nil)
190
+ while (readEverything || @outputBuffer.length < numberOfBytes)
191
+ break if internal_input_finished?
192
+ @outputBuffer << internal_produce_input(buf)
193
+ end
194
+ return value_when_finished if @outputBuffer.length==0 && input_finished?
195
+ endIndex= numberOfBytes==nil ? @outputBuffer.length : numberOfBytes
196
+ return @outputBuffer.slice!(0...endIndex)
197
+ end
198
+
199
+ def produce_input
200
+ if (@outputBuffer.empty?)
201
+ return internal_produce_input
202
+ else
203
+ return @outputBuffer.slice!(0...(@outputBuffer.length))
204
+ end
205
+ end
206
+
207
+ # to be used with produce_input, not read (as read may still have more data cached)
208
+ # is data cached anywhere other than @outputBuffer? the comment above may be wrong
209
+ def input_finished?
210
+ @outputBuffer.empty? && internal_input_finished?
211
+ end
212
+ alias :eof :input_finished?
213
+ alias :eof? :input_finished?
214
+
215
+ private
216
+
217
+ def internal_produce_input(buf = nil)
218
+ retried = 0
219
+ begin
220
+ @zlibInflater.inflate(@inputStream.read(Decompressor::CHUNK_SIZE, buf))
221
+ rescue Zlib::BufError
222
+ raise if (retried >= 5) # how many times should we retry?
223
+ retried += 1
224
+ retry
225
+ end
226
+ end
227
+
228
+ def internal_input_finished?
229
+ @zlibInflater.finished?
230
+ end
231
+
232
+ # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
233
+ def value_when_finished # mimic behaviour of ruby File object.
234
+ return nil if @hasReturnedEmptyString
235
+ @hasReturnedEmptyString=true
236
+ return ""
237
+ end
238
+ end
239
+
240
+ class PassThruDecompressor < Decompressor #:nodoc:all
241
+ def initialize(inputStream, charsToRead)
242
+ super inputStream
243
+ @charsToRead = charsToRead
244
+ @readSoFar = 0
245
+ @hasReturnedEmptyString = ! EMPTY_FILE_RETURNS_EMPTY_STRING_FIRST
246
+ end
247
+
248
+ # TODO: Specialize to handle different behaviour in ruby > 1.7.0 ?
249
+ def sysread(numberOfBytes = nil, buf = nil)
250
+ if input_finished?
251
+ hasReturnedEmptyStringVal=@hasReturnedEmptyString
252
+ @hasReturnedEmptyString=true
253
+ return "" unless hasReturnedEmptyStringVal
254
+ return nil
255
+ end
256
+
257
+ if (numberOfBytes == nil || @readSoFar+numberOfBytes > @charsToRead)
258
+ numberOfBytes = @charsToRead-@readSoFar
259
+ end
260
+ @readSoFar += numberOfBytes
261
+ @inputStream.read(numberOfBytes, buf)
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
+ File.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
+ foundEntry.name=newName
1463
+ end
1464
+
1465
+ # Replaces the specified entry with the contents of srcPath (from
1466
+ # the file system).
1467
+ def replace(entry, srcPath)
1468
+ check_file(srcPath)
1469
+ add(remove(entry), srcPath)
1470
+ end
1471
+
1472
+ # Extracts entry to file destPath.
1473
+ def extract(entry, destPath, &onExistsProc)
1474
+ onExistsProc ||= proc { false }
1475
+ foundEntry = get_entry(entry)
1476
+ foundEntry.extract(destPath, &onExistsProc)
1477
+ end
1478
+
1479
+ # Commits changes that has been made since the previous commit to
1480
+ # the zip archive.
1481
+ def commit
1482
+ return if ! commit_required?
1483
+ on_success_replace(name) {
1484
+ |tmpFile|
1485
+ ZipOutputStream.open(tmpFile) {
1486
+ |zos|
1487
+
1488
+ @entrySet.each { |e| e.write_to_zip_output_stream(zos) }
1489
+ zos.comment = comment
1490
+ }
1491
+ true
1492
+ }
1493
+ initialize(name)
1494
+ end
1495
+
1496
+ # Closes the zip file committing any changes that has been made.
1497
+ def close
1498
+ commit
1499
+ end
1500
+
1501
+ # Returns true if any changes has been made to this archive since
1502
+ # the previous commit
1503
+ def commit_required?
1504
+ return @entrySet != @storedEntries || @create == ZipFile::CREATE
1505
+ end
1506
+
1507
+ # Searches for entry with the specified name. Returns nil if
1508
+ # no entry is found. See also get_entry
1509
+ def find_entry(entry)
1510
+ @entrySet.detect {
1511
+ |e|
1512
+ e.name.sub(/\/$/, "") == entry.to_s.sub(/\/$/, "")
1513
+ }
1514
+ end
1515
+
1516
+ # Searches for an entry just as find_entry, but throws Errno::ENOENT
1517
+ # if no entry is found.
1518
+ def get_entry(entry)
1519
+ selectedEntry = find_entry(entry)
1520
+ unless selectedEntry
1521
+ raise Errno::ENOENT, entry
1522
+ end
1523
+ selectedEntry.restore_ownership = @restore_ownership
1524
+ selectedEntry.restore_permissions = @restore_permissions
1525
+ selectedEntry.restore_times = @restore_times
1526
+
1527
+ return selectedEntry
1528
+ end
1529
+
1530
+ # Creates a directory
1531
+ def mkdir(entryName, permissionInt = 0755)
1532
+ if find_entry(entryName)
1533
+ raise Errno::EEXIST, "File exists - #{entryName}"
1534
+ end
1535
+ @entrySet << ZipStreamableDirectory.new(@name, entryName.to_s.ensure_end("/"), nil, permissionInt)
1536
+ end
1537
+
1538
+ private
1539
+
1540
+ def is_directory(newEntry, srcPath)
1541
+ srcPathIsDirectory = File.directory?(srcPath)
1542
+ if newEntry.is_directory && ! srcPathIsDirectory
1543
+ raise ArgumentError,
1544
+ "entry name '#{newEntry}' indicates directory entry, but "+
1545
+ "'#{srcPath}' is not a directory"
1546
+ elsif ! newEntry.is_directory && srcPathIsDirectory
1547
+ newEntry.name += "/"
1548
+ end
1549
+ return newEntry.is_directory && srcPathIsDirectory
1550
+ end
1551
+
1552
+ def check_entry_exists(entryName, continueOnExistsProc, procedureName)
1553
+ continueOnExistsProc ||= proc { false }
1554
+ if @entrySet.detect { |e| e.name == entryName }
1555
+ if continueOnExistsProc.call
1556
+ remove get_entry(entryName)
1557
+ else
1558
+ raise ZipEntryExistsError,
1559
+ procedureName+" failed. Entry #{entryName} already exists"
1560
+ end
1561
+ end
1562
+ end
1563
+
1564
+ def check_file(path)
1565
+ unless File.readable? path
1566
+ raise Errno::ENOENT, path
1567
+ end
1568
+ end
1569
+
1570
+ def on_success_replace(aFilename)
1571
+ tmpfile = get_tempfile
1572
+ tmpFilename = tmpfile.path
1573
+ tmpfile.close
1574
+ if yield tmpFilename
1575
+ begin
1576
+ File.move(tmpFilename, name)
1577
+ rescue
1578
+ FileUtils.move(tmpFilename, name)
1579
+ end
1580
+ end
1581
+ end
1582
+
1583
+ def get_tempfile
1584
+ tempFile = Tempfile.new(File.basename(name), File.dirname(name))
1585
+ tempFile.binmode
1586
+ tempFile
1587
+ end
1588
+
1589
+ end
1590
+
1591
+ class ZipStreamableDirectory < ZipEntry
1592
+ def initialize(zipfile, entry, srcPath = nil, permissionInt = nil)
1593
+ super(zipfile, entry)
1594
+
1595
+ @ftype = :directory
1596
+ entry.get_extra_attributes_from_path(srcPath) if (srcPath)
1597
+ @unix_perms = permissionInt if (permissionInt)
1598
+ end
1599
+ end
1600
+
1601
+ class ZipStreamableStream < DelegateClass(ZipEntry) #nodoc:all
1602
+ def initialize(entry)
1603
+ super(entry)
1604
+ @tempFile = Tempfile.new(File.basename(name), File.dirname(zipfile))
1605
+ @tempFile.binmode
1606
+ end
1607
+
1608
+ def get_output_stream
1609
+ if block_given?
1610
+ begin
1611
+ yield(@tempFile)
1612
+ ensure
1613
+ @tempFile.close
1614
+ end
1615
+ else
1616
+ @tempFile
1617
+ end
1618
+ end
1619
+
1620
+ def get_input_stream
1621
+ if ! @tempFile.closed?
1622
+ raise StandardError, "cannot open entry for reading while its open for writing - #{name}"
1623
+ end
1624
+ @tempFile.open # reopens tempfile from top
1625
+ @tempFile.binmode
1626
+ if block_given?
1627
+ begin
1628
+ yield(@tempFile)
1629
+ ensure
1630
+ @tempFile.close
1631
+ end
1632
+ else
1633
+ @tempFile
1634
+ end
1635
+ end
1636
+
1637
+ def write_to_zip_output_stream(aZipOutputStream)
1638
+ aZipOutputStream.put_next_entry(self)
1639
+ get_input_stream { |is| IOExtras.copy_stream(aZipOutputStream, is) }
1640
+ end
1641
+ end
1642
+
1643
+ class ZipExtraField < Hash
1644
+ ID_MAP = {}
1645
+
1646
+ # Meta class for extra fields
1647
+ class Generic
1648
+ def self.register_map
1649
+ if self.const_defined?(:HEADER_ID)
1650
+ ID_MAP[self.const_get(:HEADER_ID)] = self
1651
+ end
1652
+ end
1653
+
1654
+ def self.name
1655
+ self.to_s.split("::")[-1]
1656
+ end
1657
+
1658
+ # return field [size, content] or false
1659
+ def initial_parse(binstr)
1660
+ if ! binstr
1661
+ # If nil, start with empty.
1662
+ return false
1663
+ elsif binstr[0,2] != self.class.const_get(:HEADER_ID)
1664
+ $stderr.puts "Warning: weired extra feild header ID. skip parsing"
1665
+ return false
1666
+ end
1667
+ [binstr[2,2].unpack("v")[0], binstr[4..-1]]
1668
+ end
1669
+
1670
+ def ==(other)
1671
+ self.class != other.class and return false
1672
+ each { |k, v|
1673
+ v != other[k] and return false
1674
+ }
1675
+ true
1676
+ end
1677
+
1678
+ def to_local_bin
1679
+ s = pack_for_local
1680
+ self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s
1681
+ end
1682
+
1683
+ def to_c_dir_bin
1684
+ s = pack_for_c_dir
1685
+ self.class.const_get(:HEADER_ID) + [s.length].pack("v") + s
1686
+ end
1687
+ end
1688
+
1689
+ # Info-ZIP Additional timestamp field
1690
+ class UniversalTime < Generic
1691
+ HEADER_ID = "UT"
1692
+ register_map
1693
+
1694
+ def initialize(binstr = nil)
1695
+ @ctime = nil
1696
+ @mtime = nil
1697
+ @atime = nil
1698
+ @flag = nil
1699
+ binstr and merge(binstr)
1700
+ end
1701
+ attr_accessor :atime, :ctime, :mtime, :flag
1702
+
1703
+ def merge(binstr)
1704
+ binstr == "" and return
1705
+ size, content = initial_parse(binstr)
1706
+ size or return
1707
+ @flag, mtime, atime, ctime = content.unpack("CVVV")
1708
+ mtime and @mtime ||= Time.at(mtime)
1709
+ atime and @atime ||= Time.at(atime)
1710
+ ctime and @ctime ||= Time.at(ctime)
1711
+ end
1712
+
1713
+ def ==(other)
1714
+ @mtime == other.mtime &&
1715
+ @atime == other.atime &&
1716
+ @ctime == other.ctime
1717
+ end
1718
+
1719
+ def pack_for_local
1720
+ s = [@flag].pack("C")
1721
+ @flag & 1 != 0 and s << [@mtime.to_i].pack("V")
1722
+ @flag & 2 != 0 and s << [@atime.to_i].pack("V")
1723
+ @flag & 4 != 0 and s << [@ctime.to_i].pack("V")
1724
+ s
1725
+ end
1726
+
1727
+ def pack_for_c_dir
1728
+ s = [@flag].pack("C")
1729
+ @flag & 1 == 1 and s << [@mtime.to_i].pack("V")
1730
+ s
1731
+ end
1732
+ end
1733
+
1734
+ # Info-ZIP Extra for UNIX uid/gid
1735
+ class IUnix < Generic
1736
+ HEADER_ID = "Ux"
1737
+ register_map
1738
+
1739
+ def initialize(binstr = nil)
1740
+ @uid = 0
1741
+ @gid = 0
1742
+ binstr and merge(binstr)
1743
+ end
1744
+ attr_accessor :uid, :gid
1745
+
1746
+ def merge(binstr)
1747
+ binstr == "" and return
1748
+ size, content = initial_parse(binstr)
1749
+ # size: 0 for central direcotry. 4 for local header
1750
+ return if(! size || size == 0)
1751
+ uid, gid = content.unpack("vv")
1752
+ @uid ||= uid
1753
+ @gid ||= gid
1754
+ end
1755
+
1756
+ def ==(other)
1757
+ @uid == other.uid &&
1758
+ @gid == other.gid
1759
+ end
1760
+
1761
+ def pack_for_local
1762
+ [@uid, @gid].pack("vv")
1763
+ end
1764
+
1765
+ def pack_for_c_dir
1766
+ ""
1767
+ end
1768
+ end
1769
+
1770
+ ## start main of ZipExtraField < Hash
1771
+ def initialize(binstr = nil)
1772
+ binstr and merge(binstr)
1773
+ end
1774
+
1775
+ def merge(binstr)
1776
+ binstr == "" and return
1777
+ i = 0
1778
+ while i < binstr.length
1779
+ id = binstr[i,2]
1780
+ len = binstr[i+2,2].to_s.unpack("v")[0]
1781
+ if id && ID_MAP.member?(id)
1782
+ field_name = ID_MAP[id].name
1783
+ if self.member?(field_name)
1784
+ self[field_name].mergea(binstr[i, len+4])
1785
+ else
1786
+ field_obj = ID_MAP[id].new(binstr[i, len+4])
1787
+ self[field_name] = field_obj
1788
+ end
1789
+ elsif id
1790
+ unless self["Unknown"]
1791
+ s = ""
1792
+ class << s
1793
+ alias_method :to_c_dir_bin, :to_s
1794
+ alias_method :to_local_bin, :to_s
1795
+ end
1796
+ self["Unknown"] = s
1797
+ end
1798
+ if ! len || len+4 > binstr[i..-1].length
1799
+ self["Unknown"] << binstr[i..-1]
1800
+ break;
1801
+ end
1802
+ self["Unknown"] << binstr[i, len+4]
1803
+ end
1804
+ i += len+4
1805
+ end
1806
+ end
1807
+
1808
+ def create(name)
1809
+ field_class = nil
1810
+ ID_MAP.each { |id, klass|
1811
+ if klass.name == name
1812
+ field_class = klass
1813
+ break
1814
+ end
1815
+ }
1816
+ if ! field_class
1817
+ raise ZipError, "Unknown extra field '#{name}'"
1818
+ end
1819
+ self[name] = field_class.new()
1820
+ end
1821
+
1822
+ def to_local_bin
1823
+ s = ""
1824
+ each { |k, v|
1825
+ s << v.to_local_bin
1826
+ }
1827
+ s
1828
+ end
1829
+ alias :to_s :to_local_bin
1830
+
1831
+ def to_c_dir_bin
1832
+ s = ""
1833
+ each { |k, v|
1834
+ s << v.to_c_dir_bin
1835
+ }
1836
+ s
1837
+ end
1838
+
1839
+ def c_dir_length
1840
+ to_c_dir_bin.length
1841
+ end
1842
+ def local_length
1843
+ to_local_bin.length
1844
+ end
1845
+ alias :c_dir_size :c_dir_length
1846
+ alias :local_size :local_length
1847
+ alias :length :local_length
1848
+ alias :size :local_length
1849
+ end # end ZipExtraField
1850
+
1851
+ end # Zip namespace module
1852
+
1853
+
1854
+
1855
+ # Copyright (C) 2002, 2003 Thomas Sondergaard
1856
+ # rubyzip is free software; you can redistribute it and/or
1857
+ # modify it under the terms of the ruby license.