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