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
@@ -1,3 +1,5 @@
1
+ # encoding: UTF-8
2
+
1
3
  require 'archive/support/io-like'
2
4
  require 'archive/zip/codec'
3
5
 
@@ -36,6 +38,10 @@ module Archive; class Zip; module Codec
36
38
  # assumption that _io_ is already buffered.
37
39
  def initialize(io)
38
40
  @io = io
41
+
42
+ # Keep track of the total number of bytes written.
43
+ @total_bytes_in = 0
44
+
39
45
  # Assume that the delegate IO object is already buffered.
40
46
  self.flush_size = 0
41
47
  end
@@ -50,9 +56,34 @@ module Archive; class Zip; module Codec
50
56
 
51
57
  private
52
58
 
59
+ # Allows resetting this object and the delegate object back to the
60
+ # beginning of the stream or reporting the current position in the stream.
61
+ #
62
+ # Raises Errno::EINVAL unless _offset_ is <tt>0</tt> and _whence_ is
63
+ # either IO::SEEK_SET or IO::SEEK_CUR. Raises Errno::EINVAL if _whence_
64
+ # is IO::SEEK_SEK and the delegate object does not respond to the _rewind_
65
+ # method.
66
+ def unbuffered_seek(offset, whence = IO::SEEK_SET)
67
+ unless offset == 0 &&
68
+ ((whence == IO::SEEK_SET && @io.respond_to?(:rewind)) ||
69
+ whence == IO::SEEK_CUR) then
70
+ raise Errno::EINVAL
71
+ end
72
+
73
+ case whence
74
+ when IO::SEEK_SET
75
+ @io.rewind
76
+ @total_bytes_in = 0
77
+ when IO::SEEK_CUR
78
+ @total_bytes_in
79
+ end
80
+ end
81
+
53
82
  # Writes _string_ to the delegate IO object and returns the result.
54
83
  def unbuffered_write(string)
55
- @io.write(string)
84
+ bytes_written = @io.write(string)
85
+ @total_bytes_in += bytes_written
86
+ bytes_written
56
87
  end
57
88
  end
58
89
 
@@ -80,21 +111,41 @@ module Archive; class Zip; module Codec
80
111
  end
81
112
 
82
113
  # Creates a new instance of this class using _io_ as a data source. _io_
83
- # must be readable and provide a read method as IO does or errors will be
84
- # raised when performing read operations. If _io_ provides a rewind
85
- # method, this class' rewind method will be enabled.
114
+ # must be readable and provide a _read_ method as an IO instance would or
115
+ # errors will be raised when performing read operations.
116
+ #
117
+ # This class has extremely limited seek capabilities. It is possible to
118
+ # seek with an offset of <tt>0</tt> and a whence of <tt>IO::SEEK_CUR</tt>.
119
+ # As a result, the _pos_ and _tell_ methods also work as expected.
120
+ #
121
+ # Due to certain optimizations within IO::Like#seek and if there is data
122
+ # in the read buffer, the _seek_ method can be used to seek forward from
123
+ # the current stream position up to the end of the buffer. Unless it is
124
+ # known definitively how much data is in the buffer, it is best to avoid
125
+ # relying on this behavior.
126
+ #
127
+ # If _io_ also responds to _rewind_, then the _rewind_ method of this
128
+ # class can be used to reset the whole stream back to the beginning. Using
129
+ # _seek_ of this class to seek directly to offset <tt>0</tt> using
130
+ # <tt>IO::SEEK_SET</tt> for whence will also work in this case.
131
+ #
132
+ # Any other seeking attempts, will raise Errno::EINVAL exceptions.
86
133
  #
87
134
  # The _fill_size_ attribute is set to <tt>0</tt> by default under the
88
135
  # assumption that _io_ is already buffered.
89
136
  def initialize(io)
90
137
  @io = io
138
+
139
+ # Keep track of the total number of bytes read.
140
+ @total_bytes_out = 0
141
+
91
142
  # Assume that the delegate IO object is already buffered.
92
143
  self.fill_size = 0
93
144
  end
94
145
 
95
146
  # Closes this object so that further write operations will fail. If
96
- # _close_delegate_ is +true+, the delegate object used as a data sink will
97
- # also be closed using its close method.
147
+ # _close_delegate_ is +true+, the delegate object used as a data source
148
+ # will also be closed using its close method.
98
149
  def close(close_delegate = true)
99
150
  super()
100
151
  @io.close if close_delegate
@@ -108,22 +159,32 @@ module Archive; class Zip; module Codec
108
159
  def unbuffered_read(length)
109
160
  buffer = @io.read(length)
110
161
  raise EOFError, 'end of file reached' if buffer.nil?
162
+ @total_bytes_out += buffer.length
111
163
 
112
164
  buffer
113
165
  end
114
166
 
115
167
  # Allows resetting this object and the delegate object back to the
116
- # beginning of the stream. _offset_ must be <tt>0</tt> and _whence_ must
117
- # be IO::SEEK_SET or an error will be raised. The delegate object must
118
- # respond to the _rewind_ method or an error will be raised.
168
+ # beginning of the stream or reporting the current position in the stream.
169
+ #
170
+ # Raises Errno::EINVAL unless _offset_ is <tt>0</tt> and _whence_ is
171
+ # either IO::SEEK_SET or IO::SEEK_CUR. Raises Errno::EINVAL if _whence_
172
+ # is IO::SEEK_SEK and the delegate object does not respond to the _rewind_
173
+ # method.
119
174
  def unbuffered_seek(offset, whence = IO::SEEK_SET)
120
- unless offset == 0 && whence == IO::SEEK_SET then
121
- raise Errno::EINVAL, 'Invalid argument'
175
+ unless offset == 0 &&
176
+ ((whence == IO::SEEK_SET && @io.respond_to?(:rewind)) ||
177
+ whence == IO::SEEK_CUR) then
178
+ raise Errno::EINVAL
122
179
  end
123
- unless @io.respond_to?(:rewind) then
124
- raise Errno::ESPIPE, 'Illegal seek'
180
+
181
+ case whence
182
+ when IO::SEEK_SET
183
+ @io.rewind
184
+ @total_bytes_out = 0
185
+ when IO::SEEK_CUR
186
+ @total_bytes_out
125
187
  end
126
- @io.rewind
127
188
  end
128
189
  end
129
190
 
@@ -1,4 +1,7 @@
1
+ # encoding: UTF-8
2
+
1
3
  require 'archive/support/io-like'
4
+ require 'archive/support/zlib'
2
5
  require 'archive/zip/codec'
3
6
  require 'archive/zip/data_descriptor'
4
7
 
@@ -58,12 +61,12 @@ module Archive; class Zip; module Codec
58
61
  nil
59
62
  end
60
63
 
61
- # Returns an instance of Archive::Zip::Entry::DataDescriptor with
62
- # information regarding the data which has passed through this object to
63
- # the delegate object. The close or flush methods should be called before
64
- # using this method in order to ensure that any possibly buffered data is
65
- # flushed to the delegate object; otherwise, the contents of the data
66
- # 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.
67
70
  def data_descriptor
68
71
  DataDescriptor.new(
69
72
  @crc32,
@@ -74,6 +77,30 @@ module Archive; class Zip; module Codec
74
77
 
75
78
  private
76
79
 
80
+ # Allows resetting this object and the delegate object back to the
81
+ # beginning of the stream or reporting the current position in the stream.
82
+ #
83
+ # Raises Errno::EINVAL unless _offset_ is <tt>0</tt> and _whence_ is
84
+ # either IO::SEEK_SET or IO::SEEK_CUR. Raises Errno::EINVAL if _whence_
85
+ # is IO::SEEK_SEK and the delegate object does not respond to the _rewind_
86
+ # method.
87
+ def unbuffered_seek(offset, whence = IO::SEEK_SET)
88
+ unless offset == 0 &&
89
+ ((whence == IO::SEEK_SET && @io.respond_to?(:rewind)) ||
90
+ whence == IO::SEEK_CUR) then
91
+ raise Errno::EINVAL
92
+ end
93
+
94
+ case whence
95
+ when IO::SEEK_SET
96
+ @io.rewind
97
+ @crc32 = 0
98
+ @uncompressed_size = 0
99
+ when IO::SEEK_CUR
100
+ @uncompressed_size
101
+ end
102
+ end
103
+
77
104
  # Writes _string_ to the delegate object and returns the number of bytes
78
105
  # actually written. Updates the uncompressed_size and crc32 attributes as
79
106
  # a side effect.
@@ -115,9 +142,25 @@ module Archive; class Zip; module Codec
115
142
  end
116
143
 
117
144
  # Creates a new instance of this class using _io_ as a data source. _io_
118
- # must be readable and provide a read method as IO does or errors will be
119
- # raised when performing read operations. If _io_ provides a rewind
120
- # method, this class' rewind method will be enabled.
145
+ # must be readable and provide a _read_ method as an IO instance would or
146
+ # errors will be raised when performing read operations.
147
+ #
148
+ # This class has extremely limited seek capabilities. It is possible to
149
+ # seek with an offset of <tt>0</tt> and a whence of <tt>IO::SEEK_CUR</tt>.
150
+ # As a result, the _pos_ and _tell_ methods also work as expected.
151
+ #
152
+ # Due to certain optimizations within IO::Like#seek and if there is data
153
+ # in the read buffer, the _seek_ method can be used to seek forward from
154
+ # the current stream position up to the end of the buffer. Unless it is
155
+ # known definitively how much data is in the buffer, it is best to avoid
156
+ # relying on this behavior.
157
+ #
158
+ # If _io_ also responds to _rewind_, then the _rewind_ method of this
159
+ # class can be used to reset the whole stream back to the beginning. Using
160
+ # _seek_ of this class to seek directly to offset <tt>0</tt> using
161
+ # <tt>IO::SEEK_SET</tt> for whence will also work in this case.
162
+ #
163
+ # Any other seeking attempts, will raise Errno::EINVAL exceptions.
121
164
  #
122
165
  # The _fill_size_ attribute is set to <tt>0</tt> by default under the
123
166
  # assumption that _io_ is already buffered.
@@ -138,11 +181,11 @@ module Archive; class Zip; module Codec
138
181
  nil
139
182
  end
140
183
 
141
- # Returns an instance of Archive::Zip::Entry::DataDescriptor with
142
- # information regarding the data which has passed through this object
143
- # from the delegate object. It is recommended to call the close method
144
- # before calling this in order to ensure that no further read operations
145
- # change the state of this object.
184
+ # Returns an instance of Archive::Zip::DataDescriptor with information
185
+ # regarding the data which has passed through this object from the
186
+ # delegate object. It is recommended to call the close method before
187
+ # calling this in order to ensure that no further read operations change
188
+ # the state of this object.
146
189
  def data_descriptor
147
190
  DataDescriptor.new(
148
191
  @crc32,
@@ -165,21 +208,27 @@ module Archive; class Zip; module Codec
165
208
  end
166
209
 
167
210
  # Allows resetting this object and the delegate object back to the
168
- # beginning of the stream. _offset_ must be <tt>0</tt> and _whence_ must
169
- # be IO::SEEK_SET or an error will be raised. The delegate object must
170
- # respond to the _rewind_ method or an error will be raised. The
171
- # uncompressed_size and crc32 attributes are reinitialized as a side
172
- # effect.
211
+ # beginning of the stream or reporting the current position in the stream.
212
+ #
213
+ # Raises Errno::EINVAL unless _offset_ is <tt>0</tt> and _whence_ is
214
+ # either IO::SEEK_SET or IO::SEEK_CUR. Raises Errno::EINVAL if _whence_
215
+ # is IO::SEEK_SEK and the delegate object does not respond to the _rewind_
216
+ # method.
173
217
  def unbuffered_seek(offset, whence = IO::SEEK_SET)
174
- unless offset == 0 && whence == IO::SEEK_SET then
175
- raise Errno::EINVAL, 'Invalid argument'
218
+ unless offset == 0 &&
219
+ ((whence == IO::SEEK_SET && @io.respond_to?(:rewind)) ||
220
+ whence == IO::SEEK_CUR) then
221
+ raise Errno::EINVAL
176
222
  end
177
- unless @io.respond_to?(:rewind) then
178
- raise Errno::ESPIPE, 'Illegal seek'
223
+
224
+ case whence
225
+ when IO::SEEK_SET
226
+ @io.rewind
227
+ @crc32 = 0
228
+ @uncompressed_size = 0
229
+ when IO::SEEK_CUR
230
+ @uncompressed_size
179
231
  end
180
- @io.rewind
181
- @crc32 = 0
182
- @uncompressed_size = 0
183
232
  end
184
233
  end
185
234
 
@@ -1,3 +1,6 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'archive/support/integer'
1
4
  require 'archive/support/io-like'
2
5
  require 'archive/support/time'
3
6
  require 'archive/support/zlib'
@@ -108,18 +111,40 @@ module Archive; class Zip; module Codec
108
111
  # The _flush_size_ attribute is set to <tt>0</tt> by default under the
109
112
  # assumption that _io_ is already buffered.
110
113
  def initialize(io, password, mtime)
114
+ # Keep track of the total number of bytes written.
115
+ # Set this here so that the call to #initialize_keys caused by the call
116
+ # to super below does not cause errors in #unbuffered_write due to this
117
+ # attribute being uninitialized.
118
+ @total_bytes_in = 0
119
+
120
+ # This buffer is used to hold the encrypted version of the string most
121
+ # recently sent to #unbuffered_write.
122
+ @encrypt_buffer = ''
123
+
111
124
  super(io, password, mtime)
112
125
 
113
126
  # Assume that the delegate IO object is already buffered.
114
127
  self.flush_size = 0
115
128
  end
116
129
 
117
- # Closes this object so that further write operations will fail. If
118
- # _close_delegate_ is +true+, the delegate object used as a data sink will
119
- # also be closed using its close method.
130
+ # Closes the stream after flushing the encryption buffer to the delegate.
131
+ # If _close_delegate_ is +true+, the delegate object used as a data sink
132
+ # will also be closed using its close method.
133
+ #
134
+ # Raises IOError if called more than once.
120
135
  def close(close_delegate = true)
136
+ flush()
137
+ begin
138
+ until @encrypt_buffer.empty? do
139
+ @encrypt_buffer.slice!(0, io.write(@encrypt_buffer))
140
+ end
141
+ rescue Errno::EAGAIN, Errno::EINTR
142
+ retry if write_ready?
143
+ end
144
+
121
145
  super()
122
146
  io.close if close_delegate
147
+ nil
123
148
  end
124
149
 
125
150
  private
@@ -131,31 +156,74 @@ module Archive; class Zip; module Codec
131
156
  super
132
157
 
133
158
  # Create and encrypt a 12 byte header to protect the encrypted file data
134
- # from attack. The first 10 bytes are random, and the lat 2 bytes are
159
+ # from attack. The first 10 bytes are random, and the last 2 bytes are
135
160
  # the low order word of the last modified time of the entry in DOS
136
161
  # format.
162
+ header = ''
137
163
  10.times do
138
- unbuffered_write(rand(256).chr)
164
+ header << rand(256).chr
139
165
  end
140
166
  time = mtime.to_dos_time.to_i
141
- unbuffered_write((time & 0xff).chr)
142
- unbuffered_write(((time >> 8) & 0xff).chr)
167
+ header << (time & 0xff).chr << ((time >> 8) & 0xff).chr
168
+
169
+ # Take care to ensure that all bytes in the header are written.
170
+ while header.size > 0 do
171
+ begin
172
+ header.slice!(0, unbuffered_write(header))
173
+ rescue Errno::EAGAIN, Errno::EINTR
174
+ sleep(1)
175
+ end
176
+ end
177
+
178
+ # Reset the total bytes written in order to disregard the header.
179
+ @total_bytes_in = 0
180
+
143
181
  nil
144
182
  end
145
183
 
184
+ # Allows resetting this object and the delegate object back to the
185
+ # beginning of the stream or reporting the current position in the stream.
186
+ #
187
+ # Raises Errno::EINVAL unless _offset_ is <tt>0</tt> and _whence_ is
188
+ # either IO::SEEK_SET or IO::SEEK_CUR. Raises Errno::EINVAL if _whence_
189
+ # is IO::SEEK_SEK and the delegate object does not respond to the _rewind_
190
+ # method.
191
+ def unbuffered_seek(offset, whence = IO::SEEK_SET)
192
+ unless offset == 0 &&
193
+ ((whence == IO::SEEK_SET && @io.respond_to?(:rewind)) ||
194
+ whence == IO::SEEK_CUR) then
195
+ raise Errno::EINVAL
196
+ end
197
+
198
+ case whence
199
+ when IO::SEEK_SET
200
+ io.rewind
201
+ @encrypt_buffer = ''
202
+ initialize_keys
203
+ @total_bytes_in = 0
204
+ when IO::SEEK_CUR
205
+ @total_bytes_in
206
+ end
207
+ end
208
+
146
209
  # Encrypts and writes _string_ to the delegate IO object. Returns the
147
- # number of bytes of _string_ written. If _string_ is not a String, it is
148
- # converted into one using its _to_s_ method.
210
+ # number of bytes of _string_ written.
149
211
  def unbuffered_write(string)
150
- string = string.to_s
151
- bytes_written = 0
212
+ # First try to write out the contents of the encrypt buffer because if
213
+ # that raises a failure we can let that pass up the call stack without
214
+ # having polluted the encryption state.
215
+ until @encrypt_buffer.empty? do
216
+ @encrypt_buffer.slice!(0, io.write(@encrypt_buffer))
217
+ end
218
+ # At this point we can encrypt the given string into a new buffer and
219
+ # behave as if it was written.
152
220
  string.each_byte do |byte|
153
221
  temp = decrypt_byte
154
- break unless io.write((byte ^ temp).chr) > 0
155
- bytes_written += 1
222
+ @encrypt_buffer << (byte ^ temp).chr
156
223
  update_keys(byte.chr)
157
224
  end
158
- bytes_written
225
+ @total_bytes_in += string.length
226
+ string.length
159
227
  end
160
228
  end
161
229
 
@@ -187,15 +255,37 @@ module Archive; class Zip; module Codec
187
255
  end
188
256
 
189
257
  # Creates a new instance of this class using _io_ as a data source. _io_
190
- # must be readable and provide a read method as IO does or errors will be
191
- # raised when performing read operations. If _io_ provides a rewind
192
- # method, this class' rewind method will be enabled. _password_ should be
193
- # the encryption key. _mtime_ must be the last modified time of the entry
194
- # to be encrypted/decrypted.
258
+ # must be readable and provide a _read_ method as an IO instance would or
259
+ # errors will be raised when performing read operations. _password_
260
+ # should be the encryption key. _mtime_ must be the last modified time of
261
+ # the entry to be encrypted/decrypted.
262
+ #
263
+ # This class has extremely limited seek capabilities. It is possible to
264
+ # seek with an offset of <tt>0</tt> and a whence of <tt>IO::SEEK_CUR</tt>.
265
+ # As a result, the _pos_ and _tell_ methods also work as expected.
266
+ #
267
+ # Due to certain optimizations within IO::Like#seek and if there is data
268
+ # in the read buffer, the _seek_ method can be used to seek forward from
269
+ # the current stream position up to the end of the buffer. Unless it is
270
+ # known definitively how much data is in the buffer, it is best to avoid
271
+ # relying on this behavior.
272
+ #
273
+ # If _io_ also responds to _rewind_, then the _rewind_ method of this
274
+ # class can be used to reset the whole stream back to the beginning. Using
275
+ # _seek_ of this class to seek directly to offset <tt>0</tt> using
276
+ # <tt>IO::SEEK_SET</tt> for whence will also work in this case.
277
+ #
278
+ # Any other seeking attempts, will raise Errno::EINVAL exceptions.
195
279
  #
196
280
  # The _fill_size_ attribute is set to <tt>0</tt> by default under the
197
281
  # assumption that _io_ is already buffered.
198
282
  def initialize(io, password, mtime)
283
+ # Keep track of the total number of bytes read.
284
+ # Set this here so that the call to #initialize_keys caused by the call
285
+ # to super below does not cause errors in #unbuffered_read due to this
286
+ # attribute being uninitialized.
287
+ @total_bytes_out = 0
288
+
199
289
  super(io, password, mtime)
200
290
 
201
291
  # Assume that the delegate IO object is already buffered.
@@ -203,8 +293,8 @@ module Archive; class Zip; module Codec
203
293
  end
204
294
 
205
295
  # Closes this object so that further write operations will fail. If
206
- # _close_delegate_ is +true+, the delegate object used as a data sink will
207
- # also be closed using its close method.
296
+ # _close_delegate_ is +true+, the delegate object used as a data source
297
+ # will also be closed using its close method.
208
298
  def close(close_delegate = true)
209
299
  super()
210
300
  io.close if close_delegate
@@ -218,8 +308,20 @@ module Archive; class Zip; module Codec
218
308
  def initialize_keys
219
309
  super
220
310
 
221
- # Decrypt the 12 byte header.
222
- unbuffered_read(12)
311
+ # Load the 12 byte header taking care to ensure that all bytes are read.
312
+ bytes_needed = 12
313
+ while bytes_needed > 0 do
314
+ begin
315
+ bytes_read = unbuffered_read(bytes_needed)
316
+ bytes_needed -= bytes_read.size
317
+ rescue Errno::EAGAIN, Errno::EINTR
318
+ sleep(1)
319
+ end
320
+ end
321
+
322
+ # Reset the total bytes read in order to disregard the header.
323
+ @total_bytes_out = 0
324
+
223
325
  nil
224
326
  end
225
327
 
@@ -230,28 +332,37 @@ module Archive; class Zip; module Codec
230
332
  def unbuffered_read(length)
231
333
  buffer = io.read(length)
232
334
  raise EOFError, 'end of file reached' if buffer.nil?
335
+ @total_bytes_out += buffer.length
233
336
 
234
- (0 ... buffer.size).each do |i|
235
- buffer[i] = (buffer[i] ^ decrypt_byte)
337
+ 0.upto(buffer.length - 1) do |i|
338
+ buffer[i] = (buffer[i].ord ^ decrypt_byte).chr
236
339
  update_keys(buffer[i].chr)
237
340
  end
238
341
  buffer
239
342
  end
240
343
 
241
344
  # Allows resetting this object and the delegate object back to the
242
- # beginning of the stream. _offset_ must be <tt>0</tt> and _whence_ must
243
- # be IO::SEEK_SET or an error will be raised. The delegate object must
244
- # respond to the _rewind_ method or an error will be raised.
345
+ # beginning of the stream or reporting the current position in the stream.
346
+ #
347
+ # Raises Errno::EINVAL unless _offset_ is <tt>0</tt> and _whence_ is
348
+ # either IO::SEEK_SET or IO::SEEK_CUR. Raises Errno::EINVAL if _whence_
349
+ # is IO::SEEK_SEK and the delegate object does not respond to the _rewind_
350
+ # method.
245
351
  def unbuffered_seek(offset, whence = IO::SEEK_SET)
246
- unless offset == 0 && whence == IO::SEEK_SET then
247
- raise Errno::EINVAL, 'Invalid argument'
352
+ unless offset == 0 &&
353
+ ((whence == IO::SEEK_SET && @io.respond_to?(:rewind)) ||
354
+ whence == IO::SEEK_CUR) then
355
+ raise Errno::EINVAL
248
356
  end
249
- unless io.respond_to?(:rewind) then
250
- raise Errno::ESPIPE, 'Illegal seek'
357
+
358
+ case whence
359
+ when IO::SEEK_SET
360
+ io.rewind
361
+ initialize_keys
362
+ @total_bytes_out = 0
363
+ when IO::SEEK_CUR
364
+ @total_bytes_out
251
365
  end
252
- io.rewind
253
- initialize_keys
254
- 0
255
366
  end
256
367
  end
257
368