rubyzip 0.9.1 → 2.3.2

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