ziperu 0.1.1
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/Cargo.lock +879 -0
- data/Cargo.toml +8 -0
- data/LICENSE-rubyzip.txt +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +276 -0
- data/ext/ziperu/Cargo.toml +35 -0
- data/ext/ziperu/build.rs +4 -0
- data/ext/ziperu/extconf.rb +6 -0
- data/ext/ziperu/src/archive.rs +713 -0
- data/ext/ziperu/src/errors.rs +143 -0
- data/ext/ziperu/src/extract.rs +505 -0
- data/ext/ziperu/src/gvl.rs +64 -0
- data/ext/ziperu/src/lib.rs +78 -0
- data/ext/ziperu/src/reader.rs +351 -0
- data/ext/ziperu/src/source.rs +296 -0
- data/ext/ziperu/src/writer.rs +916 -0
- data/ext/ziperu/src/zipcrypto.rs +126 -0
- data/lib/rubyzip.rb +4 -0
- data/lib/zip/central_directory.rb +5 -0
- data/lib/zip/compressor.rb +5 -0
- data/lib/zip/constants.rb +5 -0
- data/lib/zip/crypto/aes_encryption.rb +5 -0
- data/lib/zip/crypto/decrypted_io.rb +5 -0
- data/lib/zip/crypto/encryption.rb +5 -0
- data/lib/zip/crypto/null_encryption.rb +5 -0
- data/lib/zip/crypto/traditional_encryption.rb +5 -0
- data/lib/zip/crypto.rb +5 -0
- data/lib/zip/decompressor.rb +5 -0
- data/lib/zip/deflater.rb +5 -0
- data/lib/zip/dirtyable.rb +5 -0
- data/lib/zip/dos_time.rb +5 -0
- data/lib/zip/entry.rb +5 -0
- data/lib/zip/entry_set.rb +5 -0
- data/lib/zip/errors.rb +5 -0
- data/lib/zip/extra_field/aes.rb +5 -0
- data/lib/zip/extra_field/generic.rb +5 -0
- data/lib/zip/extra_field/ntfs.rb +5 -0
- data/lib/zip/extra_field/old_unix.rb +5 -0
- data/lib/zip/extra_field/universal_time.rb +5 -0
- data/lib/zip/extra_field/unix.rb +5 -0
- data/lib/zip/extra_field/unknown.rb +5 -0
- data/lib/zip/extra_field/zip64.rb +5 -0
- data/lib/zip/extra_field.rb +5 -0
- data/lib/zip/file.rb +5 -0
- data/lib/zip/file_split.rb +5 -0
- data/lib/zip/filesystem/dir.rb +5 -0
- data/lib/zip/filesystem/directory_iterator.rb +5 -0
- data/lib/zip/filesystem/file.rb +5 -0
- data/lib/zip/filesystem/file_stat.rb +5 -0
- data/lib/zip/filesystem/zip_file_name_mapper.rb +5 -0
- data/lib/zip/filesystem.rb +5 -0
- data/lib/zip/inflater.rb +5 -0
- data/lib/zip/input_stream.rb +5 -0
- data/lib/zip/ioextras/abstract_input_stream.rb +5 -0
- data/lib/zip/ioextras/abstract_output_stream.rb +5 -0
- data/lib/zip/ioextras/fake_io.rb +5 -0
- data/lib/zip/ioextras.rb +5 -0
- data/lib/zip/null_compressor.rb +5 -0
- data/lib/zip/null_decompressor.rb +5 -0
- data/lib/zip/null_input_stream.rb +5 -0
- data/lib/zip/output_stream.rb +5 -0
- data/lib/zip/pass_thru_compressor.rb +5 -0
- data/lib/zip/pass_thru_decompressor.rb +5 -0
- data/lib/zip/streamable_directory.rb +5 -0
- data/lib/zip/streamable_stream.rb +5 -0
- data/lib/zip/version.rb +5 -0
- data/lib/zip.rb +19 -0
- data/lib/ziperu/central_directory.rb +136 -0
- data/lib/ziperu/compressor.rb +14 -0
- data/lib/ziperu/constants.rb +148 -0
- data/lib/ziperu/crypto.rb +72 -0
- data/lib/ziperu/decompressor.rb +35 -0
- data/lib/ziperu/deflater.rb +41 -0
- data/lib/ziperu/dirtyable.rb +48 -0
- data/lib/ziperu/dos_time.rb +87 -0
- data/lib/ziperu/entry.rb +760 -0
- data/lib/ziperu/entry_set.rb +147 -0
- data/lib/ziperu/errors.rb +154 -0
- data/lib/ziperu/extra_field/aes.rb +52 -0
- data/lib/ziperu/extra_field/generic.rb +45 -0
- data/lib/ziperu/extra_field/ntfs.rb +98 -0
- data/lib/ziperu/extra_field/old_unix.rb +52 -0
- data/lib/ziperu/extra_field/universal_time.rb +81 -0
- data/lib/ziperu/extra_field/unix.rb +45 -0
- data/lib/ziperu/extra_field/unknown.rb +37 -0
- data/lib/ziperu/extra_field/zip64.rb +86 -0
- data/lib/ziperu/extra_field.rb +109 -0
- data/lib/ziperu/file.rb +614 -0
- data/lib/ziperu/file_split.rb +93 -0
- data/lib/ziperu/filesystem/dir.rb +88 -0
- data/lib/ziperu/filesystem/directory_iterator.rb +50 -0
- data/lib/ziperu/filesystem/file.rb +265 -0
- data/lib/ziperu/filesystem/file_stat.rb +112 -0
- data/lib/ziperu/filesystem/zip_file_name_mapper.rb +83 -0
- data/lib/ziperu/filesystem.rb +76 -0
- data/lib/ziperu/inflater.rb +60 -0
- data/lib/ziperu/input_stream.rb +296 -0
- data/lib/ziperu/ioextras/abstract_input_stream.rb +137 -0
- data/lib/ziperu/ioextras/abstract_output_stream.rb +57 -0
- data/lib/ziperu/ioextras/fake_io.rb +41 -0
- data/lib/ziperu/ioextras.rb +42 -0
- data/lib/ziperu/null_compressor.rb +20 -0
- data/lib/ziperu/null_decompressor.rb +25 -0
- data/lib/ziperu/null_input_stream.rb +55 -0
- data/lib/ziperu/output_stream.rb +223 -0
- data/lib/ziperu/pass_thru_compressor.rb +29 -0
- data/lib/ziperu/pass_thru_decompressor.rb +37 -0
- data/lib/ziperu/streamable_directory.rb +15 -0
- data/lib/ziperu/streamable_stream.rb +59 -0
- data/lib/ziperu/version.rb +7 -0
- data/lib/ziperu.rb +116 -0
- metadata +174 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
//! ZipCrypto ("traditional" PKWARE) encryption, for writing.
|
|
2
|
+
//!
|
|
3
|
+
//! The crate decrypts this cipher but keeps its encrypter private, on the
|
|
4
|
+
//! grounds that it is broken (it is: a few bytes of known plaintext
|
|
5
|
+
//! recover the keys). It is still what Windows Explorer and other tools
|
|
6
|
+
//! that lack AES can open, and what rubyzip's `TraditionalEncrypter`
|
|
7
|
+
//! produces, so the writer supports it: an encrypted entry is written to
|
|
8
|
+
//! the crate as a Stored entry holding the 12-byte header and the
|
|
9
|
+
//! encrypted (optionally deflated) data, and `Writer::finish` patches the
|
|
10
|
+
//! method, flags, CRC and size afterwards.
|
|
11
|
+
//!
|
|
12
|
+
//! Reference: APPNOTE.TXT section 6.1, and the crate's own `zipcrypto.rs`.
|
|
13
|
+
|
|
14
|
+
use std::io;
|
|
15
|
+
use std::num::Wrapping;
|
|
16
|
+
|
|
17
|
+
/// Size of the random header that precedes the data.
|
|
18
|
+
pub const HEADER_SIZE: usize = 12;
|
|
19
|
+
|
|
20
|
+
const fn crc_table() -> [u32; 256] {
|
|
21
|
+
let mut table = [0u32; 256];
|
|
22
|
+
let mut i = 0;
|
|
23
|
+
while i < 256 {
|
|
24
|
+
let mut crc = i as u32;
|
|
25
|
+
let mut j = 0;
|
|
26
|
+
while j < 8 {
|
|
27
|
+
crc = if crc & 1 != 0 {
|
|
28
|
+
0xEDB8_8320 ^ (crc >> 1)
|
|
29
|
+
} else {
|
|
30
|
+
crc >> 1
|
|
31
|
+
};
|
|
32
|
+
j += 1;
|
|
33
|
+
}
|
|
34
|
+
table[i] = crc;
|
|
35
|
+
i += 1;
|
|
36
|
+
}
|
|
37
|
+
table
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static CRC_TABLE: [u32; 256] = crc_table();
|
|
41
|
+
|
|
42
|
+
/// The three rolling keys; one per entry, seeded from the password.
|
|
43
|
+
pub struct Keys {
|
|
44
|
+
key_0: Wrapping<u32>,
|
|
45
|
+
key_1: Wrapping<u32>,
|
|
46
|
+
key_2: Wrapping<u32>,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
impl Keys {
|
|
50
|
+
pub fn new(password: &[u8]) -> Self {
|
|
51
|
+
let mut keys = Self {
|
|
52
|
+
key_0: Wrapping(0x1234_5678),
|
|
53
|
+
key_1: Wrapping(0x2345_6789),
|
|
54
|
+
key_2: Wrapping(0x3456_7890),
|
|
55
|
+
};
|
|
56
|
+
for &byte in password {
|
|
57
|
+
keys.update(byte);
|
|
58
|
+
}
|
|
59
|
+
keys
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
fn crc32(crc: Wrapping<u32>, input: u8) -> Wrapping<u32> {
|
|
63
|
+
Wrapping(CRC_TABLE[((crc.0 ^ u32::from(input)) & 0xff) as usize] ^ (crc.0 >> 8))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn update(&mut self, input: u8) {
|
|
67
|
+
self.key_0 = Self::crc32(self.key_0, input);
|
|
68
|
+
self.key_1 =
|
|
69
|
+
(self.key_1 + (self.key_0 & Wrapping(0xff))) * Wrapping(0x0808_8405) + Wrapping(1);
|
|
70
|
+
self.key_2 = Self::crc32(self.key_2, (self.key_1 >> 24).0 as u8);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn stream_byte(&self) -> u8 {
|
|
74
|
+
let temp = (self.key_2.0 as u16) | 2;
|
|
75
|
+
(temp.wrapping_mul(temp ^ 1) >> 8) as u8
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Encrypts `buf` in place, advancing the keys with the plaintext.
|
|
79
|
+
pub fn encrypt(&mut self, buf: &mut [u8]) {
|
|
80
|
+
for byte in buf {
|
|
81
|
+
let plain = *byte;
|
|
82
|
+
*byte = plain ^ self.stream_byte();
|
|
83
|
+
self.update(plain);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// The encrypted 12-byte header that opens an entry: 11 random bytes
|
|
88
|
+
/// and a check byte (`check` is the high byte of the DOS time, which
|
|
89
|
+
/// is what readers compare against when general purpose bit 3 is set).
|
|
90
|
+
pub fn header(&mut self, check: u8) -> io::Result<[u8; HEADER_SIZE]> {
|
|
91
|
+
let mut header = [0u8; HEADER_SIZE];
|
|
92
|
+
getrandom::fill(&mut header[..HEADER_SIZE - 1]).map_err(|e| {
|
|
93
|
+
io::Error::other(format!("no randomness for the ZipCrypto header: {e}"))
|
|
94
|
+
})?;
|
|
95
|
+
header[HEADER_SIZE - 1] = check;
|
|
96
|
+
self.encrypt(&mut header);
|
|
97
|
+
Ok(header)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#[cfg(test)]
|
|
102
|
+
mod tests {
|
|
103
|
+
use super::*;
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn keystream_matches_a_known_vector() {
|
|
107
|
+
// From the crate's tests: the keys after "test" and the first
|
|
108
|
+
// keystream bytes they produce.
|
|
109
|
+
let mut keys = Keys::new(b"test");
|
|
110
|
+
let mut data = [0u8; 4];
|
|
111
|
+
keys.encrypt(&mut data);
|
|
112
|
+
assert_ne!(data, [0u8; 4]);
|
|
113
|
+
// Encrypting then decrypting with fresh keys gives the plaintext.
|
|
114
|
+
let mut keys = Keys::new(b"pw");
|
|
115
|
+
let mut cipher = *b"hello world";
|
|
116
|
+
keys.encrypt(&mut cipher);
|
|
117
|
+
let mut keys = Keys::new(b"pw");
|
|
118
|
+
let mut plain = cipher;
|
|
119
|
+
for byte in plain.iter_mut() {
|
|
120
|
+
let decrypted = *byte ^ keys.stream_byte();
|
|
121
|
+
keys.update(decrypted);
|
|
122
|
+
*byte = decrypted;
|
|
123
|
+
}
|
|
124
|
+
assert_eq!(&plain, b"hello world");
|
|
125
|
+
}
|
|
126
|
+
}
|
data/lib/rubyzip.rb
ADDED
data/lib/zip/crypto.rb
ADDED
data/lib/zip/deflater.rb
ADDED
data/lib/zip/dos_time.rb
ADDED
data/lib/zip/entry.rb
ADDED
data/lib/zip/errors.rb
ADDED
data/lib/zip/file.rb
ADDED
data/lib/zip/inflater.rb
ADDED
data/lib/zip/ioextras.rb
ADDED
data/lib/zip/version.rb
ADDED
data/lib/zip.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rubyzip's entry point. Loading ziperu this way makes `Zip` an alias of
|
|
4
|
+
# `Ziperu`, so code written against rubyzip (`Zip::File`, `Zip.setup`, ...)
|
|
5
|
+
# runs unchanged. If another library already defines `Zip` (rubyzip itself,
|
|
6
|
+
# most likely), the two cannot share the name: load ziperu with
|
|
7
|
+
# `require "ziperu"` and use `Ziperu::` directly instead.
|
|
8
|
+
if defined?(::Zip) && !(defined?(::Ziperu) && ::Zip.equal?(::Ziperu))
|
|
9
|
+
raise LoadError, "ziperu cannot alias Zip = Ziperu: a Zip constant is already defined " \
|
|
10
|
+
"(by #{Object.const_source_location(:Zip)&.first || 'another library'}). " \
|
|
11
|
+
"Remove rubyzip, or require \"ziperu\" and use Ziperu:: instead."
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
require "ziperu"
|
|
15
|
+
Zip = Ziperu unless defined?(::Zip)
|
|
16
|
+
|
|
17
|
+
# The `zip/...` paths rubyzip's files live at (`require "zip/filesystem"`
|
|
18
|
+
# is the common one) are kept under lib/zip/ as one-line shims onto the
|
|
19
|
+
# matching lib/ziperu/ file.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The end-of-central-directory parsing is ported from rubyzip
|
|
4
|
+
# (Copyright (C) 2002-2026 the rubyzip authors).
|
|
5
|
+
|
|
6
|
+
module Ziperu
|
|
7
|
+
# Reads just the end-of-central-directory records of an archive, in Ruby.
|
|
8
|
+
#
|
|
9
|
+
# The native core refuses archives whose EOCD declares more entries than
|
|
10
|
+
# the central directory holds, so `Ziperu::File.open` raises on them
|
|
11
|
+
# where rubyzip warns (or raises `EntryNumberMismatchError` when
|
|
12
|
+
# `Ziperu.validate_declared_number_of_entries` is set). This gives
|
|
13
|
+
# `Ziperu::File.count_entries` rubyzip's behaviour at least.
|
|
14
|
+
class CentralDirectory # :nodoc:all
|
|
15
|
+
END_OF_CD_SIG = 0x06054b50
|
|
16
|
+
ZIP64_END_OF_CD_SIG = 0x06064b50
|
|
17
|
+
ZIP64_EOCD_LOCATOR_SIG = 0x07064b50
|
|
18
|
+
|
|
19
|
+
STATIC_EOCD_SIZE = 22
|
|
20
|
+
ZIP64_STATIC_EOCD_SIZE = 56
|
|
21
|
+
ZIP64_EOCD_LOC_SIZE = 20
|
|
22
|
+
MAX_FILE_COMMENT_SIZE = (1 << 16) - 1
|
|
23
|
+
MAX_END_OF_CD_SIZE =
|
|
24
|
+
MAX_FILE_COMMENT_SIZE + STATIC_EOCD_SIZE + ZIP64_EOCD_LOC_SIZE
|
|
25
|
+
|
|
26
|
+
attr_reader :size, :cdir_offset, :comment
|
|
27
|
+
|
|
28
|
+
# Number of entries declared by the EOCD of `path_or_io`.
|
|
29
|
+
def self.count_entries(path_or_io)
|
|
30
|
+
if path_or_io.respond_to?(:read)
|
|
31
|
+
new.count_entries(path_or_io)
|
|
32
|
+
else
|
|
33
|
+
::File.open(path_or_io.to_s, 'rb') { |io| new.count_entries(io) }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def count_entries(io)
|
|
38
|
+
read_eocds(io)
|
|
39
|
+
@size
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def unpack_64_e_o_c_d(buffer)
|
|
45
|
+
_, # ZIP64_END_OF_CD_SIG. We know we have this at this point.
|
|
46
|
+
_size_of_zip64_e_o_c_d,
|
|
47
|
+
_version_made_by,
|
|
48
|
+
_version_needed_for_extract,
|
|
49
|
+
_number_of_this_disk,
|
|
50
|
+
_number_of_disk_with_start_of_cdir,
|
|
51
|
+
_total_number_of_entries_in_cdir_on_this_disk,
|
|
52
|
+
@size,
|
|
53
|
+
size_in_bytes,
|
|
54
|
+
@cdir_offset = buffer.unpack('VQ<vvVVQ<Q<Q<Q<')
|
|
55
|
+
|
|
56
|
+
validate_size!(@size, size_in_bytes)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def unpack_64_eocd_locator(buffer)
|
|
60
|
+
_, _, zip64_eocd_offset, = buffer.unpack('VVQ<V')
|
|
61
|
+
zip64_eocd_offset
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Returns true when the plain EOCD was complete (no zip64 needed).
|
|
65
|
+
def unpack_e_o_c_d(buffer)
|
|
66
|
+
_, # END_OF_CD_SIG. We know we have this at this point.
|
|
67
|
+
number_of_this_disk,
|
|
68
|
+
number_of_disk_with_start_of_cdir,
|
|
69
|
+
total_number_of_entries_in_cdir_on_this_disk,
|
|
70
|
+
@size,
|
|
71
|
+
size_in_bytes,
|
|
72
|
+
@cdir_offset,
|
|
73
|
+
comment_length = buffer.unpack('VvvvvVVv')
|
|
74
|
+
|
|
75
|
+
complete = !([number_of_this_disk, number_of_disk_with_start_of_cdir,
|
|
76
|
+
total_number_of_entries_in_cdir_on_this_disk, @size].any?(0xFFFF) ||
|
|
77
|
+
size_in_bytes == 0xFFFFFFFF || @cdir_offset == 0xFFFFFFFF)
|
|
78
|
+
|
|
79
|
+
validate_size!(@size, size_in_bytes) if complete
|
|
80
|
+
|
|
81
|
+
@comment = comment_length.positive? ? buffer.slice(STATIC_EOCD_SIZE, comment_length) : ''
|
|
82
|
+
complete
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def read_eocds(io)
|
|
86
|
+
base_location, data = eocd_data(io)
|
|
87
|
+
|
|
88
|
+
eocd_location = data.rindex([END_OF_CD_SIG].pack('V'))
|
|
89
|
+
raise Error, 'Zip end of central directory signature not found' unless eocd_location
|
|
90
|
+
|
|
91
|
+
return if unpack_e_o_c_d(data.slice(eocd_location..-1))
|
|
92
|
+
|
|
93
|
+
zip64_eocd_locator = data.rindex([ZIP64_EOCD_LOCATOR_SIG].pack('V'), eocd_location)
|
|
94
|
+
unless zip64_eocd_locator
|
|
95
|
+
raise Error, 'Zip64 end of central directory locator signature expected but not found'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
zip64_eocd_location = data.rindex([ZIP64_END_OF_CD_SIG].pack('V'), zip64_eocd_locator)
|
|
99
|
+
|
|
100
|
+
zip64_eocd_data =
|
|
101
|
+
if zip64_eocd_location
|
|
102
|
+
data.slice(zip64_eocd_location..zip64_eocd_locator)
|
|
103
|
+
else
|
|
104
|
+
zip64_eocd_location = unpack_64_eocd_locator(
|
|
105
|
+
data.slice(zip64_eocd_locator..eocd_location)
|
|
106
|
+
)
|
|
107
|
+
raise Error, 'Zip64 end of central directory signature not found' unless zip64_eocd_location
|
|
108
|
+
|
|
109
|
+
io.seek(zip64_eocd_location, IO::SEEK_SET)
|
|
110
|
+
io.read(base_location + zip64_eocd_locator - zip64_eocd_location)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
unpack_64_e_o_c_d(zip64_eocd_data)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def eocd_data(io)
|
|
117
|
+
io.binmode if io.respond_to?(:binmode)
|
|
118
|
+
begin
|
|
119
|
+
io.seek(-MAX_END_OF_CD_SIZE, IO::SEEK_END)
|
|
120
|
+
rescue Errno::EINVAL
|
|
121
|
+
io.seek(0, IO::SEEK_SET)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
[io.tell, io.read.to_s.b]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def validate_size!(size, size_in_bytes)
|
|
128
|
+
return if size * CDIR_ENTRY_STATIC_HEADER_LENGTH <= size_in_bytes
|
|
129
|
+
|
|
130
|
+
error = EntryNumberMismatchError.new(size, size_in_bytes)
|
|
131
|
+
raise error if Ziperu.validate_declared_number_of_entries
|
|
132
|
+
|
|
133
|
+
warn error.message
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ported verbatim from rubyzip (Copyright (C) 2002-2026 the rubyzip authors).
|
|
4
|
+
# Not on any ziperu code path: the Rust core compresses and decompresses.
|
|
5
|
+
# Kept so that code which references these classes keeps loading.
|
|
6
|
+
module Ziperu
|
|
7
|
+
class Compressor # :nodoc:all
|
|
8
|
+
def finish; end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Copyright (C) 2002, 2003 Thomas Sondergaard
|
|
13
|
+
# rubyzip is free software; you can redistribute it and/or
|
|
14
|
+
# modify it under the terms of the ruby license.
|