ruby_archive 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.