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.
Files changed (114) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/Cargo.lock +879 -0
  4. data/Cargo.toml +8 -0
  5. data/LICENSE-rubyzip.txt +24 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +276 -0
  8. data/ext/ziperu/Cargo.toml +35 -0
  9. data/ext/ziperu/build.rs +4 -0
  10. data/ext/ziperu/extconf.rb +6 -0
  11. data/ext/ziperu/src/archive.rs +713 -0
  12. data/ext/ziperu/src/errors.rs +143 -0
  13. data/ext/ziperu/src/extract.rs +505 -0
  14. data/ext/ziperu/src/gvl.rs +64 -0
  15. data/ext/ziperu/src/lib.rs +78 -0
  16. data/ext/ziperu/src/reader.rs +351 -0
  17. data/ext/ziperu/src/source.rs +296 -0
  18. data/ext/ziperu/src/writer.rs +916 -0
  19. data/ext/ziperu/src/zipcrypto.rs +126 -0
  20. data/lib/rubyzip.rb +4 -0
  21. data/lib/zip/central_directory.rb +5 -0
  22. data/lib/zip/compressor.rb +5 -0
  23. data/lib/zip/constants.rb +5 -0
  24. data/lib/zip/crypto/aes_encryption.rb +5 -0
  25. data/lib/zip/crypto/decrypted_io.rb +5 -0
  26. data/lib/zip/crypto/encryption.rb +5 -0
  27. data/lib/zip/crypto/null_encryption.rb +5 -0
  28. data/lib/zip/crypto/traditional_encryption.rb +5 -0
  29. data/lib/zip/crypto.rb +5 -0
  30. data/lib/zip/decompressor.rb +5 -0
  31. data/lib/zip/deflater.rb +5 -0
  32. data/lib/zip/dirtyable.rb +5 -0
  33. data/lib/zip/dos_time.rb +5 -0
  34. data/lib/zip/entry.rb +5 -0
  35. data/lib/zip/entry_set.rb +5 -0
  36. data/lib/zip/errors.rb +5 -0
  37. data/lib/zip/extra_field/aes.rb +5 -0
  38. data/lib/zip/extra_field/generic.rb +5 -0
  39. data/lib/zip/extra_field/ntfs.rb +5 -0
  40. data/lib/zip/extra_field/old_unix.rb +5 -0
  41. data/lib/zip/extra_field/universal_time.rb +5 -0
  42. data/lib/zip/extra_field/unix.rb +5 -0
  43. data/lib/zip/extra_field/unknown.rb +5 -0
  44. data/lib/zip/extra_field/zip64.rb +5 -0
  45. data/lib/zip/extra_field.rb +5 -0
  46. data/lib/zip/file.rb +5 -0
  47. data/lib/zip/file_split.rb +5 -0
  48. data/lib/zip/filesystem/dir.rb +5 -0
  49. data/lib/zip/filesystem/directory_iterator.rb +5 -0
  50. data/lib/zip/filesystem/file.rb +5 -0
  51. data/lib/zip/filesystem/file_stat.rb +5 -0
  52. data/lib/zip/filesystem/zip_file_name_mapper.rb +5 -0
  53. data/lib/zip/filesystem.rb +5 -0
  54. data/lib/zip/inflater.rb +5 -0
  55. data/lib/zip/input_stream.rb +5 -0
  56. data/lib/zip/ioextras/abstract_input_stream.rb +5 -0
  57. data/lib/zip/ioextras/abstract_output_stream.rb +5 -0
  58. data/lib/zip/ioextras/fake_io.rb +5 -0
  59. data/lib/zip/ioextras.rb +5 -0
  60. data/lib/zip/null_compressor.rb +5 -0
  61. data/lib/zip/null_decompressor.rb +5 -0
  62. data/lib/zip/null_input_stream.rb +5 -0
  63. data/lib/zip/output_stream.rb +5 -0
  64. data/lib/zip/pass_thru_compressor.rb +5 -0
  65. data/lib/zip/pass_thru_decompressor.rb +5 -0
  66. data/lib/zip/streamable_directory.rb +5 -0
  67. data/lib/zip/streamable_stream.rb +5 -0
  68. data/lib/zip/version.rb +5 -0
  69. data/lib/zip.rb +19 -0
  70. data/lib/ziperu/central_directory.rb +136 -0
  71. data/lib/ziperu/compressor.rb +14 -0
  72. data/lib/ziperu/constants.rb +148 -0
  73. data/lib/ziperu/crypto.rb +72 -0
  74. data/lib/ziperu/decompressor.rb +35 -0
  75. data/lib/ziperu/deflater.rb +41 -0
  76. data/lib/ziperu/dirtyable.rb +48 -0
  77. data/lib/ziperu/dos_time.rb +87 -0
  78. data/lib/ziperu/entry.rb +760 -0
  79. data/lib/ziperu/entry_set.rb +147 -0
  80. data/lib/ziperu/errors.rb +154 -0
  81. data/lib/ziperu/extra_field/aes.rb +52 -0
  82. data/lib/ziperu/extra_field/generic.rb +45 -0
  83. data/lib/ziperu/extra_field/ntfs.rb +98 -0
  84. data/lib/ziperu/extra_field/old_unix.rb +52 -0
  85. data/lib/ziperu/extra_field/universal_time.rb +81 -0
  86. data/lib/ziperu/extra_field/unix.rb +45 -0
  87. data/lib/ziperu/extra_field/unknown.rb +37 -0
  88. data/lib/ziperu/extra_field/zip64.rb +86 -0
  89. data/lib/ziperu/extra_field.rb +109 -0
  90. data/lib/ziperu/file.rb +614 -0
  91. data/lib/ziperu/file_split.rb +93 -0
  92. data/lib/ziperu/filesystem/dir.rb +88 -0
  93. data/lib/ziperu/filesystem/directory_iterator.rb +50 -0
  94. data/lib/ziperu/filesystem/file.rb +265 -0
  95. data/lib/ziperu/filesystem/file_stat.rb +112 -0
  96. data/lib/ziperu/filesystem/zip_file_name_mapper.rb +83 -0
  97. data/lib/ziperu/filesystem.rb +76 -0
  98. data/lib/ziperu/inflater.rb +60 -0
  99. data/lib/ziperu/input_stream.rb +296 -0
  100. data/lib/ziperu/ioextras/abstract_input_stream.rb +137 -0
  101. data/lib/ziperu/ioextras/abstract_output_stream.rb +57 -0
  102. data/lib/ziperu/ioextras/fake_io.rb +41 -0
  103. data/lib/ziperu/ioextras.rb +42 -0
  104. data/lib/ziperu/null_compressor.rb +20 -0
  105. data/lib/ziperu/null_decompressor.rb +25 -0
  106. data/lib/ziperu/null_input_stream.rb +55 -0
  107. data/lib/ziperu/output_stream.rb +223 -0
  108. data/lib/ziperu/pass_thru_compressor.rb +29 -0
  109. data/lib/ziperu/pass_thru_decompressor.rb +37 -0
  110. data/lib/ziperu/streamable_directory.rb +15 -0
  111. data/lib/ziperu/streamable_stream.rb +59 -0
  112. data/lib/ziperu/version.rb +7 -0
  113. data/lib/ziperu.rb +116 -0
  114. metadata +174 -0
@@ -0,0 +1,64 @@
1
+ //! Running pure-Rust work with the GVL released.
2
+ //!
3
+ //! Inflating a chunk or deflating one takes real CPU time; letting other
4
+ //! Ruby threads run meanwhile is the point of a native core. Only work that
5
+ //! touches no Ruby object may run this way, which here means anything over a
6
+ //! `Source::File`, `Source::Memory` or the writer's file and memory sinks,
7
+ //! never a `Source::Ruby` or `Sink::Ruby` (their reads and writes call
8
+ //! back into the interpreter).
9
+
10
+ use std::ffi::c_void;
11
+ use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
12
+ use std::ptr::null_mut;
13
+ use std::thread::Result as ThreadResult;
14
+
15
+ struct Call<F, R> {
16
+ f: Option<F>,
17
+ out: Option<ThreadResult<R>>,
18
+ }
19
+
20
+ unsafe extern "C" fn trampoline<F: FnOnce() -> R, R>(data: *mut c_void) -> *mut c_void {
21
+ // SAFETY: `data` is the `Call` that `without_gvl` built on its stack and
22
+ // outlives this call.
23
+ let call = unsafe { &mut *(data as *mut Call<F, R>) };
24
+ let f = call.f.take().expect("trampoline runs once");
25
+ // A panic must not unwind through the C frame of the Ruby VM.
26
+ call.out = Some(catch_unwind(AssertUnwindSafe(f)));
27
+ null_mut()
28
+ }
29
+
30
+ /// Runs `f` with the GVL released and returns its result. `f` must not
31
+ /// touch the Ruby heap (no `Value`, no `RString` slice, no `Ruby` handle).
32
+ /// A panic inside `f` is re-raised on this side of the FFI boundary.
33
+ pub fn without_gvl<F: FnOnce() -> R, R>(f: F) -> R {
34
+ let mut call = Call {
35
+ f: Some(f),
36
+ out: None,
37
+ };
38
+ // SAFETY: `call` lives for the whole blocking call. No unblock function
39
+ // is given, so `Thread#kill` or an interrupt waits for `f` to finish:
40
+ // a chunked read returns within one 64 KiB chunk, but `read_entry`
41
+ // and `write` of a large String run to the end of the data first.
42
+ unsafe {
43
+ rb_sys::rb_thread_call_without_gvl(
44
+ Some(trampoline::<F, R>),
45
+ &mut call as *mut Call<F, R> as *mut c_void,
46
+ None,
47
+ null_mut(),
48
+ );
49
+ }
50
+ match call.out.take().expect("trampoline ran") {
51
+ Ok(r) => r,
52
+ Err(panic) => resume_unwind(panic),
53
+ }
54
+ }
55
+
56
+ /// `without_gvl` unless the work needs the interpreter (a Ruby-backed
57
+ /// source), in which case `f` simply runs inline.
58
+ pub fn maybe_without_gvl<F: FnOnce() -> R, R>(needs_gvl: bool, f: F) -> R {
59
+ if needs_gvl {
60
+ f()
61
+ } else {
62
+ without_gvl(f)
63
+ }
64
+ }
@@ -0,0 +1,78 @@
1
+ //! Native core for the `ziperu` gem.
2
+ //!
3
+ //! The Rust side exposes a small set of primitives under `Ziperu::Native`;
4
+ //! everything Ruby-shaped (kwargs, blocks, `Enumerable`, error messages)
5
+ //! lives in `lib/`.
6
+
7
+ mod archive;
8
+ mod errors;
9
+ mod extract;
10
+ mod gvl;
11
+ mod reader;
12
+ mod source;
13
+ mod writer;
14
+ mod zipcrypto;
15
+
16
+ use magnus::{function, method, prelude::*, Error, Ruby};
17
+
18
+ #[magnus::init(name = "ziperu")]
19
+ fn init(ruby: &Ruby) -> Result<(), Error> {
20
+ let zip = ruby.define_module("Ziperu")?;
21
+ let native = zip.define_module("Native")?;
22
+
23
+ let archive = native.define_class("Archive", ruby.class_object())?;
24
+ archive.define_singleton_method("open_path", function!(archive::Archive::open_path, 1))?;
25
+ archive.define_singleton_method("open_bytes", function!(archive::Archive::open_bytes, 1))?;
26
+ archive.define_singleton_method("open_io", function!(archive::Archive::open_io, 1))?;
27
+ archive.define_method("entry_count", method!(archive::Archive::entry_count, 0))?;
28
+ archive.define_method("entries", method!(archive::Archive::entries, 0))?;
29
+ archive.define_method("entry_meta", method!(archive::Archive::entry_meta, 1))?;
30
+ archive.define_method("local_extra", method!(archive::Archive::local_extra, 1))?;
31
+ archive.define_method(
32
+ "local_extra_at",
33
+ method!(archive::Archive::local_extra_at, 1),
34
+ )?;
35
+ archive.define_method(
36
+ "duplicate_names?",
37
+ method!(archive::Archive::has_duplicate_names, 0),
38
+ )?;
39
+ archive.define_method("comment", method!(archive::Archive::comment, 0))?;
40
+ archive.define_method("total_size", method!(archive::Archive::total_size, 0))?;
41
+ archive.define_method("verify_entry", method!(archive::Archive::verify_entry, 2))?;
42
+ archive.define_method(
43
+ "verify_entry_at",
44
+ method!(archive::Archive::verify_entry_at, 5),
45
+ )?;
46
+ archive.define_method("open_entry", method!(archive::Archive::open_entry, 3))?;
47
+ archive.define_method("open_entry_at", method!(archive::Archive::open_entry_at, 6))?;
48
+ archive.define_method("read_entry", method!(archive::Archive::read_entry, 1))?;
49
+ archive.define_method(
50
+ "extract_entries",
51
+ method!(archive::Archive::extract_entries, 3),
52
+ )?;
53
+ archive.define_method("close", method!(archive::Archive::close, 0))?;
54
+ archive.define_method("closed?", method!(archive::Archive::is_closed, 0))?;
55
+
56
+ let reader = native.define_class("EntryReader", ruby.class_object())?;
57
+ reader.define_method("size", method!(reader::EntryReader::size, 0))?;
58
+ reader.define_method("pos", method!(reader::EntryReader::pos, 0))?;
59
+ reader.define_method("eof?", method!(reader::EntryReader::is_eof, 0))?;
60
+ reader.define_method("read", method!(reader::EntryReader::read, 1))?;
61
+ reader.define_method("rewind", method!(reader::EntryReader::rewind, 0))?;
62
+ reader.define_method("close", method!(reader::EntryReader::close, 0))?;
63
+
64
+ let writer = native.define_class("Writer", ruby.class_object())?;
65
+ writer.define_singleton_method("open_path", function!(writer::Writer::open_path, 1))?;
66
+ writer.define_singleton_method("open_memory", function!(writer::Writer::open_memory, 0))?;
67
+ writer.define_singleton_method("open_io", function!(writer::Writer::open_io, 1))?;
68
+ writer.define_method("start_entry", method!(writer::Writer::start_entry, 2))?;
69
+ writer.define_method("add_directory", method!(writer::Writer::add_directory, 2))?;
70
+ writer.define_method("add_symlink", method!(writer::Writer::add_symlink, 3))?;
71
+ writer.define_method("write", method!(writer::Writer::write, 1))?;
72
+ writer.define_method("raw_copy", method!(writer::Writer::raw_copy, 4))?;
73
+ writer.define_method("comment=", method!(writer::Writer::set_comment, 1))?;
74
+ writer.define_method("finish", method!(writer::Writer::finish, 0))?;
75
+ writer.define_method("closed?", method!(writer::Writer::is_closed, 0))?;
76
+
77
+ Ok(())
78
+ }
@@ -0,0 +1,351 @@
1
+ //! `Ziperu::Native::EntryReader`: a chunked, decompressing reader over one entry.
2
+ //!
3
+ //! Returns raw bytes; everything IO-shaped (`gets`, `lineno`, encodings) is
4
+ //! layered on top in `lib/ziperu/input_stream.rb`. With `enforce_size` (the
5
+ //! default) it never inflates more than the declared size, so a lying header
6
+ //! cannot become a zip bomb, and reports a header that lies the other way as
7
+ //! `Ziperu::Native::SizeLimitError`. Without it, it reads to the true end of
8
+ //! the data, which is what rubyzip does when `Ziperu.validate_entry_sizes` is
9
+ //! off.
10
+
11
+ use std::cell::{RefCell, RefMut};
12
+ use std::io::{self, Read, Seek, SeekFrom};
13
+
14
+ use magnus::{
15
+ gc::Marker, typed_data::DataTypeFunctions, value::Opaque, Error, RString, Ruby, TypedData,
16
+ Value,
17
+ };
18
+ use zip::read::{read_zipfile_from_stream_with_options, ZipFile, ZipReadOptions};
19
+ use zip::result::{ZipError, ZipResult};
20
+ use zip::ZipArchive;
21
+
22
+ use crate::errors::{busy_error, read_error, size_limit_error, zip_error};
23
+ use crate::gvl::maybe_without_gvl;
24
+ use crate::source::Source;
25
+
26
+ const CHUNK: usize = 64 * 1024;
27
+ /// Reads shorter than this run under the GVL; see `read`.
28
+ const INLINE_READ: u64 = 16 * 1024;
29
+
30
+ /// What the entry was opened from. The crate's `by_index` readers always cap
31
+ /// output at the central directory's size; lifting that means reading the
32
+ /// local header ourselves through the stream API, which borrows the raw
33
+ /// `Source` instead of the `ZipArchive`.
34
+ enum Owner {
35
+ Archive(*mut ZipArchive<Source>),
36
+ Source(*mut Source),
37
+ }
38
+
39
+ /// A `ZipFile` together with what it borrows from.
40
+ ///
41
+ /// `ZipFile<'a>` borrows its owner mutably; to hand one to Ruby we keep the
42
+ /// owner on the heap behind a raw pointer and give the borrow a `'static`
43
+ /// lifetime that `Drop` makes honest.
44
+ struct OwnedEntry {
45
+ file: Option<ZipFile<'static, Source>>,
46
+ owner: Owner,
47
+ // The reader borrows the password for its own lifetime too; boxed so its
48
+ // address is stable, declared after `file` so it is dropped after it.
49
+ password: Option<Box<[u8]>>,
50
+ }
51
+
52
+ /// Central-directory facts needed to (re)open an entry.
53
+ pub(crate) struct EntryInfo {
54
+ /// The crate's index, or `None` for a record it cannot address (one of
55
+ /// several under the same name), which is read from its local header.
56
+ pub(crate) index: Option<usize>,
57
+ pub(crate) header_start: u64,
58
+ pub(crate) size: u64,
59
+ pub(crate) compressed_size: u64,
60
+ pub(crate) crc32: u32,
61
+ }
62
+
63
+ impl OwnedEntry {
64
+ fn open(
65
+ archive: ZipArchive<Source>,
66
+ enforce_size: bool,
67
+ password: Option<Vec<u8>>,
68
+ info: &EntryInfo,
69
+ ) -> ZipResult<Self> {
70
+ let owner = if enforce_size && info.index.is_some() {
71
+ Owner::Archive(Box::into_raw(Box::new(archive)))
72
+ } else {
73
+ Owner::Source(Box::into_raw(Box::new(archive.into_inner())))
74
+ };
75
+ let mut this = Self {
76
+ file: None,
77
+ owner,
78
+ password: password.map(Vec::into_boxed_slice),
79
+ };
80
+ this.reopen(info)?;
81
+ Ok(this)
82
+ }
83
+
84
+ fn reopen(&mut self, info: &EntryInfo) -> ZipResult<()> {
85
+ self.file = None;
86
+ // SAFETY: the password box and the owner allocation live as long as
87
+ // `self` and are only freed by `Drop`, after `file` has been dropped,
88
+ // so these `'static` borrows never outlive their referents. Nothing
89
+ // else holds a reference to either allocation.
90
+ let password: Option<&'static [u8]> = self
91
+ .password
92
+ .as_deref()
93
+ .map(|p| unsafe { &*(p as *const [u8]) });
94
+ let options = ZipReadOptions::new().password(password);
95
+ let file = match self.owner {
96
+ Owner::Archive(archive) => {
97
+ let archive: &'static mut ZipArchive<Source> = unsafe { &mut *archive };
98
+ let index = info.index.ok_or(ZipError::FileNotFound)?;
99
+ archive.by_index_with_options(index, options)?
100
+ }
101
+ Owner::Source(source) => {
102
+ let source: &'static mut Source = unsafe { &mut *source };
103
+ source.seek(SeekFrom::Start(info.header_start))?;
104
+ // Sizes and CRC from the central directory, so entries whose
105
+ // local header defers to a data descriptor still read; the
106
+ // uncompressed size is set absurdly high to lift the limit.
107
+ let options = options
108
+ .override_compressed_size(info.compressed_size)
109
+ .override_uncompressed_size(u64::MAX)
110
+ .override_crc(info.crc32);
111
+ read_zipfile_from_stream_with_options(source, options)?.ok_or(
112
+ ZipError::InvalidArchive("expected a local file header".into()),
113
+ )?
114
+ }
115
+ };
116
+ self.file = Some(file);
117
+ Ok(())
118
+ }
119
+ }
120
+
121
+ impl Drop for OwnedEntry {
122
+ fn drop(&mut self) {
123
+ self.file = None;
124
+ // SAFETY: created by `Box::into_raw` in `open`; freed exactly once here.
125
+ unsafe {
126
+ match self.owner {
127
+ Owner::Archive(p) => drop(Box::from_raw(p)),
128
+ Owner::Source(p) => drop(Box::from_raw(p)),
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ struct Inner {
135
+ /// `None` once closed, which frees the entry and the owner it borrows
136
+ /// from (the file handle or the Ruby IO root) right away.
137
+ entry: Option<OwnedEntry>,
138
+ info: EntryInfo,
139
+ /// Declared size, or `None` when reading to the true end of the data.
140
+ size: Option<u64>,
141
+ consumed: u64,
142
+ finished: bool,
143
+ /// Reads over a Ruby IO must keep the GVL; the rest release it.
144
+ needs_gvl: bool,
145
+ }
146
+
147
+ #[derive(TypedData)]
148
+ #[magnus(class = "Ziperu::Native::EntryReader", free_immediately, mark)]
149
+ pub struct EntryReader {
150
+ // A read releases the GVL while borrowed, so a failed borrow means
151
+ // another thread is reading through this same object right now.
152
+ inner: RefCell<Inner>,
153
+ /// The Ruby IO behind a `Source::Ruby`, kept alive by `mark`.
154
+ io: Option<Opaque<Value>>,
155
+ }
156
+
157
+ impl DataTypeFunctions for EntryReader {
158
+ fn mark(&self, marker: &Marker) {
159
+ if let Some(io) = self.io {
160
+ marker.mark(io);
161
+ }
162
+ }
163
+ }
164
+
165
+ // SAFETY: the raw pointer in `OwnedEntry` is only dereferenced from methods
166
+ // called while holding the GVL, so the reader is never used concurrently.
167
+ unsafe impl Send for EntryReader {}
168
+
169
+ impl EntryReader {
170
+ pub(crate) fn open(
171
+ ruby: &Ruby,
172
+ archive: ZipArchive<Source>,
173
+ index: usize,
174
+ password: Option<Vec<u8>>,
175
+ enforce_size: bool,
176
+ needs_gvl: bool,
177
+ io: Option<Opaque<Value>>,
178
+ ) -> Result<Self, Error> {
179
+ let info = {
180
+ // From the parsed central directory: no seek to the local
181
+ // header, which the reader is about to do anyway.
182
+ let meta = archive.metadata();
183
+ let raw = meta.entry(index).map_err(|e| zip_error(ruby, e))?;
184
+ EntryInfo {
185
+ index: Some(index),
186
+ header_start: raw.header_start(),
187
+ size: raw.size(),
188
+ compressed_size: raw.compressed_size(),
189
+ crc32: raw.crc32(),
190
+ }
191
+ };
192
+ Self::open_at(ruby, archive, info, password, enforce_size, needs_gvl, io)
193
+ }
194
+
195
+ /// A reader over the entry `info` describes, which need not be one the
196
+ /// crate can address by index.
197
+ pub(crate) fn open_at(
198
+ ruby: &Ruby,
199
+ archive: ZipArchive<Source>,
200
+ info: EntryInfo,
201
+ password: Option<Vec<u8>>,
202
+ enforce_size: bool,
203
+ needs_gvl: bool,
204
+ io: Option<Opaque<Value>>,
205
+ ) -> Result<Self, Error> {
206
+ let entry = OwnedEntry::open(archive, enforce_size, password, &info)
207
+ .map_err(|e| zip_error(ruby, e))?;
208
+ let size = enforce_size.then_some(info.size);
209
+ Ok(Self {
210
+ inner: RefCell::new(Inner {
211
+ entry: Some(entry),
212
+ info,
213
+ size,
214
+ consumed: 0,
215
+ finished: size == Some(0),
216
+ needs_gvl,
217
+ }),
218
+ io,
219
+ })
220
+ }
221
+
222
+ fn inner(&self, ruby: &Ruby) -> Result<RefMut<'_, Inner>, Error> {
223
+ self.inner
224
+ .try_borrow_mut()
225
+ .map_err(|_| busy_error(ruby, "entry reader"))
226
+ }
227
+
228
+ /// The declared size, or nil when not enforcing it.
229
+ pub fn size(ruby: &Ruby, rb_self: &Self) -> Result<Option<u64>, Error> {
230
+ Ok(rb_self.inner(ruby)?.size)
231
+ }
232
+
233
+ pub fn pos(ruby: &Ruby, rb_self: &Self) -> Result<u64, Error> {
234
+ Ok(rb_self.inner(ruby)?.consumed)
235
+ }
236
+
237
+ pub fn is_eof(ruby: &Ruby, rb_self: &Self) -> Result<bool, Error> {
238
+ Ok(rb_self.inner(ruby)?.finished)
239
+ }
240
+
241
+ /// Releases the entry and what it holds open (the file handle, or the
242
+ /// Ruby IO) now rather than at GC time; reads afterwards return nil.
243
+ pub fn close(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
244
+ let mut inner = rb_self.inner(ruby)?;
245
+ inner.entry = None;
246
+ inner.finished = true;
247
+ Ok(())
248
+ }
249
+
250
+ /// Up to `maxlen` bytes (all remaining bytes when nil) as a binary
251
+ /// String, or nil once the entry is exhausted. Short only at EOF.
252
+ pub fn read(
253
+ ruby: &Ruby,
254
+ rb_self: &Self,
255
+ maxlen: Option<usize>,
256
+ ) -> Result<Option<RString>, Error> {
257
+ let mut inner = rb_self.inner(ruby)?;
258
+ if inner.finished {
259
+ return Ok(None);
260
+ }
261
+ let remaining = inner.size.map(|s| s - inner.consumed).unwrap_or(u64::MAX);
262
+ let want = match maxlen {
263
+ Some(n) => (n as u64).min(remaining),
264
+ None => remaining,
265
+ };
266
+
267
+ // This read reaches the declared size: one more byte would mean the
268
+ // header lied, so probe for it.
269
+ let probe = inner.size == Some(inner.consumed + want);
270
+ // Releasing the GVL costs about as much as inflating a few KiB, so
271
+ // tiny reads (`getc`, `each_byte`) keep it.
272
+ let keep_gvl = inner.needs_gvl || want < INLINE_READ;
273
+ let Some(file) = inner.entry.as_mut().and_then(|e| e.file.as_mut()) else {
274
+ // Closed, or a rewind failed to reopen it.
275
+ return Ok(None);
276
+ };
277
+ let (buf, eof, oversize) = maybe_without_gvl(keep_gvl, || fill(file, want, probe))
278
+ .map_err(|e| read_error(ruby, e))?;
279
+
280
+ let filled = buf.len() as u64;
281
+ inner.consumed += filled;
282
+ if eof {
283
+ inner.finished = true;
284
+ }
285
+ if filled == 0 {
286
+ // Shorter than declared; the crate's CRC check normally raises
287
+ // before we get here. Treat as end of stream.
288
+ return Ok(None);
289
+ }
290
+ if oversize {
291
+ return Err(size_limit_error(ruby));
292
+ }
293
+ if probe && filled == want {
294
+ inner.finished = true;
295
+ }
296
+ Ok(Some(ruby.str_from_slice(&buf)))
297
+ }
298
+
299
+ pub fn rewind(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
300
+ let mut inner = rb_self.inner(ruby)?;
301
+ let Inner {
302
+ entry,
303
+ info,
304
+ size,
305
+ consumed,
306
+ finished,
307
+ ..
308
+ } = &mut *inner;
309
+ let Some(entry) = entry.as_mut() else {
310
+ return Err(Error::new(ruby.exception_io_error(), "closed stream"));
311
+ };
312
+ if let Err(e) = entry.reopen(info) {
313
+ // The entry is gone; leave the reader at EOF rather than
314
+ // half-open.
315
+ *finished = true;
316
+ return Err(zip_error(ruby, e));
317
+ }
318
+ *consumed = 0;
319
+ *finished = *size == Some(0);
320
+ Ok(())
321
+ }
322
+ }
323
+
324
+ /// Reads up to `want` bytes in `CHUNK` steps, pure Rust so it can run
325
+ /// without the GVL. Returns the bytes, whether the entry ended early, and
326
+ /// (when `probe`) whether data continues past the declared size.
327
+ fn fill(
328
+ file: &mut ZipFile<'static, Source>,
329
+ want: u64,
330
+ probe: bool,
331
+ ) -> io::Result<(Vec<u8>, bool, bool)> {
332
+ let mut buf = Vec::with_capacity(want.min(CHUNK as u64) as usize);
333
+ let mut eof = false;
334
+ while (buf.len() as u64) < want {
335
+ let step = (want - buf.len() as u64).min(CHUNK as u64) as usize;
336
+ let start = buf.len();
337
+ buf.resize(start + step, 0);
338
+ let n = file.read(&mut buf[start..])?;
339
+ buf.truncate(start + n);
340
+ if n == 0 {
341
+ eof = true;
342
+ break;
343
+ }
344
+ }
345
+ let mut oversize = false;
346
+ if probe && buf.len() as u64 == want {
347
+ let mut extra = [0u8; 1];
348
+ oversize = file.read(&mut extra)? != 0;
349
+ }
350
+ Ok((buf, eof, oversize))
351
+ }