pik 0.1.1 → 0.2.0

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