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,296 @@
|
|
|
1
|
+
//! Where an archive's bytes come from.
|
|
2
|
+
//!
|
|
3
|
+
//! Chosen once at open time. Every variant is cheaply `Clone`: an entry
|
|
4
|
+
//! reader takes its own clone of the `ZipArchive` (the parsed central
|
|
5
|
+
//! directory is behind an `Arc`, so only the reader is duplicated) and
|
|
6
|
+
//! therefore needs an independent cursor over the same bytes.
|
|
7
|
+
//!
|
|
8
|
+
//! `File` is the fast path (pure Rust I/O, runs without the GVL); `Memory`
|
|
9
|
+
//! backs a String buffer; `Ruby` drives any seekable IO-like object through
|
|
10
|
+
//! `seek`/`read` calls, so `open_buffer` on a large Tempfile does not have
|
|
11
|
+
//! to be slurped into memory first.
|
|
12
|
+
|
|
13
|
+
use std::fmt;
|
|
14
|
+
use std::fs;
|
|
15
|
+
use std::io::{self, BufReader, Cursor, Read, Seek, SeekFrom};
|
|
16
|
+
use std::sync::Arc;
|
|
17
|
+
|
|
18
|
+
use magnus::{prelude::*, value::Opaque, Error, RString, Ruby, Value};
|
|
19
|
+
|
|
20
|
+
const BUF_SIZE: usize = 64 * 1024;
|
|
21
|
+
|
|
22
|
+
pub enum Source {
|
|
23
|
+
File(BufReader<SharedFile>),
|
|
24
|
+
Memory(Cursor<Arc<[u8]>>),
|
|
25
|
+
Ruby(BufReader<RubyIo>),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
impl Source {
|
|
29
|
+
pub fn file(file: fs::File) -> Self {
|
|
30
|
+
Source::File(BufReader::with_capacity(BUF_SIZE, SharedFile::new(file)))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub fn memory(bytes: Vec<u8>) -> Self {
|
|
34
|
+
Source::Memory(Cursor::new(Arc::from(bytes)))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
pub fn ruby(io: Value) -> Result<Self, Error> {
|
|
38
|
+
Ok(Source::Ruby(BufReader::with_capacity(
|
|
39
|
+
BUF_SIZE,
|
|
40
|
+
RubyIo::new(io)?,
|
|
41
|
+
)))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Reads from a Ruby IO call back into the interpreter, so anything
|
|
45
|
+
/// over this source must keep the GVL; the other sources can drop it.
|
|
46
|
+
pub fn needs_gvl(&self) -> bool {
|
|
47
|
+
matches!(self, Source::Ruby(_))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// The Ruby IO behind a `Ruby` source, for the owner to mark.
|
|
51
|
+
pub fn ruby_io(&self) -> Option<Opaque<Value>> {
|
|
52
|
+
match self {
|
|
53
|
+
Source::Ruby(r) => Some(r.get_ref().io),
|
|
54
|
+
_ => None,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Moves to `pos` without discarding the read-ahead buffer when `pos`
|
|
59
|
+
/// is inside it. Walking the central directory record by record this
|
|
60
|
+
/// way costs one buffer fill per 64 KiB instead of one per entry.
|
|
61
|
+
pub fn seek_to(&mut self, pos: u64) -> io::Result<()> {
|
|
62
|
+
fn within<R: Read + Seek>(r: &mut BufReader<R>, pos: u64) -> io::Result<()> {
|
|
63
|
+
let current = r.stream_position()?;
|
|
64
|
+
match i64::try_from(pos)
|
|
65
|
+
.ok()
|
|
66
|
+
.and_then(|p| p.checked_sub(i64::try_from(current).ok()?))
|
|
67
|
+
{
|
|
68
|
+
Some(delta) => r.seek_relative(delta),
|
|
69
|
+
None => r.seek(SeekFrom::Start(pos)).map(drop),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
match self {
|
|
73
|
+
Source::File(r) => within(r, pos),
|
|
74
|
+
Source::Ruby(r) => within(r, pos),
|
|
75
|
+
Source::Memory(c) => {
|
|
76
|
+
c.set_position(pos);
|
|
77
|
+
Ok(())
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
impl Clone for Source {
|
|
84
|
+
fn clone(&self) -> Self {
|
|
85
|
+
// Position the clone where the logical stream is, not where the
|
|
86
|
+
// buffered reader has read ahead to.
|
|
87
|
+
match self {
|
|
88
|
+
Source::File(r) => {
|
|
89
|
+
let inner = r.get_ref();
|
|
90
|
+
Source::File(BufReader::with_capacity(
|
|
91
|
+
BUF_SIZE,
|
|
92
|
+
SharedFile {
|
|
93
|
+
file: Arc::clone(&inner.file),
|
|
94
|
+
pos: inner.pos - r.buffer().len() as u64,
|
|
95
|
+
},
|
|
96
|
+
))
|
|
97
|
+
}
|
|
98
|
+
Source::Memory(c) => Source::Memory(c.clone()),
|
|
99
|
+
Source::Ruby(r) => {
|
|
100
|
+
let inner = r.get_ref();
|
|
101
|
+
Source::Ruby(BufReader::with_capacity(
|
|
102
|
+
BUF_SIZE,
|
|
103
|
+
RubyIo {
|
|
104
|
+
io: inner.io,
|
|
105
|
+
pos: inner.pos - r.buffer().len() as u64,
|
|
106
|
+
len: inner.len,
|
|
107
|
+
},
|
|
108
|
+
))
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
impl Read for Source {
|
|
115
|
+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
116
|
+
match self {
|
|
117
|
+
Source::File(f) => f.read(buf),
|
|
118
|
+
Source::Memory(c) => c.read(buf),
|
|
119
|
+
Source::Ruby(r) => r.read(buf),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
impl Seek for Source {
|
|
125
|
+
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
|
126
|
+
match self {
|
|
127
|
+
Source::File(f) => f.seek(pos),
|
|
128
|
+
Source::Memory(c) => c.seek(pos),
|
|
129
|
+
Source::Ruby(r) => r.seek(pos),
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/// One OS file handle shared between cursors, each with its own position.
|
|
135
|
+
/// Reads are positional (`pread`/`ReadFile` with an offset), so clones never
|
|
136
|
+
/// disturb each other and no file is ever reopened by path.
|
|
137
|
+
#[derive(Clone)]
|
|
138
|
+
pub struct SharedFile {
|
|
139
|
+
file: Arc<fs::File>,
|
|
140
|
+
pos: u64,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
impl SharedFile {
|
|
144
|
+
fn new(file: fs::File) -> Self {
|
|
145
|
+
Self {
|
|
146
|
+
file: Arc::new(file),
|
|
147
|
+
pos: 0,
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
impl Read for SharedFile {
|
|
153
|
+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
154
|
+
let n = read_at(&self.file, buf, self.pos)?;
|
|
155
|
+
self.pos += n as u64;
|
|
156
|
+
Ok(n)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
impl Seek for SharedFile {
|
|
161
|
+
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
|
162
|
+
let end = || self.file.metadata().map(|m| m.len());
|
|
163
|
+
self.pos = resolve_seek(self.pos, end, pos)?;
|
|
164
|
+
Ok(self.pos)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// A Ruby IO-like object (anything with `seek`, `read` and `tell`), driven
|
|
169
|
+
/// through method calls under the GVL. One object is shared by all cursors
|
|
170
|
+
/// over it, each of which seeks before it reads, so the object's own
|
|
171
|
+
/// position is meaningless between calls (rubyzip moves it around too).
|
|
172
|
+
///
|
|
173
|
+
/// The object is kept alive by the `mark` function of whatever wraps the
|
|
174
|
+
/// source (an `Archive` or `EntryReader`), which gets it from
|
|
175
|
+
/// [`Source::ruby_io`]; nothing here registers a GC root, so dropping a
|
|
176
|
+
/// source never calls into Ruby and the wrappers can be freed immediately.
|
|
177
|
+
pub struct RubyIo {
|
|
178
|
+
io: Opaque<Value>,
|
|
179
|
+
pos: u64,
|
|
180
|
+
len: u64,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const SEEK_SET: i64 = 0;
|
|
184
|
+
const SEEK_END: i64 = 2;
|
|
185
|
+
|
|
186
|
+
impl RubyIo {
|
|
187
|
+
fn new(io: Value) -> Result<Self, Error> {
|
|
188
|
+
io.funcall::<_, _, Value>("seek", (0i64, SEEK_END))?;
|
|
189
|
+
let len: u64 = io.funcall("tell", ())?;
|
|
190
|
+
Ok(Self {
|
|
191
|
+
io: Opaque::from(io),
|
|
192
|
+
pos: 0,
|
|
193
|
+
len,
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// The IO, usable only on a Ruby thread holding the GVL, which every
|
|
198
|
+
/// read over a Ruby-backed source is.
|
|
199
|
+
fn value(&self) -> io::Result<Value> {
|
|
200
|
+
let ruby = Ruby::get().map_err(|e| io::Error::other(e.to_string()))?;
|
|
201
|
+
Ok(ruby.get_inner(self.io))
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// A Ruby exception (or non-local jump: `throw`, `Timeout`) raised inside
|
|
206
|
+
/// the IO's `read`, `write`, `seek` or `flush`, carried through the Rust layers as an
|
|
207
|
+
/// `io::Error` so that `errors::io_error` can re-raise the original.
|
|
208
|
+
pub struct RubyError(pub Error);
|
|
209
|
+
|
|
210
|
+
// SAFETY: created and consumed only under the GVL (a Ruby-backed source
|
|
211
|
+
// or sink never runs with it released), so the `Value` inside is never touched
|
|
212
|
+
// from another thread; the bounds are only there to fit in `io::Error`.
|
|
213
|
+
unsafe impl Send for RubyError {}
|
|
214
|
+
unsafe impl Sync for RubyError {}
|
|
215
|
+
|
|
216
|
+
impl fmt::Debug for RubyError {
|
|
217
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
218
|
+
fmt::Debug::fmt(&self.0, f)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
impl fmt::Display for RubyError {
|
|
223
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
224
|
+
fmt::Display::fmt(&self.0, f)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
impl std::error::Error for RubyError {}
|
|
229
|
+
|
|
230
|
+
pub(crate) fn ruby_io_error(err: Error) -> io::Error {
|
|
231
|
+
io::Error::other(RubyError(err))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
impl Read for RubyIo {
|
|
235
|
+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
236
|
+
if buf.is_empty() || self.pos >= self.len {
|
|
237
|
+
return Ok(0);
|
|
238
|
+
}
|
|
239
|
+
let io = self.value()?;
|
|
240
|
+
io.funcall::<_, _, Value>("seek", (self.pos as i64, SEEK_SET))
|
|
241
|
+
.map_err(ruby_io_error)?;
|
|
242
|
+
let chunk: Option<RString> = io.funcall("read", (buf.len(),)).map_err(ruby_io_error)?;
|
|
243
|
+
let Some(chunk) = chunk else {
|
|
244
|
+
return Ok(0);
|
|
245
|
+
};
|
|
246
|
+
// SAFETY: the slice is copied out before any other Ruby call.
|
|
247
|
+
let bytes = unsafe { chunk.as_slice() };
|
|
248
|
+
let n = bytes.len().min(buf.len());
|
|
249
|
+
buf[..n].copy_from_slice(&bytes[..n]);
|
|
250
|
+
self.pos += n as u64;
|
|
251
|
+
Ok(n)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
impl Seek for RubyIo {
|
|
256
|
+
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
|
257
|
+
let len = self.len;
|
|
258
|
+
self.pos = resolve_seek(self.pos, || Ok(len), pos)?;
|
|
259
|
+
Ok(self.pos)
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// The absolute position `pos` names, given the current one and a way to
|
|
264
|
+
/// find the end.
|
|
265
|
+
fn resolve_seek(
|
|
266
|
+
current: u64,
|
|
267
|
+
end: impl FnOnce() -> io::Result<u64>,
|
|
268
|
+
pos: SeekFrom,
|
|
269
|
+
) -> io::Result<u64> {
|
|
270
|
+
let (base, offset) = match pos {
|
|
271
|
+
SeekFrom::Start(p) => return Ok(p),
|
|
272
|
+
SeekFrom::Current(o) => (current as i64, o),
|
|
273
|
+
SeekFrom::End(o) => (end()? as i64, o),
|
|
274
|
+
};
|
|
275
|
+
base.checked_add(offset)
|
|
276
|
+
.filter(|t| *t >= 0)
|
|
277
|
+
.map(|t| t as u64)
|
|
278
|
+
.ok_or_else(|| {
|
|
279
|
+
io::Error::new(
|
|
280
|
+
io::ErrorKind::InvalidInput,
|
|
281
|
+
"invalid seek to a negative or overflowing position",
|
|
282
|
+
)
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#[cfg(unix)]
|
|
287
|
+
fn read_at(file: &fs::File, buf: &mut [u8], pos: u64) -> io::Result<usize> {
|
|
288
|
+
use std::os::unix::fs::FileExt;
|
|
289
|
+
file.read_at(buf, pos)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
#[cfg(windows)]
|
|
293
|
+
fn read_at(file: &fs::File, buf: &mut [u8], pos: u64) -> io::Result<usize> {
|
|
294
|
+
use std::os::windows::fs::FileExt;
|
|
295
|
+
file.seek_read(buf, pos)
|
|
296
|
+
}
|