rant 0.3.8 → 0.4.0

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