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,916 @@
|
|
|
1
|
+
//! `Ziperu::Native::Writer`: writes a new archive entry by entry.
|
|
2
|
+
//!
|
|
3
|
+
//! The Ruby side (`Ziperu::OutputStream`) owns entry objects, defaults and the
|
|
4
|
+
//! IO-like methods; this is the thin layer over `zip::ZipWriter`. The sink
|
|
5
|
+
//! is a file on disk, a seekable Ruby IO driven through method calls (how
|
|
6
|
+
//! `write_buffer(io)` streams entry by entry into a StringIO, File or
|
|
7
|
+
//! Tempfile), or an in-memory buffer whose bytes `finish` returns, for an
|
|
8
|
+
//! IO that cannot seek.
|
|
9
|
+
|
|
10
|
+
use std::cell::RefCell;
|
|
11
|
+
use std::fs;
|
|
12
|
+
use std::io::{self, BufWriter, Cursor, Read, Seek, SeekFrom, Write};
|
|
13
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
14
|
+
use std::sync::Arc;
|
|
15
|
+
|
|
16
|
+
use magnus::{
|
|
17
|
+
gc::Marker, prelude::*, typed_data::DataTypeFunctions, value::Opaque, Error, RArray, RHash,
|
|
18
|
+
RString, Ruby, TypedData, Value,
|
|
19
|
+
};
|
|
20
|
+
use zip::result::{ZipError, ZipResult};
|
|
21
|
+
use zip::write::FullFileOptions;
|
|
22
|
+
use zip::{AesMode, CompressionMethod, DateTime, System, ZipArchive, ZipWriter};
|
|
23
|
+
|
|
24
|
+
use crate::archive::Archive;
|
|
25
|
+
use crate::errors::{busy_error, io_error, zip_error, ERROR};
|
|
26
|
+
use crate::gvl::maybe_without_gvl;
|
|
27
|
+
use crate::source::ruby_io_error;
|
|
28
|
+
use crate::zipcrypto;
|
|
29
|
+
|
|
30
|
+
const BUF_SIZE: usize = 64 * 1024;
|
|
31
|
+
|
|
32
|
+
pub enum Sink {
|
|
33
|
+
File(BufWriter<fs::File>),
|
|
34
|
+
Memory(Cursor<Vec<u8>>),
|
|
35
|
+
Ruby(BufWriter<RubyOut>),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
impl Sink {
|
|
39
|
+
/// Sets bit `bit` of the little-endian u16 at `pos`.
|
|
40
|
+
fn set_u16_bit(&mut self, pos: u64, bit: u16) -> io::Result<()> {
|
|
41
|
+
let mut field = [0u8; 2];
|
|
42
|
+
self.seek(SeekFrom::Start(pos))?;
|
|
43
|
+
self.read_exact(&mut field)?;
|
|
44
|
+
let value = u16::from_le_bytes(field) | bit;
|
|
45
|
+
self.seek(SeekFrom::Start(pos))?;
|
|
46
|
+
self.write_all(&value.to_le_bytes())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn write_at(&mut self, pos: u64, bytes: &[u8]) -> io::Result<()> {
|
|
50
|
+
self.seek(SeekFrom::Start(pos))?;
|
|
51
|
+
self.write_all(bytes)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Applies a header patch to the local header at `local` and the
|
|
55
|
+
/// central record at `central`.
|
|
56
|
+
fn apply(&mut self, patch: &Patch, local: u64, central: u64) -> io::Result<()> {
|
|
57
|
+
self.set_u16_bit(local + 6, patch.flags)?;
|
|
58
|
+
self.set_u16_bit(central + 8, patch.flags)?;
|
|
59
|
+
if let Some((method, crc, size)) = patch.fields {
|
|
60
|
+
let size = u32::try_from(size).unwrap_or(u32::MAX);
|
|
61
|
+
self.write_at(local + 8, &method.to_le_bytes())?;
|
|
62
|
+
self.write_at(local + 14, &crc.to_le_bytes())?;
|
|
63
|
+
self.write_at(local + 22, &size.to_le_bytes())?;
|
|
64
|
+
self.write_at(central + 10, &method.to_le_bytes())?;
|
|
65
|
+
self.write_at(central + 16, &crc.to_le_bytes())?;
|
|
66
|
+
self.write_at(central + 24, &size.to_le_bytes())?;
|
|
67
|
+
}
|
|
68
|
+
Ok(())
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Reading back what was written (`finish` does, to collect per-entry
|
|
73
|
+
// facts and patch headers) goes through the file after a flush.
|
|
74
|
+
impl Read for Sink {
|
|
75
|
+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
76
|
+
match self {
|
|
77
|
+
Sink::File(f) => {
|
|
78
|
+
f.flush()?;
|
|
79
|
+
f.get_mut().read(buf)
|
|
80
|
+
}
|
|
81
|
+
Sink::Memory(c) => c.read(buf),
|
|
82
|
+
Sink::Ruby(w) => {
|
|
83
|
+
w.flush()?;
|
|
84
|
+
w.get_mut().read(buf)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
impl Write for Sink {
|
|
91
|
+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
92
|
+
match self {
|
|
93
|
+
Sink::File(f) => f.write(buf),
|
|
94
|
+
Sink::Memory(c) => c.write(buf),
|
|
95
|
+
Sink::Ruby(w) => w.write(buf),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fn flush(&mut self) -> io::Result<()> {
|
|
100
|
+
match self {
|
|
101
|
+
Sink::File(f) => f.flush(),
|
|
102
|
+
Sink::Memory(c) => c.flush(),
|
|
103
|
+
Sink::Ruby(w) => w.flush(),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
impl Seek for Sink {
|
|
109
|
+
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
|
110
|
+
match self {
|
|
111
|
+
Sink::File(f) => f.seek(pos),
|
|
112
|
+
Sink::Memory(c) => c.seek(pos),
|
|
113
|
+
Sink::Ruby(w) => w.seek(pos),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// A seekable Ruby IO-like object (`write`, `seek`, `tell`, `read` and
|
|
119
|
+
/// optionally `flush`) written through method calls under the GVL. The
|
|
120
|
+
/// writer owns the object while open, so unlike `source::RubyIo` it trusts
|
|
121
|
+
/// the object's own position and only seeks when the two have drifted
|
|
122
|
+
/// apart (every entry ends with a seek back to patch its local header).
|
|
123
|
+
///
|
|
124
|
+
/// The object is kept alive by the `Writer`'s `mark`; nothing here holds a
|
|
125
|
+
/// GC root. `ZipWriter`'s own `Drop` finalizes an archive that was never
|
|
126
|
+
/// finished, which would call into Ruby from the GC's free (where no
|
|
127
|
+
/// object may be allocated): the `Writer` clears `attached` first, after
|
|
128
|
+
/// which the sink swallows everything and touches no Ruby object.
|
|
129
|
+
pub struct RubyOut {
|
|
130
|
+
io: Opaque<Value>,
|
|
131
|
+
attached: Arc<AtomicBool>,
|
|
132
|
+
/// Where the next write or read goes.
|
|
133
|
+
pos: u64,
|
|
134
|
+
/// Where the object's own position is believed to be.
|
|
135
|
+
io_pos: u64,
|
|
136
|
+
/// One past the furthest byte written: the archive's end.
|
|
137
|
+
end: u64,
|
|
138
|
+
flushable: bool,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const SEEK_SET: i64 = 0;
|
|
142
|
+
const SEEK_END: i64 = 2;
|
|
143
|
+
|
|
144
|
+
impl RubyOut {
|
|
145
|
+
fn new(io: Value, attached: Arc<AtomicBool>) -> Result<Self, Error> {
|
|
146
|
+
let io_pos: u64 = io.funcall("tell", ())?;
|
|
147
|
+
Ok(Self {
|
|
148
|
+
flushable: io.respond_to("flush", false)?,
|
|
149
|
+
io: Opaque::from(io),
|
|
150
|
+
attached,
|
|
151
|
+
pos: 0,
|
|
152
|
+
io_pos,
|
|
153
|
+
end: 0,
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// The IO, usable only on a Ruby thread holding the GVL, which every
|
|
158
|
+
/// operation on a Ruby-backed sink is; `None` once detached.
|
|
159
|
+
fn value(&self) -> io::Result<Option<Value>> {
|
|
160
|
+
if !self.attached.load(Ordering::Acquire) {
|
|
161
|
+
return Ok(None);
|
|
162
|
+
}
|
|
163
|
+
let ruby = Ruby::get().map_err(|e| io::Error::other(e.to_string()))?;
|
|
164
|
+
Ok(Some(ruby.get_inner(self.io)))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Moves the object to `pos` if it is not there already.
|
|
168
|
+
fn sync(&mut self) -> io::Result<Option<Value>> {
|
|
169
|
+
let Some(io) = self.value()? else {
|
|
170
|
+
return Ok(None);
|
|
171
|
+
};
|
|
172
|
+
if self.io_pos != self.pos {
|
|
173
|
+
io.funcall::<_, _, Value>("seek", (self.pos as i64, SEEK_SET))
|
|
174
|
+
.map_err(ruby_io_error)?;
|
|
175
|
+
self.io_pos = self.pos;
|
|
176
|
+
}
|
|
177
|
+
Ok(Some(io))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Leaves the object positioned at the end of the archive, where
|
|
181
|
+
/// rubyzip leaves it.
|
|
182
|
+
fn seek_to_end(&mut self) -> io::Result<()> {
|
|
183
|
+
self.pos = self.end;
|
|
184
|
+
self.sync().map(drop)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
impl Write for RubyOut {
|
|
189
|
+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
190
|
+
if buf.is_empty() {
|
|
191
|
+
return Ok(0);
|
|
192
|
+
}
|
|
193
|
+
let n = match self.sync()? {
|
|
194
|
+
Some(io) => {
|
|
195
|
+
let ruby = Ruby::get().map_err(|e| io::Error::other(e.to_string()))?;
|
|
196
|
+
let chunk = ruby.str_from_slice(buf);
|
|
197
|
+
let written: Value = io.funcall("write", (chunk,)).map_err(ruby_io_error)?;
|
|
198
|
+
// `IO#write` returns the byte count; an IO-like object may
|
|
199
|
+
// return anything else, in which case the whole buffer
|
|
200
|
+
// counts as written.
|
|
201
|
+
u64::try_convert(written)
|
|
202
|
+
.ok()
|
|
203
|
+
.map_or(buf.len(), |n| usize::try_from(n).unwrap_or(usize::MAX))
|
|
204
|
+
.min(buf.len())
|
|
205
|
+
}
|
|
206
|
+
None => buf.len(),
|
|
207
|
+
};
|
|
208
|
+
if n == 0 {
|
|
209
|
+
return Err(io::Error::new(
|
|
210
|
+
io::ErrorKind::WriteZero,
|
|
211
|
+
"the IO's write accepted no bytes",
|
|
212
|
+
));
|
|
213
|
+
}
|
|
214
|
+
self.pos += n as u64;
|
|
215
|
+
self.io_pos = self.pos;
|
|
216
|
+
self.end = self.end.max(self.pos);
|
|
217
|
+
Ok(n)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn flush(&mut self) -> io::Result<()> {
|
|
221
|
+
if let (true, Some(io)) = (self.flushable, self.value()?) {
|
|
222
|
+
io.funcall::<_, _, Value>("flush", ())
|
|
223
|
+
.map_err(ruby_io_error)?;
|
|
224
|
+
}
|
|
225
|
+
Ok(())
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
impl Read for RubyOut {
|
|
230
|
+
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
231
|
+
let Some(io) = self.sync()? else {
|
|
232
|
+
return Ok(0);
|
|
233
|
+
};
|
|
234
|
+
if buf.is_empty() {
|
|
235
|
+
return Ok(0);
|
|
236
|
+
}
|
|
237
|
+
let chunk: Option<RString> = io.funcall("read", (buf.len(),)).map_err(ruby_io_error)?;
|
|
238
|
+
let Some(chunk) = chunk else {
|
|
239
|
+
return Ok(0);
|
|
240
|
+
};
|
|
241
|
+
// SAFETY: the slice is copied out before any other Ruby call.
|
|
242
|
+
let bytes = unsafe { chunk.as_slice() };
|
|
243
|
+
let n = bytes.len().min(buf.len());
|
|
244
|
+
buf[..n].copy_from_slice(&bytes[..n]);
|
|
245
|
+
self.pos += n as u64;
|
|
246
|
+
self.io_pos = self.pos;
|
|
247
|
+
Ok(n)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
impl Seek for RubyOut {
|
|
252
|
+
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
|
|
253
|
+
self.pos = match pos {
|
|
254
|
+
SeekFrom::Start(p) => p,
|
|
255
|
+
SeekFrom::Current(o) => offset(self.pos, o)?,
|
|
256
|
+
SeekFrom::End(o) => {
|
|
257
|
+
let len = match self.value()? {
|
|
258
|
+
Some(io) => {
|
|
259
|
+
io.funcall::<_, _, Value>("seek", (0i64, SEEK_END))
|
|
260
|
+
.map_err(ruby_io_error)?;
|
|
261
|
+
let len: u64 = io.funcall("tell", ()).map_err(ruby_io_error)?;
|
|
262
|
+
self.io_pos = len;
|
|
263
|
+
len
|
|
264
|
+
}
|
|
265
|
+
None => self.end,
|
|
266
|
+
};
|
|
267
|
+
offset(len, o)?
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
Ok(self.pos)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
fn offset(base: u64, delta: i64) -> io::Result<u64> {
|
|
275
|
+
(base as i64)
|
|
276
|
+
.checked_add(delta)
|
|
277
|
+
.filter(|t| *t >= 0)
|
|
278
|
+
.map(|t| t as u64)
|
|
279
|
+
.ok_or_else(|| {
|
|
280
|
+
io::Error::new(
|
|
281
|
+
io::ErrorKind::InvalidInput,
|
|
282
|
+
"invalid seek to a negative or overflowing position",
|
|
283
|
+
)
|
|
284
|
+
})
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#[derive(TypedData)]
|
|
288
|
+
#[magnus(class = "Ziperu::Native::Writer", free_immediately, mark)]
|
|
289
|
+
pub struct Writer {
|
|
290
|
+
// A write releases the GVL while borrowed, so a failed borrow means
|
|
291
|
+
// another thread is writing through this object.
|
|
292
|
+
inner: RefCell<State>,
|
|
293
|
+
/// The Ruby IO behind a `Sink::Ruby`, kept alive by `mark`.
|
|
294
|
+
io: Option<Opaque<Value>>,
|
|
295
|
+
/// Cleared when this object is freed, before the `ZipWriter` inside
|
|
296
|
+
/// drops, so that its finalizer cannot reach the IO (see `RubyOut`).
|
|
297
|
+
attached: Option<Arc<AtomicBool>>,
|
|
298
|
+
/// True for a Ruby IO sink, whose every operation must hold the GVL.
|
|
299
|
+
needs_gvl: bool,
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
impl DataTypeFunctions for Writer {
|
|
303
|
+
fn mark(&self, marker: &Marker) {
|
|
304
|
+
if let Some(io) = self.io {
|
|
305
|
+
marker.mark(io);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
impl Drop for Writer {
|
|
311
|
+
fn drop(&mut self) {
|
|
312
|
+
if let Some(attached) = &self.attached {
|
|
313
|
+
attached.store(false, Ordering::Release);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
struct State {
|
|
319
|
+
/// `None` once finished.
|
|
320
|
+
writer: Option<ZipWriter<Sink>>,
|
|
321
|
+
/// Entries started so far; the next one gets this index.
|
|
322
|
+
count: usize,
|
|
323
|
+
/// Header fields to set once the archive is written; see `Patch`.
|
|
324
|
+
patches: Vec<Patch>,
|
|
325
|
+
/// The ZipCrypto entry being written, if the current one is.
|
|
326
|
+
pending: Option<Pending>,
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/// What the crate wrote for an entry is not quite right in two cases, and
|
|
330
|
+
/// `finish` corrects it in both headers afterwards: a raw-copied ZipCrypto
|
|
331
|
+
/// entry loses its encryption flag (the crate cannot write that cipher),
|
|
332
|
+
/// and an entry this writer encrypts itself goes to the crate as a Stored
|
|
333
|
+
/// entry of ciphertext, so its method, CRC and size describe the
|
|
334
|
+
/// plaintext only after the patch.
|
|
335
|
+
struct Patch {
|
|
336
|
+
index: usize,
|
|
337
|
+
/// General purpose flag bits to set.
|
|
338
|
+
flags: u16,
|
|
339
|
+
/// Compression method, CRC-32 and uncompressed size to write.
|
|
340
|
+
fields: Option<(u16, u32, u64)>,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// A ZipCrypto entry in progress: the data is deflated here (if at all),
|
|
344
|
+
/// encrypted, and handed to the crate as the content of a Stored entry.
|
|
345
|
+
struct Pending {
|
|
346
|
+
index: usize,
|
|
347
|
+
keys: zipcrypto::Keys,
|
|
348
|
+
method: u16,
|
|
349
|
+
deflate: Option<flate2::write::DeflateEncoder<Vec<u8>>>,
|
|
350
|
+
crc: crc32fast::Hasher,
|
|
351
|
+
size: u64,
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
impl Pending {
|
|
355
|
+
/// Takes in plaintext and returns the ciphertext to write.
|
|
356
|
+
fn feed(&mut self, mut bytes: Vec<u8>) -> io::Result<Vec<u8>> {
|
|
357
|
+
self.crc.update(&bytes);
|
|
358
|
+
self.size += bytes.len() as u64;
|
|
359
|
+
let mut out = match self.deflate.as_mut() {
|
|
360
|
+
Some(encoder) => {
|
|
361
|
+
encoder.write_all(&bytes)?;
|
|
362
|
+
std::mem::take(encoder.get_mut())
|
|
363
|
+
}
|
|
364
|
+
None => std::mem::take(&mut bytes),
|
|
365
|
+
};
|
|
366
|
+
self.keys.encrypt(&mut out);
|
|
367
|
+
Ok(out)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/// Flushes the compressor; returns the last ciphertext and the patch.
|
|
371
|
+
fn seal(mut self) -> io::Result<(Vec<u8>, Patch)> {
|
|
372
|
+
let mut out = match self.deflate.take() {
|
|
373
|
+
Some(encoder) => encoder.finish()?,
|
|
374
|
+
None => Vec::new(),
|
|
375
|
+
};
|
|
376
|
+
self.keys.encrypt(&mut out);
|
|
377
|
+
let patch = Patch {
|
|
378
|
+
index: self.index,
|
|
379
|
+
// Encrypted, and (bit 3) the check byte is the DOS time's.
|
|
380
|
+
flags: ENCRYPTED_FLAG | DATA_DESCRIPTOR_FLAG,
|
|
381
|
+
fields: Some((self.method, self.crc.finalize(), self.size)),
|
|
382
|
+
};
|
|
383
|
+
Ok((out, patch))
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/// Finishes a ZipCrypto entry in progress, if any, before the writer
|
|
388
|
+
/// moves on to the next entry or the central directory.
|
|
389
|
+
fn seal_pending(state: &mut State, w: &mut ZipWriter<Sink>) -> io::Result<()> {
|
|
390
|
+
if let Some(pending) = state.pending.take() {
|
|
391
|
+
let (last, patch) = pending.seal()?;
|
|
392
|
+
w.write_all(&last)?;
|
|
393
|
+
state.patches.push(patch);
|
|
394
|
+
}
|
|
395
|
+
Ok(())
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/// What `finish` reports about each written entry, in order: local header
|
|
399
|
+
/// offset, size, compressed size, CRC-32.
|
|
400
|
+
type EntryStats = Vec<[u64; 4]>;
|
|
401
|
+
|
|
402
|
+
/// General purpose flag bit 0: the entry is encrypted.
|
|
403
|
+
const ENCRYPTED_FLAG: u16 = 1;
|
|
404
|
+
/// General purpose flag bit 3: sizes may follow the data, and (what
|
|
405
|
+
/// matters here) a ZipCrypto check byte comes from the DOS time.
|
|
406
|
+
const DATA_DESCRIPTOR_FLAG: u16 = 8;
|
|
407
|
+
|
|
408
|
+
/// Per-entry settings, read from the Hash Ruby passes to `start_entry`.
|
|
409
|
+
struct EntryOptions {
|
|
410
|
+
method: u16,
|
|
411
|
+
level: Option<i64>,
|
|
412
|
+
dos_date: u16,
|
|
413
|
+
dos_time: u16,
|
|
414
|
+
unix_perms: Option<u32>,
|
|
415
|
+
/// The "made by" host system byte (rubyzip's fstype), kept from the
|
|
416
|
+
/// entry rather than defaulting to the platform the writer runs on.
|
|
417
|
+
fstype: Option<u8>,
|
|
418
|
+
large_file: bool,
|
|
419
|
+
comment: Option<String>,
|
|
420
|
+
/// Concatenated local-format extra fields: id, len, data.
|
|
421
|
+
extra: Vec<u8>,
|
|
422
|
+
/// The central-directory forms; fields present only here are written
|
|
423
|
+
/// only there (rubyzip keeps unknown fields it was handed that way).
|
|
424
|
+
central_extra: Vec<u8>,
|
|
425
|
+
password: Option<Vec<u8>>,
|
|
426
|
+
aes_mode: Option<u8>,
|
|
427
|
+
/// Encrypt with ZipCrypto (using `password`) rather than AES.
|
|
428
|
+
zipcrypto: bool,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
impl EntryOptions {
|
|
432
|
+
fn from_hash(ruby: &Ruby, h: RHash) -> Result<Self, Error> {
|
|
433
|
+
let get = |key: &str| h.get(ruby.to_symbol(key));
|
|
434
|
+
let opt = |key: &str| -> Result<Option<magnus::Value>, Error> {
|
|
435
|
+
Ok(get(key).filter(|v| !v.is_nil()))
|
|
436
|
+
};
|
|
437
|
+
Ok(Self {
|
|
438
|
+
method: opt("method")?
|
|
439
|
+
.map(u16::try_convert)
|
|
440
|
+
.transpose()?
|
|
441
|
+
.unwrap_or(8),
|
|
442
|
+
level: opt("level")?.map(i64::try_convert).transpose()?,
|
|
443
|
+
dos_date: opt("dos_date")?
|
|
444
|
+
.map(u16::try_convert)
|
|
445
|
+
.transpose()?
|
|
446
|
+
.unwrap_or(0),
|
|
447
|
+
dos_time: opt("dos_time")?
|
|
448
|
+
.map(u16::try_convert)
|
|
449
|
+
.transpose()?
|
|
450
|
+
.unwrap_or(0),
|
|
451
|
+
unix_perms: opt("unix_perms")?.map(u32::try_convert).transpose()?,
|
|
452
|
+
fstype: opt("fstype")?.map(u8::try_convert).transpose()?,
|
|
453
|
+
large_file: opt("large_file")?
|
|
454
|
+
.map(bool::try_convert)
|
|
455
|
+
.transpose()?
|
|
456
|
+
.unwrap_or(false),
|
|
457
|
+
comment: opt("comment")?
|
|
458
|
+
.map(|v| utf8_text(ruby, "entry comment", RString::try_convert(v)?))
|
|
459
|
+
.transpose()?,
|
|
460
|
+
extra: opt("extra")?
|
|
461
|
+
.map(RString::try_convert)
|
|
462
|
+
.transpose()?
|
|
463
|
+
.map(|s| unsafe { s.as_slice() }.to_vec())
|
|
464
|
+
.unwrap_or_default(),
|
|
465
|
+
central_extra: opt("central_extra")?
|
|
466
|
+
.map(RString::try_convert)
|
|
467
|
+
.transpose()?
|
|
468
|
+
.map(|s| unsafe { s.as_slice() }.to_vec())
|
|
469
|
+
.unwrap_or_default(),
|
|
470
|
+
password: opt("password")?
|
|
471
|
+
.map(RString::try_convert)
|
|
472
|
+
.transpose()?
|
|
473
|
+
.map(|s| unsafe { s.as_slice() }.to_vec()),
|
|
474
|
+
aes_mode: opt("aes_mode")?.map(u8::try_convert).transpose()?,
|
|
475
|
+
zipcrypto: opt("zipcrypto")?
|
|
476
|
+
.map(bool::try_convert)
|
|
477
|
+
.transpose()?
|
|
478
|
+
.unwrap_or(false),
|
|
479
|
+
})
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/// The deflate level for data this writer compresses itself.
|
|
483
|
+
fn flate_level(&self) -> flate2::Compression {
|
|
484
|
+
match self.level {
|
|
485
|
+
Some(level @ 0..=9) => flate2::Compression::new(level as u32),
|
|
486
|
+
_ => flate2::Compression::default(),
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
fn to_file_options(&self, ruby: &Ruby) -> Result<FullFileOptions<'_, 'static>, Error> {
|
|
491
|
+
let method = match self.method {
|
|
492
|
+
0 => CompressionMethod::Stored,
|
|
493
|
+
8 => CompressionMethod::Deflated,
|
|
494
|
+
12 if !self.zipcrypto => CompressionMethod::Bzip2,
|
|
495
|
+
93 if !self.zipcrypto => CompressionMethod::Zstd,
|
|
496
|
+
other => {
|
|
497
|
+
return Err(Error::new(
|
|
498
|
+
ruby.exception_arg_error(),
|
|
499
|
+
format!("unsupported compression method for writing: {other}"),
|
|
500
|
+
))
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
// A ZipCrypto entry reaches the crate as Stored ciphertext whose
|
|
504
|
+
// 32-bit header fields are patched later, so no zip64 fields.
|
|
505
|
+
let (method, large_file) = if self.zipcrypto {
|
|
506
|
+
(CompressionMethod::Stored, false)
|
|
507
|
+
} else {
|
|
508
|
+
(method, self.large_file)
|
|
509
|
+
};
|
|
510
|
+
let mut options = FullFileOptions::default()
|
|
511
|
+
.compression_method(method)
|
|
512
|
+
.compression_level(self.level)
|
|
513
|
+
.large_file(large_file)
|
|
514
|
+
.last_modified_time(
|
|
515
|
+
DateTime::try_from_msdos(self.dos_date, self.dos_time)
|
|
516
|
+
.unwrap_or_else(|_| DateTime::default_for_write()),
|
|
517
|
+
);
|
|
518
|
+
if let Some(perms) = self.unix_perms {
|
|
519
|
+
options = options.unix_permissions(perms);
|
|
520
|
+
}
|
|
521
|
+
if let Some(fstype) = self.fstype {
|
|
522
|
+
options = options.system(System::from(fstype));
|
|
523
|
+
}
|
|
524
|
+
if let Some(comment) = &self.comment {
|
|
525
|
+
options = options.with_file_comment(comment.as_str());
|
|
526
|
+
}
|
|
527
|
+
if let (Some(password), false) = (&self.password, self.zipcrypto) {
|
|
528
|
+
let mode = match self.aes_mode {
|
|
529
|
+
Some(1) => AesMode::Aes128,
|
|
530
|
+
Some(2) => AesMode::Aes192,
|
|
531
|
+
_ => AesMode::Aes256,
|
|
532
|
+
};
|
|
533
|
+
options = options.with_aes_encryption_bytes(mode, password);
|
|
534
|
+
}
|
|
535
|
+
let mut local_ids = Vec::new();
|
|
536
|
+
for (id, data) in extra_fields(&self.extra) {
|
|
537
|
+
local_ids.push(id);
|
|
538
|
+
options
|
|
539
|
+
.add_extra_field(id, data, false)
|
|
540
|
+
.map_err(|e| zip_error(ruby, e))?;
|
|
541
|
+
}
|
|
542
|
+
for (id, data) in extra_fields(&self.central_extra) {
|
|
543
|
+
if !local_ids.contains(&id) {
|
|
544
|
+
options
|
|
545
|
+
.add_extra_field(id, data, true)
|
|
546
|
+
.map_err(|e| zip_error(ruby, e))?;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
Ok(options)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
impl Writer {
|
|
554
|
+
pub fn open_path(ruby: &Ruby, path: String) -> Result<Self, Error> {
|
|
555
|
+
// Readable too: `finish` reads the central directory back.
|
|
556
|
+
let file = fs::OpenOptions::new()
|
|
557
|
+
.read(true)
|
|
558
|
+
.write(true)
|
|
559
|
+
.create(true)
|
|
560
|
+
.truncate(true)
|
|
561
|
+
.open(&path)
|
|
562
|
+
.map_err(|e| io_error(ruby, e))?;
|
|
563
|
+
Ok(Self::new(Sink::File(BufWriter::new(file)), None, None))
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
pub fn open_memory() -> Self {
|
|
567
|
+
Self::new(Sink::Memory(Cursor::new(Vec::new())), None, None)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/// Any seekable IO-like Ruby object; the archive is written from its
|
|
571
|
+
/// start, entry by entry, as the stream takes shape.
|
|
572
|
+
pub fn open_io(io: Value) -> Result<Self, Error> {
|
|
573
|
+
let attached = Arc::new(AtomicBool::new(true));
|
|
574
|
+
let out = RubyOut::new(io, Arc::clone(&attached))?;
|
|
575
|
+
Ok(Self::new(
|
|
576
|
+
Sink::Ruby(BufWriter::with_capacity(BUF_SIZE, out)),
|
|
577
|
+
Some(Opaque::from(io)),
|
|
578
|
+
Some(attached),
|
|
579
|
+
))
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
fn new(sink: Sink, io: Option<Opaque<Value>>, attached: Option<Arc<AtomicBool>>) -> Self {
|
|
583
|
+
Self {
|
|
584
|
+
inner: RefCell::new(State {
|
|
585
|
+
writer: Some(ZipWriter::new(sink)),
|
|
586
|
+
count: 0,
|
|
587
|
+
patches: Vec::new(),
|
|
588
|
+
pending: None,
|
|
589
|
+
}),
|
|
590
|
+
io,
|
|
591
|
+
attached,
|
|
592
|
+
needs_gvl: io.is_some(),
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/// Finishes the previous entry and begins a new file entry.
|
|
597
|
+
pub fn start_entry(
|
|
598
|
+
ruby: &Ruby,
|
|
599
|
+
rb_self: &Self,
|
|
600
|
+
name: RString,
|
|
601
|
+
opts: RHash,
|
|
602
|
+
) -> Result<(), Error> {
|
|
603
|
+
let name = unsafe { name.as_slice() }.to_vec();
|
|
604
|
+
let options = EntryOptions::from_hash(ruby, opts)?;
|
|
605
|
+
let file_options = options.to_file_options(ruby)?;
|
|
606
|
+
// Starting an entry flushes the previous one's compressor, which is
|
|
607
|
+
// real work for a large entry; nothing Ruby-owned is touched unless
|
|
608
|
+
// the sink is a Ruby IO.
|
|
609
|
+
rb_self.with_state(ruby, |state, w| {
|
|
610
|
+
maybe_without_gvl(rb_self.needs_gvl, || -> ZipResult<()> {
|
|
611
|
+
seal_pending(state, w)?;
|
|
612
|
+
w.start_file_with_options(file_options.name(&name))?;
|
|
613
|
+
if options.zipcrypto {
|
|
614
|
+
let password = options.password.as_deref().unwrap_or_default();
|
|
615
|
+
let mut keys = zipcrypto::Keys::new(password);
|
|
616
|
+
let header = keys.header((options.dos_time >> 8) as u8)?;
|
|
617
|
+
w.write_all(&header)?;
|
|
618
|
+
let deflate = (options.method == 8).then(|| {
|
|
619
|
+
flate2::write::DeflateEncoder::new(Vec::new(), options.flate_level())
|
|
620
|
+
});
|
|
621
|
+
state.pending = Some(Pending {
|
|
622
|
+
index: state.count,
|
|
623
|
+
keys,
|
|
624
|
+
method: options.method,
|
|
625
|
+
deflate,
|
|
626
|
+
crc: crc32fast::Hasher::new(),
|
|
627
|
+
size: 0,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
Ok(())
|
|
631
|
+
})
|
|
632
|
+
.map_err(|e| entry_error(ruby, &name, e))?;
|
|
633
|
+
state.count += 1;
|
|
634
|
+
Ok(())
|
|
635
|
+
})
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/// Finishes the previous entry and adds a directory entry.
|
|
639
|
+
pub fn add_directory(
|
|
640
|
+
ruby: &Ruby,
|
|
641
|
+
rb_self: &Self,
|
|
642
|
+
name: RString,
|
|
643
|
+
opts: RHash,
|
|
644
|
+
) -> Result<(), Error> {
|
|
645
|
+
let name = utf8_name(ruby, name)?;
|
|
646
|
+
let options = EntryOptions::from_hash(ruby, opts)?;
|
|
647
|
+
let file_options = options.to_file_options(ruby)?;
|
|
648
|
+
rb_self.with_state(ruby, |state, w| {
|
|
649
|
+
maybe_without_gvl(rb_self.needs_gvl, || {
|
|
650
|
+
seal_pending(state, w)?;
|
|
651
|
+
w.add_directory(&name, file_options)
|
|
652
|
+
})
|
|
653
|
+
.map_err(|e| entry_error(ruby, name.as_bytes(), e))?;
|
|
654
|
+
state.count += 1;
|
|
655
|
+
Ok(())
|
|
656
|
+
})
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/// Finishes the previous entry and adds a symlink entry pointing at
|
|
660
|
+
/// `target` (stored as the entry's content, with `S_IFLNK` in the
|
|
661
|
+
/// external attributes).
|
|
662
|
+
pub fn add_symlink(
|
|
663
|
+
ruby: &Ruby,
|
|
664
|
+
rb_self: &Self,
|
|
665
|
+
name: RString,
|
|
666
|
+
target: RString,
|
|
667
|
+
opts: RHash,
|
|
668
|
+
) -> Result<(), Error> {
|
|
669
|
+
let name = utf8_name(ruby, name)?;
|
|
670
|
+
let target = utf8_text(ruby, "symlink target", target)?;
|
|
671
|
+
let options = EntryOptions::from_hash(ruby, opts)?;
|
|
672
|
+
let file_options = options.to_file_options(ruby)?;
|
|
673
|
+
rb_self.with_state(ruby, |state, w| {
|
|
674
|
+
maybe_without_gvl(rb_self.needs_gvl, || {
|
|
675
|
+
seal_pending(state, w)?;
|
|
676
|
+
w.add_symlink(&name, &target, file_options)
|
|
677
|
+
})
|
|
678
|
+
.map_err(|e| entry_error(ruby, name.as_bytes(), e))?;
|
|
679
|
+
state.count += 1;
|
|
680
|
+
Ok(())
|
|
681
|
+
})
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
pub fn write(ruby: &Ruby, rb_self: &Self, data: RString) -> Result<usize, Error> {
|
|
685
|
+
// Copied out of the Ruby heap so compression can run without the
|
|
686
|
+
// GVL (the string could be moved by compaction meanwhile).
|
|
687
|
+
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
688
|
+
let len = bytes.len();
|
|
689
|
+
rb_self.with_state(ruby, |state, w| {
|
|
690
|
+
maybe_without_gvl(rb_self.needs_gvl, || match state.pending.as_mut() {
|
|
691
|
+
Some(pending) => {
|
|
692
|
+
let cipher = pending.feed(bytes)?;
|
|
693
|
+
w.write_all(&cipher)
|
|
694
|
+
}
|
|
695
|
+
None => w.write_all(&bytes),
|
|
696
|
+
})
|
|
697
|
+
.map_err(|e| io_error(ruby, e))?;
|
|
698
|
+
Ok(len)
|
|
699
|
+
})
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/// Copies entry `index` of `archive` without decompressing it, under
|
|
703
|
+
/// `new_name` if given. `touch` is an optional Hash with `dos_date`,
|
|
704
|
+
/// `dos_time` and `unix_perms` to write instead of the source's; the
|
|
705
|
+
/// crate offers that or a rename, not both.
|
|
706
|
+
pub fn raw_copy(
|
|
707
|
+
ruby: &Ruby,
|
|
708
|
+
rb_self: &Self,
|
|
709
|
+
archive: &Archive,
|
|
710
|
+
index: usize,
|
|
711
|
+
new_name: Option<RString>,
|
|
712
|
+
touch: Option<RHash>,
|
|
713
|
+
) -> Result<(), Error> {
|
|
714
|
+
let needs_gvl = archive.needs_gvl() || rb_self.needs_gvl;
|
|
715
|
+
let new_name = new_name.map(|n| utf8_name(ruby, n)).transpose()?;
|
|
716
|
+
// A clone, so the archive's borrow is released before the GVL is.
|
|
717
|
+
let mut source = archive.archive(ruby)?.clone();
|
|
718
|
+
let file = source.by_index_raw(index).map_err(|e| zip_error(ruby, e))?;
|
|
719
|
+
let zipcrypto = file.encrypted() && file.compression() != CompressionMethod::Aes;
|
|
720
|
+
let touch = touch
|
|
721
|
+
.map(|h| EntryOptions::from_hash(ruby, h))
|
|
722
|
+
.transpose()?;
|
|
723
|
+
rb_self.with_state(ruby, |state, w| {
|
|
724
|
+
maybe_without_gvl(needs_gvl, || {
|
|
725
|
+
seal_pending(state, w)?;
|
|
726
|
+
match (new_name, touch) {
|
|
727
|
+
(Some(name), _) => w.raw_copy_file_rename(file, name),
|
|
728
|
+
(None, Some(t)) => {
|
|
729
|
+
let mtime = DateTime::try_from_msdos(t.dos_date, t.dos_time)
|
|
730
|
+
.unwrap_or_else(|_| DateTime::default_for_write());
|
|
731
|
+
w.raw_copy_file_touch(file, mtime, t.unix_perms)
|
|
732
|
+
}
|
|
733
|
+
(None, None) => w.raw_copy_file(file),
|
|
734
|
+
}
|
|
735
|
+
})
|
|
736
|
+
.map_err(|e| zip_error(ruby, e))?;
|
|
737
|
+
if zipcrypto {
|
|
738
|
+
state.patches.push(Patch {
|
|
739
|
+
index: state.count,
|
|
740
|
+
flags: ENCRYPTED_FLAG,
|
|
741
|
+
fields: None,
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
state.count += 1;
|
|
745
|
+
Ok(())
|
|
746
|
+
})
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
pub fn set_comment(ruby: &Ruby, rb_self: &Self, comment: RString) -> Result<(), Error> {
|
|
750
|
+
let bytes = unsafe { comment.as_slice() }.to_vec().into_boxed_slice();
|
|
751
|
+
rb_self.with_writer(ruby, |w| {
|
|
752
|
+
w.set_raw_comment(bytes).map_err(|e| zip_error(ruby, e))
|
|
753
|
+
})
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/// Writes the central directory. Returns `[bytes, stats]`: the archive
|
|
757
|
+
/// bytes for a memory sink (nil for a file or Ruby IO), and one
|
|
758
|
+
/// `[header_offset, size, compressed_size, crc]` per entry in the order
|
|
759
|
+
/// written, which is how Ruby learns what a streamed entry came to.
|
|
760
|
+
pub fn finish(ruby: &Ruby, rb_self: &Self) -> Result<(Option<RString>, RArray), Error> {
|
|
761
|
+
let mut state = rb_self
|
|
762
|
+
.inner
|
|
763
|
+
.try_borrow_mut()
|
|
764
|
+
.map_err(|_| busy_error(ruby, "writer"))?;
|
|
765
|
+
let mut writer = state
|
|
766
|
+
.writer
|
|
767
|
+
.take()
|
|
768
|
+
.ok_or_else(|| Error::new(ruby.exception_io_error(), "closed stream"))?;
|
|
769
|
+
let mut patches = std::mem::take(&mut state.patches);
|
|
770
|
+
let pending = state.pending.take();
|
|
771
|
+
drop(state);
|
|
772
|
+
// Finishes the last entry and writes the central directory, then
|
|
773
|
+
// reads that directory back for the stats and the header patches.
|
|
774
|
+
let needs_gvl = rb_self.needs_gvl;
|
|
775
|
+
let (sink, stats) = maybe_without_gvl(needs_gvl, || -> ZipResult<(Sink, EntryStats)> {
|
|
776
|
+
if let Some(pending) = pending {
|
|
777
|
+
let (last, patch) = pending.seal()?;
|
|
778
|
+
writer.write_all(&last)?;
|
|
779
|
+
patches.push(patch);
|
|
780
|
+
}
|
|
781
|
+
let archive = writer.finish_into_readable()?;
|
|
782
|
+
let meta = archive.metadata();
|
|
783
|
+
let mut stats = (0..meta.len())
|
|
784
|
+
.map(|i| {
|
|
785
|
+
let e = meta.entry(i)?;
|
|
786
|
+
Ok([
|
|
787
|
+
e.header_start(),
|
|
788
|
+
e.size(),
|
|
789
|
+
e.compressed_size(),
|
|
790
|
+
u64::from(e.crc32()),
|
|
791
|
+
])
|
|
792
|
+
})
|
|
793
|
+
.collect::<ZipResult<EntryStats>>()?;
|
|
794
|
+
let mut sink = archive.into_inner();
|
|
795
|
+
if !patches.is_empty() {
|
|
796
|
+
// The writer's own view has no central record offsets (it
|
|
797
|
+
// never read them); parse what was just written to get them.
|
|
798
|
+
let reread = ZipArchive::new(sink)?;
|
|
799
|
+
let meta = reread.metadata();
|
|
800
|
+
let targets = patches
|
|
801
|
+
.iter()
|
|
802
|
+
.map(|p| {
|
|
803
|
+
let e = meta.entry(p.index)?;
|
|
804
|
+
Ok((e.header_start(), e.central_header_start()))
|
|
805
|
+
})
|
|
806
|
+
.collect::<ZipResult<Vec<_>>>()?;
|
|
807
|
+
sink = reread.into_inner();
|
|
808
|
+
for (patch, (local, central)) in patches.iter().zip(targets) {
|
|
809
|
+
sink.apply(patch, local, central)?;
|
|
810
|
+
if let Some((_, crc, size)) = patch.fields {
|
|
811
|
+
if let Some(stat) = stats.get_mut(patch.index) {
|
|
812
|
+
stat[1] = size;
|
|
813
|
+
stat[3] = u64::from(crc);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
sink.flush()?;
|
|
819
|
+
if let Sink::Ruby(w) = &mut sink {
|
|
820
|
+
w.get_mut().seek_to_end()?;
|
|
821
|
+
}
|
|
822
|
+
Ok((sink, stats))
|
|
823
|
+
})
|
|
824
|
+
.map_err(|e| zip_error(ruby, e))?;
|
|
825
|
+
let bytes = match sink {
|
|
826
|
+
Sink::File(_) | Sink::Ruby(_) => None,
|
|
827
|
+
Sink::Memory(c) => Some(ruby.str_from_slice(&c.into_inner())),
|
|
828
|
+
};
|
|
829
|
+
let out = ruby.ary_new_capa(stats.len());
|
|
830
|
+
for entry in stats {
|
|
831
|
+
out.push(entry.to_vec())?;
|
|
832
|
+
}
|
|
833
|
+
Ok((bytes, out))
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
pub fn is_closed(&self) -> bool {
|
|
837
|
+
self.inner.try_borrow().is_ok_and(|s| s.writer.is_none())
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
fn with_writer<T>(
|
|
841
|
+
&self,
|
|
842
|
+
ruby: &Ruby,
|
|
843
|
+
f: impl FnOnce(&mut ZipWriter<Sink>) -> Result<T, Error>,
|
|
844
|
+
) -> Result<T, Error> {
|
|
845
|
+
self.with_state(ruby, |_, w| f(w))
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
fn with_state<T>(
|
|
849
|
+
&self,
|
|
850
|
+
ruby: &Ruby,
|
|
851
|
+
f: impl FnOnce(&mut State, &mut ZipWriter<Sink>) -> Result<T, Error>,
|
|
852
|
+
) -> Result<T, Error> {
|
|
853
|
+
let mut guard = self
|
|
854
|
+
.inner
|
|
855
|
+
.try_borrow_mut()
|
|
856
|
+
.map_err(|_| busy_error(ruby, "writer"))?;
|
|
857
|
+
let state = &mut *guard;
|
|
858
|
+
let mut writer = state
|
|
859
|
+
.writer
|
|
860
|
+
.take()
|
|
861
|
+
.ok_or_else(|| Error::new(ruby.exception_io_error(), "closed stream"))?;
|
|
862
|
+
let result = f(state, &mut writer);
|
|
863
|
+
state.writer = Some(writer);
|
|
864
|
+
result
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/// Walks concatenated extra fields (id, length, data) as `(id, data)`.
|
|
869
|
+
fn extra_fields(bytes: &[u8]) -> impl Iterator<Item = (u16, &[u8])> {
|
|
870
|
+
let mut i = 0;
|
|
871
|
+
std::iter::from_fn(move || {
|
|
872
|
+
if i + 4 > bytes.len() {
|
|
873
|
+
return None;
|
|
874
|
+
}
|
|
875
|
+
let id = u16::from_le_bytes([bytes[i], bytes[i + 1]]);
|
|
876
|
+
let len = u16::from_le_bytes([bytes[i + 2], bytes[i + 3]]) as usize;
|
|
877
|
+
let data = bytes.get(i + 4..i + 4 + len).unwrap_or(&[]);
|
|
878
|
+
i += 4 + len;
|
|
879
|
+
Some((id, data))
|
|
880
|
+
})
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/// The crate takes directory names, rename targets and entry comments as
|
|
884
|
+
/// `str` (file names are raw bytes), so this limit is stated plainly
|
|
885
|
+
/// rather than surfacing as a conversion error.
|
|
886
|
+
fn utf8_name(ruby: &Ruby, name: RString) -> Result<String, Error> {
|
|
887
|
+
utf8_text(ruby, "entry name", name)
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
fn utf8_text(ruby: &Ruby, what: &str, text: RString) -> Result<String, Error> {
|
|
891
|
+
let bytes = unsafe { text.as_slice() };
|
|
892
|
+
String::from_utf8(bytes.to_vec()).map_err(|_| {
|
|
893
|
+
Error::new(
|
|
894
|
+
ruby.get_inner(&ERROR),
|
|
895
|
+
format!(
|
|
896
|
+
"{what} {:?} is not valid UTF-8, which the native writer needs for directory names, renames and comments",
|
|
897
|
+
String::from_utf8_lossy(bytes)
|
|
898
|
+
),
|
|
899
|
+
)
|
|
900
|
+
})
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/// `zip_error`, except that the crate's duplicate-name message (which
|
|
904
|
+
/// shows the name as a Debug dump) is replaced with one naming the entry.
|
|
905
|
+
fn entry_error(ruby: &Ruby, name: &[u8], err: ZipError) -> Error {
|
|
906
|
+
match &err {
|
|
907
|
+
ZipError::InvalidArchive(msg) if msg.starts_with("Duplicate filename") => Error::new(
|
|
908
|
+
ruby.get_inner(&ERROR),
|
|
909
|
+
format!(
|
|
910
|
+
"entry {:?} was already written to this stream",
|
|
911
|
+
String::from_utf8_lossy(name)
|
|
912
|
+
),
|
|
913
|
+
),
|
|
914
|
+
_ => zip_error(ruby, err),
|
|
915
|
+
}
|
|
916
|
+
}
|