pik 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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.