archive-zip 0.3.0 → 0.4.0

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