archive-zip 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (132) hide show
  1. data/HACKING +25 -42
  2. data/NEWS +25 -0
  3. data/README +2 -2
  4. data/Rakefile +202 -0
  5. data/TODO +5 -0
  6. data/default.mspec +8 -0
  7. data/lib/archive/support/binary_stringio.rb +23 -0
  8. data/lib/archive/support/integer.rb +13 -0
  9. data/lib/archive/support/io-like.rb +3 -1
  10. data/lib/archive/support/ioextensions.rb +16 -0
  11. data/lib/archive/support/iowindow.rb +10 -18
  12. data/lib/archive/support/time.rb +2 -0
  13. data/lib/archive/support/zlib.rb +298 -71
  14. data/lib/archive/zip.rb +161 -139
  15. data/lib/archive/zip/codec.rb +2 -0
  16. data/lib/archive/zip/codec/deflate.rb +59 -11
  17. data/lib/archive/zip/codec/null_encryption.rb +75 -14
  18. data/lib/archive/zip/codec/store.rb +75 -26
  19. data/lib/archive/zip/codec/traditional_encryption.rb +146 -35
  20. data/lib/archive/zip/data_descriptor.rb +6 -4
  21. data/lib/archive/zip/entry.rb +184 -132
  22. data/lib/archive/zip/error.rb +2 -0
  23. data/lib/archive/zip/extra_field.rb +20 -6
  24. data/lib/archive/zip/extra_field/extended_timestamp.rb +141 -60
  25. data/lib/archive/zip/extra_field/raw.rb +70 -12
  26. data/lib/archive/zip/extra_field/unix.rb +58 -16
  27. data/lib/archive/zip/version.rb +6 -0
  28. data/spec/archive/zip/codec/deflate/compress/checksum_spec.rb +42 -0
  29. data/spec/archive/zip/codec/deflate/compress/close_spec.rb +44 -0
  30. data/spec/archive/zip/codec/deflate/compress/crc32_spec.rb +21 -0
  31. data/spec/archive/zip/codec/deflate/compress/data_descriptor_spec.rb +67 -0
  32. data/spec/archive/zip/codec/deflate/compress/new_spec.rb +37 -0
  33. data/spec/archive/zip/codec/deflate/compress/open_spec.rb +46 -0
  34. data/spec/archive/zip/codec/deflate/compress/write_spec.rb +109 -0
  35. data/spec/archive/zip/codec/deflate/decompress/checksum_spec.rb +18 -0
  36. data/spec/archive/zip/codec/deflate/decompress/close_spec.rb +33 -0
  37. data/spec/archive/zip/codec/deflate/decompress/crc32_spec.rb +18 -0
  38. data/spec/archive/zip/codec/deflate/decompress/data_descriptor_spec.rb +67 -0
  39. data/spec/archive/zip/codec/deflate/decompress/new_spec.rb +14 -0
  40. data/spec/archive/zip/codec/deflate/decompress/open_spec.rb +27 -0
  41. data/spec/archive/zip/codec/deflate/fixtures/classes.rb +25 -0
  42. data/spec/archive/zip/codec/deflate/fixtures/compressed_file.bin +1 -0
  43. data/spec/archive/zip/codec/deflate/fixtures/compressed_file_nocomp.bin +0 -0
  44. data/spec/archive/zip/codec/deflate/fixtures/raw_file.txt +10 -0
  45. data/spec/archive/zip/codec/null_encryption/decrypt/close_spec.rb +33 -0
  46. data/spec/archive/zip/codec/null_encryption/decrypt/new_spec.rb +14 -0
  47. data/spec/archive/zip/codec/null_encryption/decrypt/open_spec.rb +27 -0
  48. data/spec/archive/zip/codec/null_encryption/decrypt/read_spec.rb +24 -0
  49. data/spec/archive/zip/codec/null_encryption/decrypt/rewind_spec.rb +25 -0
  50. data/spec/archive/zip/codec/null_encryption/decrypt/seek_spec.rb +57 -0
  51. data/spec/archive/zip/codec/null_encryption/decrypt/tell_spec.rb +21 -0
  52. data/spec/archive/zip/codec/null_encryption/encrypt/close_spec.rb +33 -0
  53. data/spec/archive/zip/codec/null_encryption/encrypt/new_spec.rb +14 -0
  54. data/spec/archive/zip/codec/null_encryption/encrypt/open_spec.rb +27 -0
  55. data/spec/archive/zip/codec/null_encryption/encrypt/rewind_spec.rb +26 -0
  56. data/spec/archive/zip/codec/null_encryption/encrypt/seek_spec.rb +50 -0
  57. data/spec/archive/zip/codec/null_encryption/encrypt/tell_spec.rb +29 -0
  58. data/spec/archive/zip/codec/null_encryption/encrypt/write_spec.rb +29 -0
  59. data/spec/archive/zip/codec/null_encryption/fixtures/classes.rb +12 -0
  60. data/spec/archive/zip/codec/null_encryption/fixtures/raw_file.txt +10 -0
  61. data/spec/archive/zip/codec/store/compress/close_spec.rb +33 -0
  62. data/spec/archive/zip/codec/store/compress/data_descriptor_spec.rb +68 -0
  63. data/spec/archive/zip/codec/store/compress/new_spec.rb +14 -0
  64. data/spec/archive/zip/codec/store/compress/open_spec.rb +27 -0
  65. data/spec/archive/zip/codec/store/compress/rewind_spec.rb +26 -0
  66. data/spec/archive/zip/codec/store/compress/seek_spec.rb +50 -0
  67. data/spec/archive/zip/codec/store/compress/tell_spec.rb +29 -0
  68. data/spec/archive/zip/codec/store/compress/write_spec.rb +29 -0
  69. data/spec/archive/zip/codec/store/decompress/close_spec.rb +33 -0
  70. data/spec/archive/zip/codec/store/decompress/data_descriptor_spec.rb +68 -0
  71. data/spec/archive/zip/codec/store/decompress/new_spec.rb +14 -0
  72. data/spec/archive/zip/codec/store/decompress/open_spec.rb +27 -0
  73. data/spec/archive/zip/codec/store/decompress/read_spec.rb +24 -0
  74. data/spec/archive/zip/codec/store/decompress/rewind_spec.rb +25 -0
  75. data/spec/archive/zip/codec/store/decompress/seek_spec.rb +57 -0
  76. data/spec/archive/zip/codec/store/decompress/tell_spec.rb +21 -0
  77. data/spec/archive/zip/codec/store/fixtures/classes.rb +12 -0
  78. data/spec/archive/zip/codec/store/fixtures/raw_file.txt +10 -0
  79. data/spec/archive/zip/codec/traditional_encryption/decrypt/close_spec.rb +64 -0
  80. data/spec/archive/zip/codec/traditional_encryption/decrypt/new_spec.rb +18 -0
  81. data/spec/archive/zip/codec/traditional_encryption/decrypt/open_spec.rb +39 -0
  82. data/spec/archive/zip/codec/traditional_encryption/decrypt/read_spec.rb +126 -0
  83. data/spec/archive/zip/codec/traditional_encryption/decrypt/rewind_spec.rb +38 -0
  84. data/spec/archive/zip/codec/traditional_encryption/decrypt/seek_spec.rb +82 -0
  85. data/spec/archive/zip/codec/traditional_encryption/decrypt/tell_spec.rb +25 -0
  86. data/spec/archive/zip/codec/traditional_encryption/encrypt/close_spec.rb +64 -0
  87. data/spec/archive/zip/codec/traditional_encryption/encrypt/new_spec.rb +18 -0
  88. data/spec/archive/zip/codec/traditional_encryption/encrypt/open_spec.rb +39 -0
  89. data/spec/archive/zip/codec/traditional_encryption/encrypt/rewind_spec.rb +41 -0
  90. data/spec/archive/zip/codec/traditional_encryption/encrypt/seek_spec.rb +75 -0
  91. data/spec/archive/zip/codec/traditional_encryption/encrypt/tell_spec.rb +42 -0
  92. data/spec/archive/zip/codec/traditional_encryption/encrypt/write_spec.rb +127 -0
  93. data/spec/archive/zip/codec/traditional_encryption/fixtures/classes.rb +27 -0
  94. data/spec/archive/zip/codec/traditional_encryption/fixtures/encrypted_file.bin +0 -0
  95. data/spec/archive/zip/codec/traditional_encryption/fixtures/raw_file.txt +10 -0
  96. data/spec/binary_stringio/new_spec.rb +34 -0
  97. data/spec/binary_stringio/set_encoding_spec.rb +14 -0
  98. data/spec/ioextensions/read_exactly_spec.rb +50 -0
  99. data/spec/zlib/fixtures/classes.rb +65 -0
  100. data/spec/zlib/fixtures/compressed_file.bin +1 -0
  101. data/spec/zlib/fixtures/compressed_file_gzip.bin +0 -0
  102. data/spec/zlib/fixtures/compressed_file_huffman.bin +2 -0
  103. data/spec/zlib/fixtures/compressed_file_minmem.bin +0 -0
  104. data/spec/zlib/fixtures/compressed_file_minwin.bin +1 -0
  105. data/spec/zlib/fixtures/compressed_file_nocomp.bin +0 -0
  106. data/spec/zlib/fixtures/compressed_file_raw.bin +1 -0
  107. data/spec/zlib/fixtures/raw_file.txt +10 -0
  108. data/spec/zlib/zreader/checksum_spec.rb +40 -0
  109. data/spec/zlib/zreader/close_spec.rb +14 -0
  110. data/spec/zlib/zreader/compressed_size_spec.rb +18 -0
  111. data/spec/zlib/zreader/new_spec.rb +41 -0
  112. data/spec/zlib/zreader/open_spec.rb +49 -0
  113. data/spec/zlib/zreader/read_spec.rb +47 -0
  114. data/spec/zlib/zreader/rewind_spec.rb +23 -0
  115. data/spec/zlib/zreader/seek_spec.rb +55 -0
  116. data/spec/zlib/zreader/tell_spec.rb +21 -0
  117. data/spec/zlib/zreader/uncompressed_size_spec.rb +18 -0
  118. data/spec/zlib/zwriter/checksum_spec.rb +41 -0
  119. data/spec/zlib/zwriter/close_spec.rb +14 -0
  120. data/spec/zlib/zwriter/compressed_size_spec.rb +19 -0
  121. data/spec/zlib/zwriter/new_spec.rb +64 -0
  122. data/spec/zlib/zwriter/open_spec.rb +68 -0
  123. data/spec/zlib/zwriter/rewind_spec.rb +26 -0
  124. data/spec/zlib/zwriter/seek_spec.rb +54 -0
  125. data/spec/zlib/zwriter/tell_spec.rb +29 -0
  126. data/spec/zlib/zwriter/uncompressed_size_spec.rb +19 -0
  127. data/spec/zlib/zwriter/write_spec.rb +28 -0
  128. data/spec_helper.rb +49 -0
  129. metadata +296 -74
  130. data/MANIFEST +0 -27
  131. data/lib/archive/support/io.rb +0 -14
  132. data/lib/archive/support/stringio.rb +0 -22
data/lib/archive/zip.rb CHANGED
@@ -1,12 +1,11 @@
1
- #!/usr/bin/env ruby
1
+ # encoding: UTF-8
2
2
 
3
3
  require 'fileutils'
4
4
  require 'set'
5
5
  require 'tempfile'
6
6
 
7
- require 'archive/support/io'
7
+ require 'archive/support/ioextensions'
8
8
  require 'archive/support/iowindow'
9
- require 'archive/support/stringio'
10
9
  require 'archive/support/time'
11
10
  require 'archive/support/zlib'
12
11
  require 'archive/zip/codec'
@@ -44,20 +43,61 @@ module Archive # :nodoc:
44
43
  DD_SIGNATURE = "PK\x7\x8" # 0x08074b50
45
44
 
46
45
 
47
- # A convenience method which opens a new or existing archive located in the
48
- # path indicated by _archive_path_, adds and updates entries based on the
49
- # paths given in _paths_, and then saves and closes the archive. See the
50
- # instance method #archive for more information about _paths_ and _options_.
51
- def self.archive(archive_path, paths, options = {})
52
- open(archive_path) { |z| z.archive(paths, options) }
46
+ # Creates or possibly updates an archive using _paths_ for new contents.
47
+ #
48
+ # If _archive_ is a String, it is treated as a file path which will receive
49
+ # the archive contents. If the file already exists, it is assumed to be an
50
+ # archive and will be updated "in place". Otherwise, a new archive
51
+ # is created. The archive will be closed once written.
52
+ #
53
+ # If _archive_ has any other kind of value, it is treated as a writable
54
+ # IO-like object which will be left open after the completion of this
55
+ # method.
56
+ #
57
+ # <b>NOTE:</b> No attempt is made to prevent adding multiple entries with
58
+ # the same archive path.
59
+ #
60
+ # See the instance method #archive for more information about _paths_ and
61
+ # _options_.
62
+ def self.archive(archive, paths, options = {})
63
+ if archive.kind_of?(String) && File.exist?(archive) then
64
+ # Update the archive "in place".
65
+ tmp_archive_path = nil
66
+ File.open(archive) do |archive_in|
67
+ Tempfile.open(*File.split(archive_in.path).reverse) do |archive_out|
68
+ # Save off the path so that the temporary file can be renamed to the
69
+ # archive file later.
70
+ tmp_archive_path = archive_out.path
71
+ # Ensure the file is in binary mode for Windows.
72
+ archive_out.binmode
73
+ # Update the archive.
74
+ open(archive_in, :r) do |z_in|
75
+ open(archive_out, :w) do |z_out|
76
+ z_in.each { |entry| z_out << entry }
77
+ z_out.archive(paths, options)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ # Set more reasonable permissions than those set by Tempfile.
83
+ File.chmod(0666 & ~File.umask, tmp_archive_path)
84
+ # Replace the input archive with the output archive.
85
+ File.rename(tmp_archive_path, archive)
86
+ else
87
+ open(archive, :w) { |z| z.archive(paths, options) }
88
+ end
53
89
  end
54
90
 
55
- # A convenience method which opens an archive located in the path indicated
56
- # by _archive_path_, extracts the entries to the path indicated by
57
- # _destination_, and then closes the archive. See the instance method
58
- # #extract for more information about _destination_ and _options_.
59
- def self.extract(archive_path, destination, options = {})
60
- open(archive_path) { |z| z.extract(destination, options) }
91
+ # Extracts the entries from an archive to _destination_.
92
+ #
93
+ # If _archive_ is a String, it is treated as a file path pointing to an
94
+ # existing archive file. Otherwise, it is treated as a seekable and
95
+ # readable IO-like object.
96
+ #
97
+ # See the instance method #extract for more information about _destination_
98
+ # and _options_.
99
+ def self.extract(archive, destination, options = {})
100
+ open(archive, :r) { |z| z.extract(destination, options) }
61
101
  end
62
102
 
63
103
  # Calls #new with the given arguments and yields the resulting Zip instance
@@ -65,8 +105,8 @@ module Archive # :nodoc:
65
105
  # Zip instance is closed.
66
106
  #
67
107
  # This is a synonym for #new if no block is given.
68
- def self.open(archive_path, archive_out = nil)
69
- zf = new(archive_path, archive_out)
108
+ def self.open(archive, mode = :r)
109
+ zf = new(archive, mode)
70
110
  return zf unless block_given?
71
111
 
72
112
  begin
@@ -76,140 +116,121 @@ module Archive # :nodoc:
76
116
  end
77
117
  end
78
118
 
79
- # Open and parse the file located at the path indicated by _archive_path_ if
80
- # _archive_path_ is not +nil+ and the path exists. If _archive_out_ is
81
- # unspecified or +nil+, any changes made will be saved in place, replacing
82
- # the current archive with a new one having the same name. If _archive_out_
83
- # is a String, it points to a file which will recieve the new archive's
84
- # contents. Otherwise, _archive_out_ is assumed to be a writable, IO-like
85
- # object operating in *binary* mode which will recieve the new archive's
86
- # contents.
87
- #
88
- # At least one of _archive_path_ and _archive_out_ must be specified and
89
- # non-nil; otherwise, an error will be raised.
90
- def initialize(archive_path, archive_out = nil)
91
- if (archive_path.nil? || archive_path.empty?) &&
92
- (archive_out.nil? ||
93
- archive_out.kind_of?(String) && archive_out.empty?) then
94
- raise ArgumentError, 'No valid source or destination archive specified'
119
+ # Opens an existing archive and/or creates a new archive.
120
+ #
121
+ # If _archive_ is a String, it will be treated as a file path; otherwise, it
122
+ # is assumed to be an IO-like object with the necessary read or write
123
+ # support depending on the setting of _mode_. IO-like objects are not
124
+ # closed when the archive is closed, but files opened from file paths are.
125
+ # Set _mode_ to <tt>:r</tt> or <tt>"r"</tt> to read the archive, and set it
126
+ # to <tt>:w</tt> or <tt>"w"</tt> to write the archive.
127
+ #
128
+ # <b>NOTE:</b> The #close method must be called in order to save any
129
+ # modifications to the archive. Due to limitations in the Ruby finalization
130
+ # capabilities, the #close method is _not_ automatically called when this
131
+ # object is garbage collected. Make sure to call #close when finished with
132
+ # this object.
133
+ def initialize(archive, mode = :r)
134
+ @archive = archive
135
+ mode = mode.to_sym
136
+ if mode == :r || mode == :w then
137
+ @mode = mode
138
+ else
139
+ raise ArgumentError, "illegal access mode #{mode}"
140
+ end
141
+
142
+ @close_delegate = false
143
+ if @archive.kind_of?(String) then
144
+ @close_delegate = true
145
+ if mode == :r then
146
+ @archive = File.open(@archive, 'rb')
147
+ else
148
+ @archive = File.open(@archive, 'wb')
149
+ end
95
150
  end
96
- @archive_path = archive_path
97
- @archive_out = archive_out
98
- @entries = {}
99
- @dirty = false
151
+ @entries = []
100
152
  @comment = ''
101
153
  @closed = false
102
- if ! @archive_path.nil? && File.exist?(@archive_path) then
103
- @archive_in = File.new(@archive_path, 'rb')
104
- parse(@archive_in)
105
- end
106
154
  end
107
155
 
108
156
  # A comment string for the ZIP archive.
109
157
  attr_accessor :comment
110
158
 
111
- # Close the archive. It is at this point that any changes made to the
112
- # archive will be persisted to an output stream.
159
+ # Closes the archive.
160
+ #
161
+ # Failure to close the archive by calling this method may result in a loss
162
+ # of data for writable archives.
163
+ #
164
+ # <b>NOTE:</b> The underlying stream is only closed if the archive was
165
+ # opened with a String for the _archive_ parameter.
113
166
  #
114
167
  # Raises Archive::Zip::IOError if called more than once.
115
168
  def close
116
169
  raise IOError, 'closed archive' if closed?
117
170
 
118
- if @dirty then
119
- # There is something to write...
120
- if @archive_out.nil? then
121
- # Update the archive "in place".
122
- tmp_archive_path = nil
123
- Tempfile.open(*File.split(@archive_path).reverse) do |archive_out|
124
- # Ensure the file is in binary mode for Windows.
125
- archive_out.binmode
126
- # Save off the path so that the temporary file can be renamed to the
127
- # archive file later.
128
- tmp_archive_path = archive_out.path
129
- dump(archive_out)
130
- end
131
- File.chmod(0666 & ~File.umask, tmp_archive_path)
132
- elsif @archive_out.kind_of?(String) then
133
- # Open a new archive to receive the data.
134
- File.open(@archive_out, 'wb') do |archive_out|
135
- dump(archive_out)
136
- end
137
- else
138
- # Assume the given object is an IO-like object and dump the archive
139
- # contents to it.
140
- dump(@archive_out)
141
- end
142
- @archive_in.close unless @archive_in.nil?
143
- # The rename must happen after the original archive is closed when
144
- # running on Windows since that platform does not allow a file which is
145
- # in use to be replaced as is required when trying to update the archive
146
- # "in place".
147
- File.rename(tmp_archive_path, @archive_path) if @archive_out.nil?
148
- elsif ! @archive_in.nil? then
149
- @archive_in.close
171
+ if writable? then
172
+ # Write the new archive contents.
173
+ dump(@archive)
150
174
  end
151
175
 
152
- closed = true
176
+ # Note that we only close delegate streams which are opened by us so that
177
+ # the user may do so for other delegate streams at his/her discretion.
178
+ @archive.close if @close_delegate
179
+
180
+ @closed = true
153
181
  nil
154
182
  end
155
183
 
156
- # Returns +true+ if the ZIP archive is closed, false otherwise.
184
+ # Returns +true+ if the ZIP archive is closed, +false+ otherwise.
157
185
  def closed?
158
186
  @closed
159
187
  end
160
188
 
161
- # When the ZIP archive is open, this method iterates through each entry in
162
- # turn yielding each one to the given block. Since Zip includes Enumerable,
163
- # Zip instances are enumerables of Entry instances.
189
+ # Returns +true+ if the ZIP archive is readable, +false+ otherwise.
190
+ def readable?
191
+ @mode == :r
192
+ end
193
+
194
+ # Returns +true+ if the ZIP archive is writable, +false+ otherwise.
195
+ def writable?
196
+ @mode == :w
197
+ end
198
+
199
+ # Iterates through each entry of a readable ZIP archive in turn yielding
200
+ # each one to the given block.
164
201
  #
165
- # Raises Archive::Zip::IOError if called after #close.
202
+ # Raises Archive::Zip::IOError if called on a non-readable archive or after
203
+ # the archive is closed.
166
204
  def each(&b)
167
- raise IOError, 'closed archive' if @closed
205
+ raise IOError, 'non-readable archive' unless readable?
206
+ raise IOError, 'closed archive' if closed?
168
207
 
169
- @entries.each_value(&b)
208
+ unless @parse_complete then
209
+ parse(@archive)
210
+ @parse_complete = true
211
+ end
212
+ @entries.each(&b)
170
213
  end
171
214
 
172
- # Add _entry_ into the ZIP archive replacing any existing entry with the
173
- # same zip path.
215
+ # Adds _entry_ into a writable ZIP archive.
216
+ #
217
+ # <b>NOTE:</b> No attempt is made to prevent adding multiple entries with
218
+ # the same archive path.
174
219
  #
175
- # Raises Archive::Zip::IOError if called after #close.
220
+ # Raises Archive::Zip::IOError if called on a non-writable archive or after
221
+ # the archive is closed.
176
222
  def add_entry(entry)
177
- raise IOError, 'closed archive' if @closed
223
+ raise IOError, 'non-writable archive' unless writable?
224
+ raise IOError, 'closed archive' if closed?
178
225
  unless entry.kind_of?(Entry) then
179
226
  raise ArgumentError, 'Archive::Zip::Entry instance required'
180
227
  end
181
228
 
182
- @entries[entry.zip_path] = entry
183
- @dirty = true
229
+ @entries << entry
184
230
  self
185
231
  end
186
232
  alias :<< :add_entry
187
233
 
188
- # Look up an entry based on the zip path located in _zip_path_. Returns
189
- # +nil+ if no entry is found.
190
- def get_entry(zip_path)
191
- @entries[zip_path]
192
- end
193
- alias :[] :get_entry
194
-
195
- # Removes an entry from the ZIP file and returns the entry or +nil+ if no
196
- # entry was found to remove. If _entry_ is an instance of
197
- # Archive::Zip::Entry, the zip_path attribute is used to find the entry to
198
- # remove; otherwise, _entry_ is assumed to be a zip path matching an entry
199
- # in the ZIP archive.
200
- #
201
- # Raises Archive::Zip::IOError if called after #close.
202
- def remove_entry(entry)
203
- raise IOError, 'closed archive' if @closed
204
-
205
- zip_path = entry
206
- zip_path = entry.zip_path if entry.kind_of?(Entry)
207
- entry = @entries.delete(zip_path)
208
- entry = entry[1] unless entry.nil?
209
- @dirty ||= ! entry.nil?
210
- entry
211
- end
212
-
213
234
  # Adds _paths_ to the archive. _paths_ may be either a single path or an
214
235
  # Array of paths. The files and directories referenced by _paths_ are added
215
236
  # using their respective basenames as their zip paths. The exception to
@@ -267,10 +288,13 @@ module Archive # :nodoc:
267
288
  # Any other options which are supported by Archive::Zip::Entry.from_file are
268
289
  # also supported.
269
290
  #
270
- # Raises Archive::Zip::IOError if called after #close. Raises
271
- # Archive::Zip::EntryError if the <b>:on_error</b> option is either unset or
272
- # indicates that the error should be raised and
273
- # Archive::Zip::Entry.from_file raises an error.
291
+ # <b>NOTE:</b> No attempt is made to prevent adding multiple entries with
292
+ # the same archive path.
293
+ #
294
+ # Raises Archive::Zip::IOError if called on a non-writable archive or after
295
+ # the archive is closed. Raises Archive::Zip::EntryError if the
296
+ # <b>:on_error</b> option is either unset or indicates that the error should
297
+ # be raised and Archive::Zip::Entry.from_file raises an error.
274
298
  #
275
299
  # == Example
276
300
  #
@@ -317,7 +341,8 @@ module Archive # :nodoc:
317
341
  # zip-test/dir1/
318
342
  # zip-test/dir2/
319
343
  def archive(paths, options = {})
320
- raise IOError, 'closed archive' if @closed
344
+ raise IOError, 'non-writable archive' unless writable?
345
+ raise IOError, 'closed archive' if closed?
321
346
 
322
347
  # Ensure that paths is an enumerable.
323
348
  paths = [paths] unless paths.kind_of?(Enumerable)
@@ -472,7 +497,8 @@ module Archive # :nodoc:
472
497
  # Any other options which are supported by Archive::Zip::Entry#extract are
473
498
  # also supported.
474
499
  #
475
- # Raises Archive::Zip::IOError if called after #close.
500
+ # Raises Archive::Zip::IOError if called on a non-readable archive or after
501
+ # the archive is closed.
476
502
  #
477
503
  # == Example
478
504
  #
@@ -529,7 +555,8 @@ module Archive # :nodoc:
529
555
  # +- dir1
530
556
  # +- dir2
531
557
  def extract(destination, options = {})
532
- raise IOError, 'closed archive' if @closed
558
+ raise IOError, 'non-readable archive' unless readable?
559
+ raise IOError, 'closed archive' if closed?
533
560
 
534
561
  # Ensure that unspecified options have default values.
535
562
  options[:directories] = true unless options.has_key?(:directories)
@@ -627,23 +654,16 @@ module Archive # :nodoc:
627
654
 
628
655
  private
629
656
 
630
- # <b>NOTE:</b> For now _io_ MUST be seekable and report such by returning
631
- # +true+ from its seekable? method. See IO#seekable?.
632
- #
633
- # Raises Archive::Zip::IOError if _io_ is not seekable.
657
+ # <b>NOTE:</b> For now _io_ MUST be seekable.
634
658
  def parse(io)
635
- # Error out if the IO object is not confirmed seekable.
636
- raise Zip::IOError, 'non-seekable IO object given' unless io.respond_to?(:seekable?) and io.seekable?
637
-
638
659
  socd_pos = find_central_directory(io)
639
660
  io.seek(socd_pos)
640
661
  # Parse each entry in the central directory.
641
662
  loop do
642
- signature = io.readbytes(4)
663
+ signature = IOExtensions.read_exactly(io, 4)
643
664
  break unless signature == CFH_SIGNATURE
644
- add_entry(Zip::Entry.parse(io))
665
+ @entries << Zip::Entry.parse(io)
645
666
  end
646
- @dirty = false
647
667
  # Maybe add support for digital signatures and ZIP64 records... Later
648
668
 
649
669
  nil
@@ -665,9 +685,12 @@ module Archive # :nodoc:
665
685
  eocd_offset = -22
666
686
  loop do
667
687
  io.seek(eocd_offset, IO::SEEK_END)
668
- if io.readbytes(4) == EOCD_SIGNATURE then
688
+ if IOExtensions.read_exactly(io, 4) == EOCD_SIGNATURE then
669
689
  io.seek(16, IO::SEEK_CUR)
670
- break if io.readbytes(2).unpack('v')[0] == (eocd_offset + 22).abs
690
+ if IOExtensions.read_exactly(io, 2).unpack('v')[0] ==
691
+ (eocd_offset + 22).abs then
692
+ break
693
+ end
671
694
  end
672
695
  eocd_offset -= 1
673
696
  end
@@ -676,7 +699,7 @@ module Archive # :nodoc:
676
699
  # Now, jump into the location in the record which contains a pointer to
677
700
  # the start of the central directory record and return the value.
678
701
  io.seek(eocd_offset + 16, IO::SEEK_END)
679
- return io.readbytes(4).unpack('V')[0]
702
+ return IOExtensions.read_exactly(io, 4).unpack('V')[0]
680
703
  rescue Errno::EINVAL
681
704
  raise Zip::UnzipError, 'unable to locate end-of-central-directory record'
682
705
  end
@@ -686,12 +709,11 @@ module Archive # :nodoc:
686
709
  # bytes written.
687
710
  def dump(io)
688
711
  bytes_written = 0
689
- entries = @entries.values
690
- entries.each do |entry|
712
+ @entries.each do |entry|
691
713
  bytes_written += entry.dump_local_file_record(io, bytes_written)
692
714
  end
693
715
  central_directory_offset = bytes_written
694
- entries.each do |entry|
716
+ @entries.each do |entry|
695
717
  bytes_written += entry.dump_central_file_record(io)
696
718
  end
697
719
  central_directory_length = bytes_written - central_directory_offset
@@ -700,8 +722,8 @@ module Archive # :nodoc:
700
722
  [
701
723
  0,
702
724
  0,
703
- entries.length,
704
- entries.length,
725
+ @entries.length,
726
+ @entries.length,
705
727
  central_directory_length,
706
728
  central_directory_offset,
707
729
  comment.length
@@ -1,3 +1,5 @@
1
+ # encoding: UTF-8
2
+
1
3
  module Archive; class Zip
2
4
  # Archive::Zip::Codec is a factory class for generating codec object instances
3
5
  # based on the compression method and general purpose flag fields of ZIP
@@ -1,3 +1,5 @@
1
+ # encoding: UTF-8
2
+
1
3
  require 'archive/support/zlib'
2
4
  require 'archive/zip/codec'
3
5
  require 'archive/zip/data_descriptor'
@@ -40,8 +42,17 @@ module Archive; class Zip; module Codec
40
42
  # compression to be applied to the data stream.
41
43
  def initialize(io, compression_level)
42
44
  super(io, compression_level, -Zlib::MAX_WBITS)
45
+ @crc32 = 0
43
46
  end
44
47
 
48
+ # The CRC32 checksum of the uncompressed data written using this object.
49
+ #
50
+ # <b>NOTE:</b> Anything still in the internal write buffer has not been
51
+ # processed, so calling #flush prior to examining this attribute may be
52
+ # necessary for an accurate computation.
53
+ attr_reader :crc32
54
+ alias :checksum :crc32
55
+
45
56
  # Closes this object so that further write operations will fail. If
46
57
  # _close_delegate_ is +true+, the delegate object used as a data sink will
47
58
  # also be closed using its close method.
@@ -50,15 +61,29 @@ module Archive; class Zip; module Codec
50
61
  delegate.close if close_delegate
51
62
  end
52
63
 
53
- # Returns an instance of Archive::Zip::Entry::DataDescriptor with
54
- # information regarding the data which has passed through this object to
55
- # the delegate object. The close or flush methods should be called before
56
- # using this method in order to ensure that any possibly buffered data is
57
- # flushed to the delegate object; otherwise, the contents of the data
58
- # descriptor may be inaccurate.
64
+ # Returns an instance of Archive::Zip::DataDescriptor with information
65
+ # regarding the data which has passed through this object to the delegate
66
+ # object. The close or flush methods should be called before using this
67
+ # method in order to ensure that any possibly buffered data is flushed to
68
+ # the delegate object; otherwise, the contents of the data descriptor may
69
+ # be inaccurate.
59
70
  def data_descriptor
60
71
  DataDescriptor.new(crc32, compressed_size, uncompressed_size)
61
72
  end
73
+
74
+ private
75
+
76
+ def unbuffered_seek(offset, whence = IO::SEEK_SET)
77
+ result = super(offset, whence)
78
+ @crc32 = 0 if whence == IO::SEEK_SET
79
+ result
80
+ end
81
+
82
+ def unbuffered_write(string)
83
+ result = super(string)
84
+ @crc32 = Zlib.crc32(string, @crc32)
85
+ result
86
+ end
62
87
  end
63
88
 
64
89
  # Archive::Zip::Codec::Deflate::Decompress extends Zlib::ZReader in order to
@@ -92,8 +117,17 @@ module Archive; class Zip; module Codec
92
117
  # method, this class' _rewind_ method will be enabled.
93
118
  def initialize(io)
94
119
  super(io, -Zlib::MAX_WBITS)
120
+ @crc32 = 0
95
121
  end
96
122
 
123
+ # The CRC32 checksum of the uncompressed data read using this object.
124
+ #
125
+ # <b>NOTE:</b> The contents of the internal read buffer are immediately
126
+ # processed any time the internal buffer is filled, so this checksum is
127
+ # only accurate if all data has been read out of this object.
128
+ attr_reader :crc32
129
+ alias :checksum :crc32
130
+
97
131
  # Closes this object so that further read operations will fail. If
98
132
  # _close_delegate_ is +true+, the delegate object used as a data source
99
133
  # will also be closed using its close method.
@@ -102,14 +136,28 @@ module Archive; class Zip; module Codec
102
136
  delegate.close if close_delegate
103
137
  end
104
138
 
105
- # Returns an instance of Archive::Zip::Entry::DataDescriptor with
106
- # information regarding the data which has passed through this object
107
- # from the delegate object. It is recommended to call the close method
108
- # before calling this in order to ensure that no further read operations
109
- # change the state of this object.
139
+ # Returns an instance of Archive::Zip::DataDescriptor with information
140
+ # regarding the data which has passed through this object from the
141
+ # delegate object. It is recommended to call the close method before
142
+ # calling this in order to ensure that no further read operations change
143
+ # the state of this object.
110
144
  def data_descriptor
111
145
  DataDescriptor.new(crc32, compressed_size, uncompressed_size)
112
146
  end
147
+
148
+ private
149
+
150
+ def unbuffered_read(length)
151
+ result = super(length)
152
+ @crc32 = Zlib.crc32(result, @crc32)
153
+ result
154
+ end
155
+
156
+ def unbuffered_seek(offset, whence = IO::SEEK_SET)
157
+ result = super(offset, whence)
158
+ @crc32 = 0 if whence == IO::SEEK_SET
159
+ result
160
+ end
113
161
  end
114
162
 
115
163
  # The numeric identifier assigned to this compression codec by the ZIP