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,713 @@
|
|
|
1
|
+
//! `Ziperu::Native::Archive`: a read-only handle on a parsed central directory.
|
|
2
|
+
//!
|
|
3
|
+
//! Entry metadata crosses the Ruby boundary as plain Hashes (all at once via
|
|
4
|
+
//! `entries`, or one at a time via `entry_meta`); afterwards Ruby only talks
|
|
5
|
+
//! to the archive by index.
|
|
6
|
+
|
|
7
|
+
use std::cell::{RefCell, RefMut};
|
|
8
|
+
use std::collections::HashMap;
|
|
9
|
+
use std::fs;
|
|
10
|
+
use std::io::{self, Read, Seek, SeekFrom};
|
|
11
|
+
|
|
12
|
+
use magnus::{
|
|
13
|
+
gc::Marker, typed_data::DataTypeFunctions, value::Opaque, Error, RArray, RHash, RString, Ruby,
|
|
14
|
+
Symbol, TypedData, Value,
|
|
15
|
+
};
|
|
16
|
+
use zip::read::{read_zipfile_from_stream_with_options, ZipFileEntry, ZipReadOptions};
|
|
17
|
+
use zip::result::{ZipError, ZipResult};
|
|
18
|
+
use zip::ZipArchive;
|
|
19
|
+
|
|
20
|
+
use crate::errors::{busy_error, io_error, read_error, zip_error};
|
|
21
|
+
use crate::gvl::maybe_without_gvl;
|
|
22
|
+
use crate::reader::{EntryInfo, EntryReader};
|
|
23
|
+
use crate::source::Source;
|
|
24
|
+
|
|
25
|
+
/// Never pre-allocate more than this on the strength of a header field.
|
|
26
|
+
const CAPACITY_HINT_MAX: u64 = 1 << 20;
|
|
27
|
+
|
|
28
|
+
#[derive(TypedData)]
|
|
29
|
+
#[magnus(class = "Ziperu::Native::Archive", free_immediately, mark)]
|
|
30
|
+
pub struct Archive {
|
|
31
|
+
// Borrowed only while holding the GVL and never across a release of it
|
|
32
|
+
// (work that releases the GVL runs on a clone), so a failed borrow
|
|
33
|
+
// means another thread is inside a method right now. `None` once
|
|
34
|
+
// closed, which releases the file handle at once rather than when the
|
|
35
|
+
// GC gets round to it (commit renames over the file).
|
|
36
|
+
inner: RefCell<Option<ZipArchive<Source>>>,
|
|
37
|
+
/// The Ruby IO behind a `Source::Ruby`, kept alive by `mark`.
|
|
38
|
+
io: Option<Opaque<Value>>,
|
|
39
|
+
/// True for a Ruby IO source, whose reads must hold the GVL.
|
|
40
|
+
needs_gvl: bool,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl DataTypeFunctions for Archive {
|
|
44
|
+
fn mark(&self, marker: &Marker) {
|
|
45
|
+
if let Some(io) = self.io {
|
|
46
|
+
marker.mark(io);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
impl Archive {
|
|
52
|
+
pub fn open_path(ruby: &Ruby, path: String) -> Result<Self, Error> {
|
|
53
|
+
let file = fs::File::open(&path).map_err(|e| io_error(ruby, e))?;
|
|
54
|
+
Self::from_source(ruby, Source::file(file))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub fn open_bytes(ruby: &Ruby, bytes: RString) -> Result<Self, Error> {
|
|
58
|
+
// Copy once; the Ruby string may be mutated or GC'd after this call.
|
|
59
|
+
let vec = unsafe { bytes.as_slice() }.to_vec();
|
|
60
|
+
Self::from_source(ruby, Source::memory(vec))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Any seekable IO-like Ruby object; read lazily, never copied.
|
|
64
|
+
pub fn open_io(ruby: &Ruby, io: Value) -> Result<Self, Error> {
|
|
65
|
+
Self::from_source(ruby, Source::ruby(io)?)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fn from_source(ruby: &Ruby, source: Source) -> Result<Self, Error> {
|
|
69
|
+
let needs_gvl = source.needs_gvl();
|
|
70
|
+
let io = source.ruby_io();
|
|
71
|
+
let archive = maybe_without_gvl(needs_gvl, || ZipArchive::new(source))
|
|
72
|
+
.map_err(|e| zip_error(ruby, e))?;
|
|
73
|
+
Ok(Self {
|
|
74
|
+
inner: RefCell::new(Some(archive)),
|
|
75
|
+
io,
|
|
76
|
+
needs_gvl,
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pub(crate) fn needs_gvl(&self) -> bool {
|
|
81
|
+
self.needs_gvl
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// The open archive, or an IOError once closed.
|
|
85
|
+
pub(crate) fn archive(&self, ruby: &Ruby) -> Result<RefMut<'_, ZipArchive<Source>>, Error> {
|
|
86
|
+
let guard = self
|
|
87
|
+
.inner
|
|
88
|
+
.try_borrow_mut()
|
|
89
|
+
.map_err(|_| busy_error(ruby, "archive"))?;
|
|
90
|
+
RefMut::filter_map(guard, Option::as_mut)
|
|
91
|
+
.map_err(|_| Error::new(ruby.exception_io_error(), "closed archive"))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Releases the underlying file. Readers already opened keep their own
|
|
95
|
+
/// handle and continue to work.
|
|
96
|
+
pub fn close(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
|
|
97
|
+
rb_self
|
|
98
|
+
.inner
|
|
99
|
+
.try_borrow_mut()
|
|
100
|
+
.map_err(|_| busy_error(ruby, "archive"))?
|
|
101
|
+
.take();
|
|
102
|
+
Ok(())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
pub fn is_closed(&self) -> bool {
|
|
106
|
+
self.inner.try_borrow().is_ok_and(|a| a.is_none())
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pub fn entry_count(ruby: &Ruby, rb_self: &Self) -> Result<usize, Error> {
|
|
110
|
+
Ok(rb_self.archive(ruby)?.len())
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pub fn comment(ruby: &Ruby, rb_self: &Self) -> Result<RString, Error> {
|
|
114
|
+
Ok(ruby.str_from_slice(rb_self.archive(ruby)?.comment()))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// True when the central directory holds several records under one
|
|
118
|
+
/// name. The crate keys its parsed view by name and so keeps only the
|
|
119
|
+
/// last of them; `entries` then walks the raw records instead.
|
|
120
|
+
pub fn has_duplicate_names(ruby: &Ruby, rb_self: &Self) -> Result<bool, Error> {
|
|
121
|
+
Ok(rb_self.archive(ruby)?.has_duplicate_names())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Sum of the declared uncompressed sizes, for `Ziperu.max_total_size`.
|
|
125
|
+
pub fn total_size(ruby: &Ruby, rb_self: &Self) -> Result<u64, Error> {
|
|
126
|
+
let archive = rb_self.archive(ruby)?;
|
|
127
|
+
let meta = archive.metadata();
|
|
128
|
+
let mut total: u64 = 0;
|
|
129
|
+
for i in 0..meta.len() {
|
|
130
|
+
total = total.saturating_add(meta.entry(i).map_err(|e| zip_error(ruby, e))?.size());
|
|
131
|
+
}
|
|
132
|
+
Ok(total)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Inflates entry `index` to nowhere so the crate checks its CRC (and
|
|
136
|
+
/// its declared size), with the GVL released when the source allows.
|
|
137
|
+
pub fn verify_entry(
|
|
138
|
+
ruby: &Ruby,
|
|
139
|
+
rb_self: &Self,
|
|
140
|
+
index: usize,
|
|
141
|
+
password: Option<RString>,
|
|
142
|
+
) -> Result<(), Error> {
|
|
143
|
+
let password = password.map(|p| unsafe { p.as_slice() }.to_vec());
|
|
144
|
+
let mut archive = rb_self.archive(ruby)?.clone();
|
|
145
|
+
maybe_without_gvl(rb_self.needs_gvl, || {
|
|
146
|
+
let options = ZipReadOptions::new().password(password.as_deref());
|
|
147
|
+
let mut entry = archive.by_index_with_options(index, options)?;
|
|
148
|
+
io::copy(&mut entry, &mut io::sink())?;
|
|
149
|
+
Ok::<_, ZipError>(())
|
|
150
|
+
})
|
|
151
|
+
.map_err(|e| match e {
|
|
152
|
+
ZipError::Io(io) => read_error(ruby, io),
|
|
153
|
+
other => zip_error(ruby, other),
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// `verify_entry` for a record the crate cannot address by index (one
|
|
158
|
+
/// of several under the same name): the entry is read from its local
|
|
159
|
+
/// header with the central record's sizes and CRC, as `open_entry_at`
|
|
160
|
+
/// reads it.
|
|
161
|
+
pub fn verify_entry_at(
|
|
162
|
+
ruby: &Ruby,
|
|
163
|
+
rb_self: &Self,
|
|
164
|
+
header_offset: u64,
|
|
165
|
+
size: u64,
|
|
166
|
+
compressed_size: u64,
|
|
167
|
+
crc: u32,
|
|
168
|
+
password: Option<RString>,
|
|
169
|
+
) -> Result<(), Error> {
|
|
170
|
+
let password = password.map(|p| unsafe { p.as_slice() }.to_vec());
|
|
171
|
+
let mut source = rb_self.archive(ruby)?.clone().into_inner();
|
|
172
|
+
maybe_without_gvl(rb_self.needs_gvl, || {
|
|
173
|
+
source.seek(SeekFrom::Start(header_offset))?;
|
|
174
|
+
let options = ZipReadOptions::new()
|
|
175
|
+
.password(password.as_deref())
|
|
176
|
+
.override_compressed_size(compressed_size)
|
|
177
|
+
.override_uncompressed_size(size)
|
|
178
|
+
.override_crc(crc);
|
|
179
|
+
let mut entry = read_zipfile_from_stream_with_options(&mut source, options)?.ok_or(
|
|
180
|
+
ZipError::InvalidArchive("expected a local file header".into()),
|
|
181
|
+
)?;
|
|
182
|
+
io::copy(&mut entry, &mut io::sink())?;
|
|
183
|
+
Ok::<_, ZipError>(())
|
|
184
|
+
})
|
|
185
|
+
.map_err(|e| match e {
|
|
186
|
+
ZipError::Io(io) => read_error(ruby, io),
|
|
187
|
+
other => zip_error(ruby, other),
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// One Hash per entry, in central-directory order.
|
|
192
|
+
///
|
|
193
|
+
/// The parsed central directory already holds nearly everything; the
|
|
194
|
+
/// few fields read raw come from one sequential pass over the records,
|
|
195
|
+
/// done without the GVL when the source allows it. An archive with
|
|
196
|
+
/// several records under one name is listed from the raw records
|
|
197
|
+
/// alone, since the crate's view keeps only the last of each name: a
|
|
198
|
+
/// record the crate still addresses carries its index, the others nil.
|
|
199
|
+
pub fn entries(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
|
|
200
|
+
let archive = rb_self.archive(ruby)?;
|
|
201
|
+
let meta = archive.metadata();
|
|
202
|
+
let duplicates = archive.has_duplicate_names();
|
|
203
|
+
let dir_start = archive.central_directory_start();
|
|
204
|
+
let archive_offset = archive.offset();
|
|
205
|
+
let mut raw = archive.clone().into_inner();
|
|
206
|
+
drop(archive);
|
|
207
|
+
let keys = MetaKeys::new(ruby);
|
|
208
|
+
|
|
209
|
+
if duplicates {
|
|
210
|
+
let records = maybe_without_gvl(rb_self.needs_gvl, || {
|
|
211
|
+
walk_central_directory(&mut raw, dir_start)
|
|
212
|
+
})
|
|
213
|
+
.map_err(|e| io_error(ruby, e))?;
|
|
214
|
+
let mut by_offset = HashMap::with_capacity(meta.len());
|
|
215
|
+
for i in 0..meta.len() {
|
|
216
|
+
let entry = meta.entry(i).map_err(|e| zip_error(ruby, e))?;
|
|
217
|
+
by_offset.insert(entry.central_header_start(), i);
|
|
218
|
+
}
|
|
219
|
+
let out = ruby.ary_new_capa(records.len());
|
|
220
|
+
for record in &records {
|
|
221
|
+
let index = by_offset.get(&record.central_offset).copied();
|
|
222
|
+
out.push(record_hash(ruby, &keys, index, record, archive_offset)?)?;
|
|
223
|
+
}
|
|
224
|
+
return Ok(out);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let records = maybe_without_gvl(rb_self.needs_gvl, || {
|
|
228
|
+
(0..meta.len())
|
|
229
|
+
.map(|i| {
|
|
230
|
+
let entry = meta.entry(i)?;
|
|
231
|
+
Ok(raw_central_record(&mut raw, entry.central_header_start())?)
|
|
232
|
+
})
|
|
233
|
+
.collect::<ZipResult<Vec<_>>>()
|
|
234
|
+
})
|
|
235
|
+
.map_err(|e| zip_error(ruby, e))?;
|
|
236
|
+
let out = ruby.ary_new_capa(meta.len());
|
|
237
|
+
for (i, record) in records.iter().enumerate() {
|
|
238
|
+
let entry = meta.entry(i).map_err(|e| zip_error(ruby, e))?;
|
|
239
|
+
out.push(entry_hash(ruby, &keys, i, &entry, record)?)?;
|
|
240
|
+
}
|
|
241
|
+
Ok(out)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// The Hash for a single entry.
|
|
245
|
+
pub fn entry_meta(ruby: &Ruby, rb_self: &Self, index: usize) -> Result<RHash, Error> {
|
|
246
|
+
let archive = rb_self.archive(ruby)?;
|
|
247
|
+
let meta = archive.metadata();
|
|
248
|
+
let mut raw = archive.clone().into_inner();
|
|
249
|
+
drop(archive);
|
|
250
|
+
let entry = meta.entry(index).map_err(|e| zip_error(ruby, e))?;
|
|
251
|
+
let record = raw_central_record(&mut raw, entry.central_header_start())
|
|
252
|
+
.map_err(|e| io_error(ruby, e))?;
|
|
253
|
+
entry_hash(ruby, &MetaKeys::new(ruby), index, &entry, &record)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// The raw extra field bytes of entry `index`'s local header. Some
|
|
257
|
+
/// fields (Info-ZIP unix uid/gid, extended timestamp atime/ctime) only
|
|
258
|
+
/// carry their data there; Ruby merges them lazily.
|
|
259
|
+
pub fn local_extra(ruby: &Ruby, rb_self: &Self, index: usize) -> Result<RString, Error> {
|
|
260
|
+
let archive = rb_self.archive(ruby)?;
|
|
261
|
+
let header_start = archive
|
|
262
|
+
.metadata()
|
|
263
|
+
.entry(index)
|
|
264
|
+
.map_err(|e| zip_error(ruby, e))?
|
|
265
|
+
.header_start();
|
|
266
|
+
let mut raw = archive.clone().into_inner();
|
|
267
|
+
drop(archive);
|
|
268
|
+
let extra = raw_local_extra(&mut raw, header_start).map_err(|e| io_error(ruby, e))?;
|
|
269
|
+
Ok(ruby.str_from_slice(&extra))
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/// `local_extra` for a record addressed by its local header offset.
|
|
273
|
+
pub fn local_extra_at(
|
|
274
|
+
ruby: &Ruby,
|
|
275
|
+
rb_self: &Self,
|
|
276
|
+
header_offset: u64,
|
|
277
|
+
) -> Result<RString, Error> {
|
|
278
|
+
let mut raw = rb_self.archive(ruby)?.clone().into_inner();
|
|
279
|
+
let extra = raw_local_extra(&mut raw, header_offset).map_err(|e| io_error(ruby, e))?;
|
|
280
|
+
Ok(ruby.str_from_slice(&extra))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/// A chunked reader over entry `index`, with its own cursor. With
|
|
284
|
+
/// `enforce_size` false it reads past the declared size to the true end
|
|
285
|
+
/// of the data.
|
|
286
|
+
pub fn open_entry(
|
|
287
|
+
ruby: &Ruby,
|
|
288
|
+
rb_self: &Self,
|
|
289
|
+
index: usize,
|
|
290
|
+
password: Option<RString>,
|
|
291
|
+
enforce_size: bool,
|
|
292
|
+
) -> Result<EntryReader, Error> {
|
|
293
|
+
let password = password.map(|p| unsafe { p.as_slice() }.to_vec());
|
|
294
|
+
let archive = rb_self.archive(ruby)?.clone();
|
|
295
|
+
EntryReader::open(
|
|
296
|
+
ruby,
|
|
297
|
+
archive,
|
|
298
|
+
index,
|
|
299
|
+
password,
|
|
300
|
+
enforce_size,
|
|
301
|
+
rb_self.needs_gvl,
|
|
302
|
+
rb_self.io,
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/// `open_entry` for a record the crate cannot address by index: the
|
|
307
|
+
/// entry is read from its local header, with the sizes and CRC of its
|
|
308
|
+
/// central record standing in for whatever the local header lacks.
|
|
309
|
+
#[allow(clippy::too_many_arguments)]
|
|
310
|
+
pub fn open_entry_at(
|
|
311
|
+
ruby: &Ruby,
|
|
312
|
+
rb_self: &Self,
|
|
313
|
+
header_offset: u64,
|
|
314
|
+
size: u64,
|
|
315
|
+
compressed_size: u64,
|
|
316
|
+
crc: u32,
|
|
317
|
+
password: Option<RString>,
|
|
318
|
+
enforce_size: bool,
|
|
319
|
+
) -> Result<EntryReader, Error> {
|
|
320
|
+
let password = password.map(|p| unsafe { p.as_slice() }.to_vec());
|
|
321
|
+
let archive = rb_self.archive(ruby)?.clone();
|
|
322
|
+
let info = EntryInfo {
|
|
323
|
+
index: None,
|
|
324
|
+
header_start: header_offset,
|
|
325
|
+
size,
|
|
326
|
+
compressed_size,
|
|
327
|
+
crc32: crc,
|
|
328
|
+
};
|
|
329
|
+
EntryReader::open_at(
|
|
330
|
+
ruby,
|
|
331
|
+
archive,
|
|
332
|
+
info,
|
|
333
|
+
password,
|
|
334
|
+
enforce_size,
|
|
335
|
+
rb_self.needs_gvl,
|
|
336
|
+
rb_self.io,
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// Whole decompressed entry as a binary String.
|
|
341
|
+
pub fn read_entry(ruby: &Ruby, rb_self: &Self, index: usize) -> Result<RString, Error> {
|
|
342
|
+
// A clone (the central directory is shared behind an Arc) so the
|
|
343
|
+
// borrow is released before the GVL is.
|
|
344
|
+
let mut archive = rb_self.archive(ruby)?.clone();
|
|
345
|
+
let mut entry = archive.by_index(index).map_err(|e| zip_error(ruby, e))?;
|
|
346
|
+
// A hint only: the declared size is untrusted and may be a lie.
|
|
347
|
+
let mut buf = Vec::with_capacity(entry.size().min(CAPACITY_HINT_MAX) as usize);
|
|
348
|
+
maybe_without_gvl(rb_self.needs_gvl, || entry.read_to_end(&mut buf))
|
|
349
|
+
.map_err(|e| read_error(ruby, e))?;
|
|
350
|
+
Ok(ruby.str_from_slice(&buf))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// Extracts entries to `dest` in one call; see `extract::extract_entries`.
|
|
354
|
+
pub fn extract_entries(
|
|
355
|
+
ruby: &Ruby,
|
|
356
|
+
rb_self: &Self,
|
|
357
|
+
dest: String,
|
|
358
|
+
specs: RArray,
|
|
359
|
+
enforce_size: bool,
|
|
360
|
+
) -> Result<RArray, Error> {
|
|
361
|
+
let archive = rb_self.archive(ruby)?.clone();
|
|
362
|
+
crate::extract::extract_entries(ruby, archive, rb_self.needs_gvl, dest, specs, enforce_size)
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/// A central directory record read straight from the bytes.
|
|
367
|
+
///
|
|
368
|
+
/// Two fields are read this way for every entry, where the crate's parsed
|
|
369
|
+
/// view is not what rubyzip exposes: the extra field exactly as stored
|
|
370
|
+
/// (the crate re-serializes the fields it parsed, which drops some, an
|
|
371
|
+
/// extended timestamp without the local-only parts for one), and the DOS
|
|
372
|
+
/// time and date as stored (the crate's `DateTime` rejects values a
|
|
373
|
+
/// calendar would roll over, February 30th, which rubyzip reads as the
|
|
374
|
+
/// next day). The rest only matters for a record the crate does not keep.
|
|
375
|
+
struct RawCentralRecord {
|
|
376
|
+
/// Where the record starts, which is how it is matched to the crate's
|
|
377
|
+
/// `central_header_start`.
|
|
378
|
+
central_offset: u64,
|
|
379
|
+
version_made_by: u16,
|
|
380
|
+
flags: u16,
|
|
381
|
+
method: u16,
|
|
382
|
+
dos_time: u16,
|
|
383
|
+
dos_date: u16,
|
|
384
|
+
crc32: u32,
|
|
385
|
+
compressed_size: u64,
|
|
386
|
+
size: u64,
|
|
387
|
+
external_attributes: u32,
|
|
388
|
+
/// As stored: relative to the start of the archive proper, before any
|
|
389
|
+
/// prepended data.
|
|
390
|
+
header_offset: u64,
|
|
391
|
+
name: Vec<u8>,
|
|
392
|
+
extra: Vec<u8>,
|
|
393
|
+
comment: Vec<u8>,
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const CENTRAL_SIGNATURE: u32 = 0x0201_4b50;
|
|
397
|
+
const CENTRAL_HEADER_LEN: usize = 46;
|
|
398
|
+
const ZIP64_EXTRA_ID: u16 = 0x0001;
|
|
399
|
+
|
|
400
|
+
/// The record at `central_offset`.
|
|
401
|
+
fn raw_central_record(source: &mut Source, central_offset: u64) -> io::Result<RawCentralRecord> {
|
|
402
|
+
source.seek_to(central_offset)?;
|
|
403
|
+
read_central_record(source, central_offset)?.ok_or_else(|| {
|
|
404
|
+
io::Error::new(
|
|
405
|
+
io::ErrorKind::InvalidData,
|
|
406
|
+
"central directory signature not found",
|
|
407
|
+
)
|
|
408
|
+
})
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/// Every record from `dir_start` to the end of the central directory.
|
|
412
|
+
fn walk_central_directory(
|
|
413
|
+
source: &mut Source,
|
|
414
|
+
dir_start: u64,
|
|
415
|
+
) -> io::Result<Vec<RawCentralRecord>> {
|
|
416
|
+
source.seek(SeekFrom::Start(dir_start))?;
|
|
417
|
+
let mut records = Vec::new();
|
|
418
|
+
let mut offset = dir_start;
|
|
419
|
+
while let Some(record) = read_central_record(source, offset)? {
|
|
420
|
+
offset = record.next_offset();
|
|
421
|
+
records.push(record);
|
|
422
|
+
}
|
|
423
|
+
Ok(records)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/// The record at the source's position, or `None` where the central
|
|
427
|
+
/// directory has ended (an end-of-central-directory signature instead).
|
|
428
|
+
fn read_central_record(
|
|
429
|
+
source: &mut Source,
|
|
430
|
+
central_offset: u64,
|
|
431
|
+
) -> io::Result<Option<RawCentralRecord>> {
|
|
432
|
+
// The signature first: what follows the last record (the end of
|
|
433
|
+
// central directory) is shorter than a record header.
|
|
434
|
+
let mut header = [0u8; CENTRAL_HEADER_LEN];
|
|
435
|
+
source.read_exact(&mut header[..4])?;
|
|
436
|
+
if le_u32(&header[0..4]) != CENTRAL_SIGNATURE {
|
|
437
|
+
return Ok(None);
|
|
438
|
+
}
|
|
439
|
+
source.read_exact(&mut header[4..])?;
|
|
440
|
+
let name_len = le_u16(&header[28..30]) as usize;
|
|
441
|
+
let extra_len = le_u16(&header[30..32]) as usize;
|
|
442
|
+
let comment_len = le_u16(&header[32..34]) as usize;
|
|
443
|
+
let mut name = vec![0u8; name_len];
|
|
444
|
+
source.read_exact(&mut name)?;
|
|
445
|
+
let mut extra = vec![0u8; extra_len];
|
|
446
|
+
source.read_exact(&mut extra)?;
|
|
447
|
+
let mut comment = vec![0u8; comment_len];
|
|
448
|
+
source.read_exact(&mut comment)?;
|
|
449
|
+
|
|
450
|
+
let mut record = RawCentralRecord {
|
|
451
|
+
central_offset,
|
|
452
|
+
version_made_by: le_u16(&header[4..6]),
|
|
453
|
+
flags: le_u16(&header[8..10]),
|
|
454
|
+
method: le_u16(&header[10..12]),
|
|
455
|
+
dos_time: le_u16(&header[12..14]),
|
|
456
|
+
dos_date: le_u16(&header[14..16]),
|
|
457
|
+
crc32: le_u32(&header[16..20]),
|
|
458
|
+
compressed_size: u64::from(le_u32(&header[20..24])),
|
|
459
|
+
size: u64::from(le_u32(&header[24..28])),
|
|
460
|
+
external_attributes: le_u32(&header[38..42]),
|
|
461
|
+
header_offset: u64::from(le_u32(&header[42..46])),
|
|
462
|
+
name,
|
|
463
|
+
extra,
|
|
464
|
+
comment,
|
|
465
|
+
};
|
|
466
|
+
record.apply_zip64();
|
|
467
|
+
Ok(Some(record))
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
impl RawCentralRecord {
|
|
471
|
+
fn next_offset(&self) -> u64 {
|
|
472
|
+
self.central_offset
|
|
473
|
+
+ (CENTRAL_HEADER_LEN + self.name.len() + self.extra.len() + self.comment.len()) as u64
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/// A zip64 extra field carries the 64-bit value of each size or
|
|
477
|
+
/// offset the record stores as 0xFFFFFFFF, in header order.
|
|
478
|
+
fn apply_zip64(&mut self) {
|
|
479
|
+
let mut rest = self.extra.as_slice();
|
|
480
|
+
while rest.len() >= 4 {
|
|
481
|
+
let id = le_u16(&rest[0..2]);
|
|
482
|
+
let len = le_u16(&rest[2..4]) as usize;
|
|
483
|
+
rest = &rest[4..];
|
|
484
|
+
if rest.len() < len {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
let (mut data, tail) = rest.split_at(len);
|
|
488
|
+
rest = tail;
|
|
489
|
+
if id != ZIP64_EXTRA_ID {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
for field in [
|
|
493
|
+
&mut self.size,
|
|
494
|
+
&mut self.compressed_size,
|
|
495
|
+
&mut self.header_offset,
|
|
496
|
+
] {
|
|
497
|
+
if *field != u64::from(u32::MAX) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
let Some((value, more)) = data.split_first_chunk::<8>() else {
|
|
501
|
+
return;
|
|
502
|
+
};
|
|
503
|
+
*field = u64::from_le_bytes(*value);
|
|
504
|
+
data = more;
|
|
505
|
+
}
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
fn le_u16(bytes: &[u8]) -> u16 {
|
|
512
|
+
u16::from_le_bytes([bytes[0], bytes[1]])
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
fn le_u32(bytes: &[u8]) -> u32 {
|
|
516
|
+
u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/// The extra field bytes of a local file header, exactly as stored.
|
|
520
|
+
fn raw_local_extra(source: &mut Source, header_offset: u64) -> io::Result<Vec<u8>> {
|
|
521
|
+
const SIGNATURE: u32 = 0x0403_4b50;
|
|
522
|
+
const HEADER_LEN: usize = 30;
|
|
523
|
+
|
|
524
|
+
source.seek(SeekFrom::Start(header_offset))?;
|
|
525
|
+
let mut header = [0u8; HEADER_LEN];
|
|
526
|
+
source.read_exact(&mut header)?;
|
|
527
|
+
if u32::from_le_bytes([header[0], header[1], header[2], header[3]]) != SIGNATURE {
|
|
528
|
+
return Err(io::Error::new(
|
|
529
|
+
io::ErrorKind::InvalidData,
|
|
530
|
+
"local file header signature not found",
|
|
531
|
+
));
|
|
532
|
+
}
|
|
533
|
+
let name_len = u16::from_le_bytes([header[26], header[27]]) as i64;
|
|
534
|
+
let extra_len = u16::from_le_bytes([header[28], header[29]]) as usize;
|
|
535
|
+
source.seek(SeekFrom::Current(name_len))?;
|
|
536
|
+
let mut extra = vec![0u8; extra_len];
|
|
537
|
+
source.read_exact(&mut extra)?;
|
|
538
|
+
Ok(extra)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/// The Hash keys, interned once per listing rather than once per entry.
|
|
542
|
+
struct MetaKeys {
|
|
543
|
+
index: Symbol,
|
|
544
|
+
name: Symbol,
|
|
545
|
+
size: Symbol,
|
|
546
|
+
compressed_size: Symbol,
|
|
547
|
+
crc: Symbol,
|
|
548
|
+
compression_method: Symbol,
|
|
549
|
+
flags: Symbol,
|
|
550
|
+
external_attributes: Symbol,
|
|
551
|
+
fstype: Symbol,
|
|
552
|
+
version_made_by: Symbol,
|
|
553
|
+
dos_date: Symbol,
|
|
554
|
+
dos_time: Symbol,
|
|
555
|
+
comment: Symbol,
|
|
556
|
+
extra: Symbol,
|
|
557
|
+
header_offset: Symbol,
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
impl MetaKeys {
|
|
561
|
+
fn new(ruby: &Ruby) -> Self {
|
|
562
|
+
Self {
|
|
563
|
+
index: ruby.to_symbol("index"),
|
|
564
|
+
name: ruby.to_symbol("name"),
|
|
565
|
+
size: ruby.to_symbol("size"),
|
|
566
|
+
compressed_size: ruby.to_symbol("compressed_size"),
|
|
567
|
+
crc: ruby.to_symbol("crc"),
|
|
568
|
+
compression_method: ruby.to_symbol("compression_method"),
|
|
569
|
+
flags: ruby.to_symbol("flags"),
|
|
570
|
+
external_attributes: ruby.to_symbol("external_attributes"),
|
|
571
|
+
fstype: ruby.to_symbol("fstype"),
|
|
572
|
+
version_made_by: ruby.to_symbol("version_made_by"),
|
|
573
|
+
dos_date: ruby.to_symbol("dos_date"),
|
|
574
|
+
dos_time: ruby.to_symbol("dos_time"),
|
|
575
|
+
comment: ruby.to_symbol("comment"),
|
|
576
|
+
extra: ruby.to_symbol("extra"),
|
|
577
|
+
header_offset: ruby.to_symbol("header_offset"),
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
fn entry_hash(
|
|
583
|
+
ruby: &Ruby,
|
|
584
|
+
keys: &MetaKeys,
|
|
585
|
+
index: usize,
|
|
586
|
+
entry: &ZipFileEntry<'_>,
|
|
587
|
+
raw: &RawCentralRecord,
|
|
588
|
+
) -> Result<RHash, Error> {
|
|
589
|
+
let h = ruby.hash_new();
|
|
590
|
+
h.aset(keys.index, index)?;
|
|
591
|
+
h.aset(keys.name, ruby.str_from_slice(entry.name_raw()))?;
|
|
592
|
+
h.aset(keys.size, entry.size())?;
|
|
593
|
+
h.aset(keys.compressed_size, entry.compressed_size())?;
|
|
594
|
+
h.aset(keys.crc, entry.crc32())?;
|
|
595
|
+
h.aset(
|
|
596
|
+
keys.compression_method,
|
|
597
|
+
compression_code(entry.compression()),
|
|
598
|
+
)?;
|
|
599
|
+
h.aset(keys.flags, entry.flags().as_u16())?;
|
|
600
|
+
h.aset(keys.external_attributes, entry.external_attributes())?;
|
|
601
|
+
// "Version made by": high byte is the host system, low byte the spec
|
|
602
|
+
// version; rubyzip keeps them as fstype / version.
|
|
603
|
+
h.aset(keys.fstype, u8::from(entry.system()))?;
|
|
604
|
+
let (major, minor) = entry.version_made_by();
|
|
605
|
+
h.aset(keys.version_made_by, major * 10 + minor)?;
|
|
606
|
+
h.aset(keys.dos_date, raw.dos_date)?;
|
|
607
|
+
h.aset(keys.dos_time, raw.dos_time)?;
|
|
608
|
+
h.aset(
|
|
609
|
+
keys.comment,
|
|
610
|
+
ruby.str_from_slice(entry.comment().as_bytes()),
|
|
611
|
+
)?;
|
|
612
|
+
h.aset(keys.extra, ruby.str_from_slice(&raw.extra))?;
|
|
613
|
+
h.aset(keys.header_offset, entry.header_start())?;
|
|
614
|
+
Ok(h)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/// `entry_hash` from the raw record alone, for an archive the crate's
|
|
618
|
+
/// parsed view does not cover; `index` is nil for a record it dropped.
|
|
619
|
+
fn record_hash(
|
|
620
|
+
ruby: &Ruby,
|
|
621
|
+
keys: &MetaKeys,
|
|
622
|
+
index: Option<usize>,
|
|
623
|
+
record: &RawCentralRecord,
|
|
624
|
+
archive_offset: u64,
|
|
625
|
+
) -> Result<RHash, Error> {
|
|
626
|
+
let h = ruby.hash_new();
|
|
627
|
+
h.aset(keys.index, index)?;
|
|
628
|
+
h.aset(keys.name, ruby.str_from_slice(&record.name))?;
|
|
629
|
+
h.aset(keys.size, record.size)?;
|
|
630
|
+
h.aset(keys.compressed_size, record.compressed_size)?;
|
|
631
|
+
h.aset(keys.crc, record.crc32)?;
|
|
632
|
+
h.aset(keys.compression_method, record.method)?;
|
|
633
|
+
h.aset(keys.flags, record.flags)?;
|
|
634
|
+
h.aset(keys.external_attributes, record.external_attributes)?;
|
|
635
|
+
h.aset(keys.fstype, (record.version_made_by >> 8) as u8)?;
|
|
636
|
+
h.aset(keys.version_made_by, record.version_made_by & 0xFF)?;
|
|
637
|
+
h.aset(keys.dos_date, record.dos_date)?;
|
|
638
|
+
h.aset(keys.dos_time, record.dos_time)?;
|
|
639
|
+
h.aset(keys.comment, ruby.str_from_slice(&record.comment))?;
|
|
640
|
+
h.aset(keys.extra, ruby.str_from_slice(&record.extra))?;
|
|
641
|
+
h.aset(
|
|
642
|
+
keys.header_offset,
|
|
643
|
+
record.header_offset.saturating_add(archive_offset),
|
|
644
|
+
)?;
|
|
645
|
+
Ok(h)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/// The on-disk compression method code, as rubyzip exposes it.
|
|
649
|
+
///
|
|
650
|
+
/// `to_u16` is deprecated in favour of matching on constants, but it is the
|
|
651
|
+
/// only public way to recover the raw code, which is what we hand to Ruby.
|
|
652
|
+
#[allow(deprecated)]
|
|
653
|
+
fn compression_code(method: zip::CompressionMethod) -> u16 {
|
|
654
|
+
method.to_u16()
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
#[cfg(test)]
|
|
658
|
+
mod tests {
|
|
659
|
+
use super::*;
|
|
660
|
+
|
|
661
|
+
fn record(
|
|
662
|
+
size: u64,
|
|
663
|
+
compressed_size: u64,
|
|
664
|
+
header_offset: u64,
|
|
665
|
+
extra: Vec<u8>,
|
|
666
|
+
) -> RawCentralRecord {
|
|
667
|
+
RawCentralRecord {
|
|
668
|
+
central_offset: 0,
|
|
669
|
+
version_made_by: 0,
|
|
670
|
+
flags: 0,
|
|
671
|
+
method: 0,
|
|
672
|
+
dos_time: 0,
|
|
673
|
+
dos_date: 0,
|
|
674
|
+
crc32: 0,
|
|
675
|
+
compressed_size,
|
|
676
|
+
size,
|
|
677
|
+
external_attributes: 0,
|
|
678
|
+
header_offset,
|
|
679
|
+
name: Vec::new(),
|
|
680
|
+
extra,
|
|
681
|
+
comment: Vec::new(),
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
fn zip64(values: &[u64]) -> Vec<u8> {
|
|
686
|
+
let mut extra = vec![0x01, 0x00, (values.len() * 8) as u8, 0x00];
|
|
687
|
+
for v in values {
|
|
688
|
+
extra.extend_from_slice(&v.to_le_bytes());
|
|
689
|
+
}
|
|
690
|
+
extra
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
#[test]
|
|
694
|
+
fn zip64_fills_only_the_saturated_fields_in_order() {
|
|
695
|
+
let max = u64::from(u32::MAX);
|
|
696
|
+
let mut r = record(max, 7, max, zip64(&[1 << 33, 1 << 40]));
|
|
697
|
+
r.apply_zip64();
|
|
698
|
+
assert_eq!(
|
|
699
|
+
(r.size, r.compressed_size, r.header_offset),
|
|
700
|
+
(1 << 33, 7, 1 << 40)
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
#[test]
|
|
705
|
+
fn zip64_is_found_after_other_fields_and_tolerates_short_data() {
|
|
706
|
+
let max = u64::from(u32::MAX);
|
|
707
|
+
let mut extra = vec![0x55, 0x54, 0x01, 0x00, 0x03]; // an extended timestamp first
|
|
708
|
+
extra.extend(zip64(&[42]));
|
|
709
|
+
let mut r = record(max, max, 5, extra);
|
|
710
|
+
r.apply_zip64();
|
|
711
|
+
assert_eq!((r.size, r.compressed_size, r.header_offset), (42, max, 5));
|
|
712
|
+
}
|
|
713
|
+
}
|