FreedomCoder-rubyzip 0.9.2 → 0.9.3

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