rant 0.3.8 → 0.4.0

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