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,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
+ }