From 0803c17aeb82720c70f878659667c91c211ede75 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Fri, 2 Feb 2024 20:50:09 +0100 Subject: [PATCH 01/23] EntryFsm: give back the buffer Also use method from local header --- rc-zip-sync/src/entry_reader.rs | 4 ++-- rc-zip-tokio/src/entry_reader.rs | 4 ++-- rc-zip/src/fsm/entry/mod.rs | 11 +++++------ 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/rc-zip-sync/src/entry_reader.rs b/rc-zip-sync/src/entry_reader.rs index a48a6df..f72e7b6 100644 --- a/rc-zip-sync/src/entry_reader.rs +++ b/rc-zip-sync/src/entry_reader.rs @@ -19,7 +19,7 @@ where pub(crate) fn new(entry: &StoredEntry, rd: R) -> Self { Self { rd, - fsm: Some(EntryFsm::new(entry.method(), entry.inner)), + fsm: Some(EntryFsm::new(entry.inner)), } } } @@ -53,7 +53,7 @@ where self.read(buf) } } - FsmResult::Done(()) => { + FsmResult::Done(_) => { // neat! Ok(0) } diff --git a/rc-zip-tokio/src/entry_reader.rs b/rc-zip-tokio/src/entry_reader.rs index c4af59b..6d7e6f7 100644 --- a/rc-zip-tokio/src/entry_reader.rs +++ b/rc-zip-tokio/src/entry_reader.rs @@ -28,7 +28,7 @@ where { Self { rd: get_reader(entry.header_offset), - fsm: Some(EntryFsm::new(entry.method(), entry.inner)), + fsm: Some(EntryFsm::new(entry.inner)), } } } @@ -78,7 +78,7 @@ where return self.poll_read(cx, buf); } } - FsmResult::Done(()) => { + FsmResult::Done(_) => { // neat! } } diff --git a/rc-zip/src/fsm/entry/mod.rs b/rc-zip/src/fsm/entry/mod.rs index db72965..4b6a12c 100644 --- a/rc-zip/src/fsm/entry/mod.rs +++ b/rc-zip/src/fsm/entry/mod.rs @@ -85,18 +85,16 @@ enum State { pub struct EntryFsm { state: State, entry: StoredEntryInner, - method: Method, buffer: Buffer, eof: bool, } impl EntryFsm { /// Create a new state machine for decompressing a zip entry - pub fn new(method: Method, entry: StoredEntryInner) -> Self { + pub fn new(entry: StoredEntryInner) -> Self { Self { state: State::ReadLocalHeader, entry, - method, buffer: Buffer::with_capacity(256 * 1024), eof: false, } @@ -131,7 +129,7 @@ impl EntryFsm { pub fn process( mut self, out: &mut [u8], - ) -> Result, Error> { + ) -> Result, Error> { tracing::trace!( state = match &self.state { State::ReadLocalHeader => "ReadLocalHeader", @@ -152,12 +150,13 @@ impl EntryFsm { let consumed = input.as_bytes().offset_from(&self.buffer.data()); tracing::trace!(local_file_header = ?header, consumed, "parsed local file header"); self.buffer.consume(consumed); + let decompressor = AnyDecompressor::new(header.method, &self.entry)?; self.state = S::ReadData { header, compressed_bytes: 0, uncompressed_bytes: 0, hasher: crc32fast::Hasher::new(), - decompressor: AnyDecompressor::new(self.method, &self.entry)?, + decompressor, }; self.process(out) } @@ -284,7 +283,7 @@ impl EntryFsm { })); } - Ok(FsmResult::Done(())) + Ok(FsmResult::Done(self.buffer)) } S::Transition => { unreachable!("the state machine should never be in the transition state") From 183f9809e014494755c5156ec4df180e93887957 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Fri, 2 Feb 2024 22:00:15 +0100 Subject: [PATCH 02/23] Moer fixes for EntryFsm --- rc-zip-sync/src/entry_reader.rs | 2 +- rc-zip-tokio/src/entry_reader.rs | 2 +- rc-zip/src/fsm/entry/lzma_dec.rs | 4 +-- rc-zip/src/fsm/entry/mod.rs | 57 +++++++++++++++++++++++++------- 4 files changed, 49 insertions(+), 16 deletions(-) diff --git a/rc-zip-sync/src/entry_reader.rs b/rc-zip-sync/src/entry_reader.rs index f72e7b6..2e078d2 100644 --- a/rc-zip-sync/src/entry_reader.rs +++ b/rc-zip-sync/src/entry_reader.rs @@ -19,7 +19,7 @@ where pub(crate) fn new(entry: &StoredEntry, rd: R) -> Self { Self { rd, - fsm: Some(EntryFsm::new(entry.inner)), + fsm: Some(EntryFsm::new(Some(entry.inner))), } } } diff --git a/rc-zip-tokio/src/entry_reader.rs b/rc-zip-tokio/src/entry_reader.rs index 6d7e6f7..d8eb20e 100644 --- a/rc-zip-tokio/src/entry_reader.rs +++ b/rc-zip-tokio/src/entry_reader.rs @@ -28,7 +28,7 @@ where { Self { rd: get_reader(entry.header_offset), - fsm: Some(EntryFsm::new(entry.inner)), + fsm: Some(EntryFsm::new(Some(entry.inner))), } } } diff --git a/rc-zip/src/fsm/entry/lzma_dec.rs b/rc-zip/src/fsm/entry/lzma_dec.rs index 0b98890..346e041 100644 --- a/rc-zip/src/fsm/entry/lzma_dec.rs +++ b/rc-zip/src/fsm/entry/lzma_dec.rs @@ -21,10 +21,10 @@ pub(crate) struct LzmaDec { } impl LzmaDec { - pub fn new(uncompressed_size: u64) -> Self { + pub fn new(uncompressed_size: Option) -> Self { let stream = Stream::new_with_options( &(Options { - unpacked_size: UnpackedSize::UseProvided(Some(uncompressed_size)), + unpacked_size: UnpackedSize::UseProvided(uncompressed_size), allow_incomplete: false, memlimit: Some(128 * 1024 * 1024), }), diff --git a/rc-zip/src/fsm/entry/mod.rs b/rc-zip/src/fsm/entry/mod.rs index 4b6a12c..39c2769 100644 --- a/rc-zip/src/fsm/entry/mod.rs +++ b/rc-zip/src/fsm/entry/mod.rs @@ -45,6 +45,9 @@ enum State { /// The local file header for this entry header: LocalFileHeaderRecord, + /// Entry compressed size + compressed_size: u64, + /// Amount of bytes we've fed to the decompressor compressed_bytes: u64, @@ -84,14 +87,14 @@ enum State { /// A state machine that can parse a zip entry pub struct EntryFsm { state: State, - entry: StoredEntryInner, + entry: Option, buffer: Buffer, eof: bool, } impl EntryFsm { /// Create a new state machine for decompressing a zip entry - pub fn new(entry: StoredEntryInner) -> Self { + pub fn new(entry: Option) -> Self { Self { state: State::ReadLocalHeader, entry, @@ -150,9 +153,27 @@ impl EntryFsm { let consumed = input.as_bytes().offset_from(&self.buffer.data()); tracing::trace!(local_file_header = ?header, consumed, "parsed local file header"); self.buffer.consume(consumed); - let decompressor = AnyDecompressor::new(header.method, &self.entry)?; + let decompressor = AnyDecompressor::new( + header.method, + self.entry.map(|entry| entry.uncompressed_size), + )?; + let compressed_size = match &self.entry { + Some(entry) => entry.compressed_size, + None => { + if header.compressed_size == u32::MAX { + return Err(Error::Decompression { + method: header.method, + msg: "This entry cannot be decompressed because its compressed size is larger than 4GiB".into(), + }); + } else { + header.compressed_size as u64 + } + } + }; + self.state = S::ReadData { header, + compressed_size, compressed_bytes: 0, uncompressed_bytes: 0, hasher: crc32fast::Hasher::new(), @@ -167,6 +188,7 @@ impl EntryFsm { } } S::ReadData { + compressed_size, compressed_bytes, uncompressed_bytes, hasher, @@ -176,15 +198,16 @@ impl EntryFsm { let in_buf = self.buffer.data(); // don't feed the decompressor bytes beyond the entry's compressed size + let in_buf_max_len = cmp::min( in_buf.len(), - self.entry.compressed_size as usize - *compressed_bytes as usize, + *compressed_size as usize - *compressed_bytes as usize, ); let in_buf = &in_buf[..in_buf_max_len]; let fed_bytes_after_this = *compressed_bytes + in_buf.len() as u64; - let has_more_input = if fed_bytes_after_this == self.entry.compressed_size as _ { + let has_more_input = if fed_bytes_after_this == *compressed_size as _ { HasMoreInput::No } else { HasMoreInput::Yes @@ -232,7 +255,14 @@ impl EntryFsm { } S::ReadDataDescriptor { .. } => { let mut input = Partial::new(self.buffer.data()); - match DataDescriptorRecord::mk_parser(self.entry.is_zip64).parse_next(&mut input) { + + // if we don't have entry info, we're dangerously assuming the + // file isn't zip64. oh well. + // FIXME: we can just read until the next local file header and + // determine whether the file is zip64 or not from there? + let is_zip64 = self.entry.as_ref().map(|e| e.is_zip64).unwrap_or(false); + + match DataDescriptorRecord::mk_parser(is_zip64).parse_next(&mut input) { Ok(descriptor) => { self.buffer .consume(input.as_bytes().offset_from(&self.buffer.data())); @@ -253,16 +283,19 @@ impl EntryFsm { metrics, descriptor, } => { - let expected_crc32 = if self.entry.crc32 != 0 { - self.entry.crc32 + let entry_crc32 = self.entry.map(|e| e.crc32).unwrap_or_default(); + let expected_crc32 = if entry_crc32 != 0 { + entry_crc32 } else if let Some(descriptor) = descriptor.as_ref() { descriptor.crc32 } else { header.crc32 }; - let expected_size = if self.entry.uncompressed_size != 0 { - self.entry.uncompressed_size + let entry_uncompressed_size = + self.entry.map(|e| e.uncompressed_size).unwrap_or_default(); + let expected_size = if entry_uncompressed_size != 0 { + entry_uncompressed_size } else if let Some(descriptor) = descriptor.as_ref() { descriptor.uncompressed_size } else { @@ -353,7 +386,7 @@ trait Decompressor { } impl AnyDecompressor { - fn new(method: Method, #[allow(unused)] entry: &StoredEntryInner) -> Result { + fn new(method: Method, #[allow(unused)] uncompressed_size: Option) -> Result { let dec = match method { Method::Store => Self::Store(Default::default()), @@ -382,7 +415,7 @@ impl AnyDecompressor { } #[cfg(feature = "lzma")] - Method::Lzma => Self::Lzma(Box::new(lzma_dec::LzmaDec::new(entry.uncompressed_size))), + Method::Lzma => Self::Lzma(Box::new(lzma_dec::LzmaDec::new(uncompressed_size))), #[cfg(not(feature = "lzma"))] Method::Lzma => { let err = Error::Unsupported(UnsupportedError::MethodNotEnabled(method)); From f0d34fe45d80d08abb4cc982764f5737866934e3 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Fri, 2 Feb 2024 22:30:26 +0100 Subject: [PATCH 03/23] Prototype StreamingEntryReader --- Cargo.lock | 1 + rc-zip-sync/Cargo.toml | 1 + rc-zip-sync/src/lib.rs | 1 + rc-zip-sync/src/read_zip.rs | 49 +++++++- rc-zip-sync/src/streaming_entry_reader.rs | 132 ++++++++++++++++++++++ 5 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 rc-zip-sync/src/streaming_entry_reader.rs diff --git a/Cargo.lock b/Cargo.lock index 5bbaee2..574cfa6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -820,6 +820,7 @@ dependencies = [ "test-log", "tracing", "tracing-subscriber", + "winnow", ] [[package]] diff --git a/rc-zip-sync/Cargo.toml b/rc-zip-sync/Cargo.toml index 020a80a..25b2c2f 100644 --- a/rc-zip-sync/Cargo.toml +++ b/rc-zip-sync/Cargo.toml @@ -24,6 +24,7 @@ positioned-io = { version = "0.3.3", optional = true } rc-zip = { version = "3.0.0", path = "../rc-zip" } oval = "2.0.0" tracing = "0.1.40" +winnow = "0.5.36" [features] default = ["file", "deflate"] diff --git a/rc-zip-sync/src/lib.rs b/rc-zip-sync/src/lib.rs index 304a1dd..1e35669 100644 --- a/rc-zip-sync/src/lib.rs +++ b/rc-zip-sync/src/lib.rs @@ -9,6 +9,7 @@ mod entry_reader; mod read_zip; +mod streaming_entry_reader; // re-exports pub use rc_zip; diff --git a/rc-zip-sync/src/read_zip.rs b/rc-zip-sync/src/read_zip.rs index 3089090..7e0c1a8 100644 --- a/rc-zip-sync/src/read_zip.rs +++ b/rc-zip-sync/src/read_zip.rs @@ -1,10 +1,16 @@ use rc_zip::{ error::Error, fsm::{ArchiveFsm, FsmResult}, - parse::{Archive, StoredEntry}, + parse::{Archive, LocalFileHeaderRecord, StoredEntry}, +}; +use winnow::{ + error::ErrMode, + stream::{AsBytes, Offset}, + Parser, Partial, }; use crate::entry_reader::EntryReader; +use crate::streaming_entry_reader::StreamingEntryReader; use std::{io::Read, ops::Deref}; /// A trait for reading something as a zip archive @@ -215,3 +221,44 @@ impl ReadZip for std::fs::File { self.read_zip_with_size(size) } } + +pub trait ReadZipEntriesStreaming +where + R: Read, +{ + fn first_entry(self) -> Result, Error>; +} + +impl ReadZipEntriesStreaming for R +where + R: Read, +{ + fn first_entry(mut self) -> Result, Error> { + // first, get enough data to read the first local file header + let mut buf = oval::Buffer::with_capacity(16 * 1024); + + let header = loop { + let n = self.read(buf.space())?; + buf.fill(n); + + let mut input = Partial::new(buf.data()); + match LocalFileHeaderRecord::parser.parse_next(&mut input) { + Ok(header) => { + let consumed = input.as_bytes().offset_from(&buf.data()); + buf.consume(consumed); + tracing::trace!(?header, %consumed, "Got local file header record!"); + break header; + } + // TODO: keep reading if we don't have enough data + Err(ErrMode::Incomplete(_)) => { + // read more + } + Err(e) => { + panic!("{e}") + } + } + }; + + Ok(StreamingEntryReader::new(buf, header, self)) + } +} diff --git a/rc-zip-sync/src/streaming_entry_reader.rs b/rc-zip-sync/src/streaming_entry_reader.rs new file mode 100644 index 0000000..672d9d9 --- /dev/null +++ b/rc-zip-sync/src/streaming_entry_reader.rs @@ -0,0 +1,132 @@ +use oval::Buffer; +use rc_zip::{ + fsm::{EntryFsm, FsmResult}, + parse::LocalFileHeaderRecord, +}; +use std::{ + io::{self, Write}, + str::Utf8Error, +}; + +pub(crate) struct StreamingEntryReader { + header: LocalFileHeaderRecord, + rd: R, + state: State, +} + +#[derive(Default)] +enum State { + Reading { + remain: Buffer, + fsm: EntryFsm, + }, + Finished { + /// remaining buffer for next entry + remain: Buffer, + }, + #[default] + Transition, +} + +impl StreamingEntryReader +where + R: io::Read, +{ + pub(crate) fn new(remain: Buffer, header: LocalFileHeaderRecord, rd: R) -> Self { + Self { + rd, + header, + state: State::Reading { + remain, + fsm: EntryFsm::new(None), + }, + } + } +} + +impl io::Read for StreamingEntryReader +where + R: io::Read, +{ + fn read(&mut self, buf: &mut [u8]) -> io::Result { + match std::mem::take(&mut self.state) { + State::Reading { + mut remain, + mut fsm, + } => { + if fsm.wants_read() { + tracing::trace!("fsm wants read"); + if remain.available_data() > 0 { + let n = remain.read(buf)?; + tracing::trace!("giving fsm {} bytes from remain", n); + fsm.fill(n); + } else { + let n = self.rd.read(fsm.space())?; + tracing::trace!("giving fsm {} bytes from rd", n); + fsm.fill(n); + } + } else { + tracing::trace!("fsm does not want read"); + } + + match fsm.process(buf)? { + FsmResult::Continue((fsm, outcome)) => { + self.state = State::Reading { remain, fsm }; + + if outcome.bytes_written > 0 { + Ok(outcome.bytes_written) + } else { + // loop, it happens + self.read(buf) + } + } + FsmResult::Done(mut fsm_remain) => { + // if our remain still has remaining data, it goes after + // what the fsm just gave back + if remain.available_data() > 0 { + fsm_remain.grow(fsm_remain.capacity() + remain.available_data()); + fsm_remain.write_all(remain.data()); + drop(remain) + } + + // FIXME: read the next local file header here + + self.state = State::Finished { remain: fsm_remain }; + + // neat! + Ok(0) + } + } + } + State::Finished { remain } => { + // wait for them to call finished + self.state = State::Finished { remain }; + Ok(0) + } + State::Transition => unreachable!(), + } + } +} + +impl StreamingEntryReader { + /// Return the name of this entry, decoded as UTF-8. + /// + /// There is no support for CP-437 in the streaming interface + pub fn name(&self) -> Result<&str, Utf8Error> { + std::str::from_utf8(&self.header.name.0) + } + + /// Finish reading this entry, returning the next streaming entry reader, if + /// any. This panics if the entry is not fully read. + pub fn finish(self) -> Option> { + match self.state { + State::Reading { .. } => { + panic!("finish called before entry is fully read") + } + State::Finished { .. } => { + todo!("read local file header for next entry") + } + State::Transition => unreachable!(), + } + } +} From 58e92dd0f6f24147660b3a8c909c6b1fb5dfc2e5 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Fri, 2 Feb 2024 22:35:28 +0100 Subject: [PATCH 04/23] wip --- rc-zip-sync/examples/jean.rs | 120 ++++++++++++++++++++++++++++++++++- rc-zip-sync/src/lib.rs | 4 +- rc-zip-sync/src/read_zip.rs | 4 +- 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/rc-zip-sync/examples/jean.rs b/rc-zip-sync/examples/jean.rs index bf613d4..287943f 100644 --- a/rc-zip-sync/examples/jean.rs +++ b/rc-zip-sync/examples/jean.rs @@ -2,7 +2,7 @@ use cfg_if::cfg_if; use clap::{Parser, Subcommand}; use humansize::{format_size, BINARY}; use rc_zip::parse::{Archive, EntryContents, Method, Version}; -use rc_zip_sync::ReadZip; +use rc_zip_sync::{ReadZip, ReadZipEntriesStreaming}; use std::{ borrow::Cow, @@ -62,6 +62,12 @@ enum Commands { Unzip { zipfile: PathBuf, + #[arg(long)] + dir: Option, + }, + UnzipStreaming { + zipfile: PathBuf, + #[arg(long)] dir: Option, }, @@ -294,6 +300,118 @@ fn do_main(cli: Cli) -> Result<(), Box> { let bps = (uncompressed_size as f64 / seconds) as u64; println!("Overall extraction speed: {} / s", format_size(bps, BINARY)); } + Commands::UnzipStreaming { zipfile, dir } => { + let zipfile = File::open(zipfile)?; + let dir = PathBuf::from(dir.unwrap_or_else(|| ".".into())); + + let mut entry = zipfile.read_first_zip_entry_streaming()?; + + let mut num_dirs = 0; + let mut num_files = 0; + let mut num_symlinks = 0; + let mut done_bytes: u64 = 0; + + use indicatif::{ProgressBar, ProgressStyle}; + let pbar = ProgressBar::new(100); + pbar.set_style( + ProgressStyle::default_bar() + .template("{eta_precise} [{bar:20.cyan/blue}] {wide_msg}") + .unwrap() + .progress_chars("=>-"), + ); + + pbar.enable_steady_tick(Duration::from_millis(125)); + + let start_time = std::time::SystemTime::now(); + loop { + let entry_name = entry.name().unwrap(); + let entry_name = match sanitize_entry_name(entry_name) { + Some(name) => name, + None => continue, + }; + + pbar.set_message(entry_name.to_string()); + match entry.contents() { + EntryContents::Symlink => { + num_symlinks += 1; + + cfg_if! { + if #[cfg(windows)] { + let path = dir.join(entry_name); + std::fs::create_dir_all( + path.parent() + .expect("all full entry paths should have parent paths"), + )?; + let mut entry_writer = File::create(path)?; + let mut entry_reader = entry.reader(); + std::io::copy(&mut entry_reader, &mut entry_writer)?; + } else { + let path = dir.join(entry_name); + std::fs::create_dir_all( + path.parent() + .expect("all full entry paths should have parent paths"), + )?; + if let Ok(metadata) = std::fs::symlink_metadata(&path) { + if metadata.is_file() { + std::fs::remove_file(&path)?; + } + } + + let mut src = String::new(); + entry.reader().read_to_string(&mut src)?; + + // validate pointing path before creating a symbolic link + if src.contains("..") { + continue; + } + std::os::unix::fs::symlink(src, &path)?; + } + } + } + EntryContents::Directory => { + num_dirs += 1; + let path = dir.join(entry_name); + std::fs::create_dir_all( + path.parent() + .expect("all full entry paths should have parent paths"), + )?; + } + EntryContents::File => { + num_files += 1; + let path = dir.join(entry_name); + std::fs::create_dir_all( + path.parent() + .expect("all full entry paths should have parent paths"), + )?; + let mut entry_writer = File::create(path)?; + let entry_reader = entry.reader(); + let before_entry_bytes = done_bytes; + let mut progress_reader = ProgressRead::new( + entry_reader, + entry.inner.uncompressed_size, + |prog| { + pbar.set_position(before_entry_bytes + prog.done); + }, + ); + + let copied_bytes = std::io::copy(&mut progress_reader, &mut entry_writer)?; + done_bytes = before_entry_bytes + copied_bytes; + } + } + } + pbar.finish(); + let duration = start_time.elapsed()?; + println!( + "Extracted {} (in {} files, {} dirs, {} symlinks)", + format_size(uncompressed_size, BINARY), + num_files, + num_dirs, + num_symlinks + ); + let seconds = (duration.as_millis() as f64) / 1000.0; + let bps = (uncompressed_size as f64 / seconds) as u64; + println!("Overall extraction speed: {} / s", format_size(bps, BINARY)); + } } Ok(()) diff --git a/rc-zip-sync/src/lib.rs b/rc-zip-sync/src/lib.rs index 1e35669..1bb0ef1 100644 --- a/rc-zip-sync/src/lib.rs +++ b/rc-zip-sync/src/lib.rs @@ -13,4 +13,6 @@ mod streaming_entry_reader; // re-exports pub use rc_zip; -pub use read_zip::{HasCursor, ReadZip, ReadZipWithSize, SyncArchive, SyncStoredEntry}; +pub use read_zip::{ + HasCursor, ReadZip, ReadZipEntriesStreaming, ReadZipWithSize, SyncArchive, SyncStoredEntry, +}; diff --git a/rc-zip-sync/src/read_zip.rs b/rc-zip-sync/src/read_zip.rs index 7e0c1a8..0231574 100644 --- a/rc-zip-sync/src/read_zip.rs +++ b/rc-zip-sync/src/read_zip.rs @@ -226,14 +226,14 @@ pub trait ReadZipEntriesStreaming where R: Read, { - fn first_entry(self) -> Result, Error>; + fn read_first_zip_entry_streaming(self) -> Result, Error>; } impl ReadZipEntriesStreaming for R where R: Read, { - fn first_entry(mut self) -> Result, Error> { + fn read_first_zip_entry_streaming(mut self) -> Result, Error> { // first, get enough data to read the first local file header let mut buf = oval::Buffer::with_capacity(16 * 1024); From 9a30c74c31d6327ad1304426ae75c86bf954e7c3 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 13:13:28 +0100 Subject: [PATCH 05/23] Modify jean to add a streaming unzip option --- rc-zip-sync/examples/jean.rs | 162 ++++++++++++---------- rc-zip-sync/src/streaming_entry_reader.rs | 2 +- 2 files changed, 89 insertions(+), 75 deletions(-) diff --git a/rc-zip-sync/examples/jean.rs b/rc-zip-sync/examples/jean.rs index 287943f..a58c210 100644 --- a/rc-zip-sync/examples/jean.rs +++ b/rc-zip-sync/examples/jean.rs @@ -331,86 +331,100 @@ fn do_main(cli: Cli) -> Result<(), Box> { }; pbar.set_message(entry_name.to_string()); - match entry.contents() { - EntryContents::Symlink => { - num_symlinks += 1; - - cfg_if! { - if #[cfg(windows)] { - let path = dir.join(entry_name); - std::fs::create_dir_all( - path.parent() - .expect("all full entry paths should have parent paths"), - )?; - let mut entry_writer = File::create(path)?; - let mut entry_reader = entry.reader(); - std::io::copy(&mut entry_reader, &mut entry_writer)?; - } else { - let path = dir.join(entry_name); - std::fs::create_dir_all( - path.parent() - .expect("all full entry paths should have parent paths"), - )?; - if let Ok(metadata) = std::fs::symlink_metadata(&path) { - if metadata.is_file() { - std::fs::remove_file(&path)?; - } - } + let mut buf = vec![]; + entry.read_to_end(&mut buf)?; - let mut src = String::new(); - entry.reader().read_to_string(&mut src)?; - - // validate pointing path before creating a symbolic link - if src.contains("..") { - continue; - } - std::os::unix::fs::symlink(src, &path)?; - } - } + match entry.finish() { + Some(next_entry) => { + println!("Found next entry!"); + entry = next_entry; } - EntryContents::Directory => { - num_dirs += 1; - let path = dir.join(entry_name); - std::fs::create_dir_all( - path.parent() - .expect("all full entry paths should have parent paths"), - )?; - } - EntryContents::File => { - num_files += 1; - let path = dir.join(entry_name); - std::fs::create_dir_all( - path.parent() - .expect("all full entry paths should have parent paths"), - )?; - let mut entry_writer = File::create(path)?; - let entry_reader = entry.reader(); - let before_entry_bytes = done_bytes; - let mut progress_reader = ProgressRead::new( - entry_reader, - entry.inner.uncompressed_size, - |prog| { - pbar.set_position(before_entry_bytes + prog.done); - }, - ); - - let copied_bytes = std::io::copy(&mut progress_reader, &mut entry_writer)?; - done_bytes = before_entry_bytes + copied_bytes; + None => { + println!("End of archive!"); + break; } } + + // match entry.contents() { + // EntryContents::Symlink => { + // num_symlinks += 1; + + // cfg_if! { + // if #[cfg(windows)] { + // let path = dir.join(entry_name); + // std::fs::create_dir_all( + // path.parent() + // .expect("all full entry paths should have parent paths"), + // )?; + // let mut entry_writer = File::create(path)?; + // let mut entry_reader = entry.reader(); + // std::io::copy(&mut entry_reader, &mut entry_writer)?; + // } else { + // let path = dir.join(entry_name); + // std::fs::create_dir_all( + // path.parent() + // .expect("all full entry paths should have parent paths"), + // )?; + // if let Ok(metadata) = std::fs::symlink_metadata(&path) { + // if metadata.is_file() { + // std::fs::remove_file(&path)?; + // } + // } + + // let mut src = String::new(); + // entry.reader().read_to_string(&mut src)?; + + // // validate pointing path before creating a symbolic link + // if src.contains("..") { + // continue; + // } + // std::os::unix::fs::symlink(src, &path)?; + // } + // } + // } + // EntryContents::Directory => { + // num_dirs += 1; + // let path = dir.join(entry_name); + // std::fs::create_dir_all( + // path.parent() + // .expect("all full entry paths should have parent paths"), + // )?; + // } + // EntryContents::File => { + // num_files += 1; + // let path = dir.join(entry_name); + // std::fs::create_dir_all( + // path.parent() + // .expect("all full entry paths should have parent paths"), + // )?; + // let mut entry_writer = File::create(path)?; + // let entry_reader = entry.reader(); + // let before_entry_bytes = done_bytes; + // let mut progress_reader = ProgressRead::new( + // entry_reader, + // entry.inner.uncompressed_size, + // |prog| { + // pbar.set_position(before_entry_bytes + prog.done); + // }, + // ); + + // let copied_bytes = std::io::copy(&mut progress_reader, &mut entry_writer)?; + // done_bytes = before_entry_bytes + copied_bytes; + // } + // } } pbar.finish(); - let duration = start_time.elapsed()?; - println!( - "Extracted {} (in {} files, {} dirs, {} symlinks)", - format_size(uncompressed_size, BINARY), - num_files, - num_dirs, - num_symlinks - ); - let seconds = (duration.as_millis() as f64) / 1000.0; - let bps = (uncompressed_size as f64 / seconds) as u64; - println!("Overall extraction speed: {} / s", format_size(bps, BINARY)); + // let duration = start_time.elapsed()?; + // println!( + // "Extracted {} (in {} files, {} dirs, {} symlinks)", + // format_size(uncompressed_size, BINARY), + // num_files, + // num_dirs, + // num_symlinks + // ); + // let seconds = (duration.as_millis() as f64) / 1000.0; + // let bps = (uncompressed_size as f64 / seconds) as u64; + // println!("Overall extraction speed: {} / s", format_size(bps, BINARY)); } } diff --git a/rc-zip-sync/src/streaming_entry_reader.rs b/rc-zip-sync/src/streaming_entry_reader.rs index 672d9d9..382ded6 100644 --- a/rc-zip-sync/src/streaming_entry_reader.rs +++ b/rc-zip-sync/src/streaming_entry_reader.rs @@ -8,7 +8,7 @@ use std::{ str::Utf8Error, }; -pub(crate) struct StreamingEntryReader { +pub struct StreamingEntryReader { header: LocalFileHeaderRecord, rd: R, state: State, From 4352ab1660e62496f5f58b9740eb1cf461d961cf Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 15:40:17 +0100 Subject: [PATCH 06/23] jean getting further --- rc-zip-sync/examples/jean.rs | 88 +---------------------- rc-zip-sync/src/read_zip.rs | 14 +++- rc-zip-sync/src/streaming_entry_reader.rs | 18 +++-- 3 files changed, 26 insertions(+), 94 deletions(-) diff --git a/rc-zip-sync/examples/jean.rs b/rc-zip-sync/examples/jean.rs index a58c210..be5458a 100644 --- a/rc-zip-sync/examples/jean.rs +++ b/rc-zip-sync/examples/jean.rs @@ -300,17 +300,11 @@ fn do_main(cli: Cli) -> Result<(), Box> { let bps = (uncompressed_size as f64 / seconds) as u64; println!("Overall extraction speed: {} / s", format_size(bps, BINARY)); } - Commands::UnzipStreaming { zipfile, dir } => { + Commands::UnzipStreaming { zipfile, .. } => { let zipfile = File::open(zipfile)?; - let dir = PathBuf::from(dir.unwrap_or_else(|| ".".into())); let mut entry = zipfile.read_first_zip_entry_streaming()?; - let mut num_dirs = 0; - let mut num_files = 0; - let mut num_symlinks = 0; - let mut done_bytes: u64 = 0; - use indicatif::{ProgressBar, ProgressStyle}; let pbar = ProgressBar::new(100); pbar.set_style( @@ -322,7 +316,6 @@ fn do_main(cli: Cli) -> Result<(), Box> { pbar.enable_steady_tick(Duration::from_millis(125)); - let start_time = std::time::SystemTime::now(); loop { let entry_name = entry.name().unwrap(); let entry_name = match sanitize_entry_name(entry_name) { @@ -344,87 +337,8 @@ fn do_main(cli: Cli) -> Result<(), Box> { break; } } - - // match entry.contents() { - // EntryContents::Symlink => { - // num_symlinks += 1; - - // cfg_if! { - // if #[cfg(windows)] { - // let path = dir.join(entry_name); - // std::fs::create_dir_all( - // path.parent() - // .expect("all full entry paths should have parent paths"), - // )?; - // let mut entry_writer = File::create(path)?; - // let mut entry_reader = entry.reader(); - // std::io::copy(&mut entry_reader, &mut entry_writer)?; - // } else { - // let path = dir.join(entry_name); - // std::fs::create_dir_all( - // path.parent() - // .expect("all full entry paths should have parent paths"), - // )?; - // if let Ok(metadata) = std::fs::symlink_metadata(&path) { - // if metadata.is_file() { - // std::fs::remove_file(&path)?; - // } - // } - - // let mut src = String::new(); - // entry.reader().read_to_string(&mut src)?; - - // // validate pointing path before creating a symbolic link - // if src.contains("..") { - // continue; - // } - // std::os::unix::fs::symlink(src, &path)?; - // } - // } - // } - // EntryContents::Directory => { - // num_dirs += 1; - // let path = dir.join(entry_name); - // std::fs::create_dir_all( - // path.parent() - // .expect("all full entry paths should have parent paths"), - // )?; - // } - // EntryContents::File => { - // num_files += 1; - // let path = dir.join(entry_name); - // std::fs::create_dir_all( - // path.parent() - // .expect("all full entry paths should have parent paths"), - // )?; - // let mut entry_writer = File::create(path)?; - // let entry_reader = entry.reader(); - // let before_entry_bytes = done_bytes; - // let mut progress_reader = ProgressRead::new( - // entry_reader, - // entry.inner.uncompressed_size, - // |prog| { - // pbar.set_position(before_entry_bytes + prog.done); - // }, - // ); - - // let copied_bytes = std::io::copy(&mut progress_reader, &mut entry_writer)?; - // done_bytes = before_entry_bytes + copied_bytes; - // } - // } } pbar.finish(); - // let duration = start_time.elapsed()?; - // println!( - // "Extracted {} (in {} files, {} dirs, {} symlinks)", - // format_size(uncompressed_size, BINARY), - // num_files, - // num_dirs, - // num_symlinks - // ); - // let seconds = (duration.as_millis() as f64) / 1000.0; - // let bps = (uncompressed_size as f64 / seconds) as u64; - // println!("Overall extraction speed: {} / s", format_size(bps, BINARY)); } } diff --git a/rc-zip-sync/src/read_zip.rs b/rc-zip-sync/src/read_zip.rs index 0231574..ad1ba43 100644 --- a/rc-zip-sync/src/read_zip.rs +++ b/rc-zip-sync/src/read_zip.rs @@ -222,10 +222,22 @@ impl ReadZip for std::fs::File { } } +/// Allows reading zip entries in a streaming fashion, without seeking, +/// based only on local headers. THIS IS NOT RECOMMENDED, as correctly +/// reading zip files requires reading the central directory (located at +/// the end of the file). +/// +/// Using local headers only involves a lot of guesswork and is only really +/// useful if you have some level of control over your input. pub trait ReadZipEntriesStreaming where R: Read, { + /// Get the first zip entry from the stream as a [StreamingEntryReader]. + /// + /// See [ReadZipEntriesStreaming]'s documentation for why using this is + /// generally a bad idea: you might want to use [ReadZip] or + /// [ReadZipWithSize] instead. fn read_first_zip_entry_streaming(self) -> Result, Error>; } @@ -239,13 +251,13 @@ where let header = loop { let n = self.read(buf.space())?; + tracing::trace!("read {} bytes into buf for first zip entry", n); buf.fill(n); let mut input = Partial::new(buf.data()); match LocalFileHeaderRecord::parser.parse_next(&mut input) { Ok(header) => { let consumed = input.as_bytes().offset_from(&buf.data()); - buf.consume(consumed); tracing::trace!(?header, %consumed, "Got local file header record!"); break header; } diff --git a/rc-zip-sync/src/streaming_entry_reader.rs b/rc-zip-sync/src/streaming_entry_reader.rs index 382ded6..238e25c 100644 --- a/rc-zip-sync/src/streaming_entry_reader.rs +++ b/rc-zip-sync/src/streaming_entry_reader.rs @@ -7,6 +7,7 @@ use std::{ io::{self, Write}, str::Utf8Error, }; +use tracing::trace; pub struct StreamingEntryReader { header: LocalFileHeaderRecord, @@ -15,6 +16,7 @@ pub struct StreamingEntryReader { } #[derive(Default)] +#[allow(clippy::large_enum_variant)] enum State { Reading { remain: Buffer, @@ -55,18 +57,22 @@ where mut fsm, } => { if fsm.wants_read() { - tracing::trace!("fsm wants read"); + trace!("fsm wants read"); if remain.available_data() > 0 { - let n = remain.read(buf)?; - tracing::trace!("giving fsm {} bytes from remain", n); + trace!( + "remain has {} data bytes available", + remain.available_data(), + ); + let n = remain.read(fsm.space())?; + trace!("giving fsm {} bytes from remain", n); fsm.fill(n); } else { let n = self.rd.read(fsm.space())?; - tracing::trace!("giving fsm {} bytes from rd", n); + trace!("giving fsm {} bytes from rd", n); fsm.fill(n); } } else { - tracing::trace!("fsm does not want read"); + trace!("fsm does not want read"); } match fsm.process(buf)? { @@ -85,7 +91,7 @@ where // what the fsm just gave back if remain.available_data() > 0 { fsm_remain.grow(fsm_remain.capacity() + remain.available_data()); - fsm_remain.write_all(remain.data()); + fsm_remain.write_all(remain.data())?; drop(remain) } From cd3a9a9b795c115725ec438e6bf3abea096cfd15 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 15:43:22 +0100 Subject: [PATCH 07/23] Remove old files --- rc-zip-sync/src/read_zip.rs | 3 + rc-zip/src/format/archive.rs | 326 -------------------------- rc-zip/src/format/date_time.rs | 104 -------- rc-zip/src/format/directory_header.rs | 244 ------------------- rc-zip/src/format/eocd.rs | 263 --------------------- rc-zip/src/format/extra_field.rs | 289 ----------------------- rc-zip/src/format/local.rs | 188 --------------- rc-zip/src/format/mod.rs | 23 -- rc-zip/src/format/mode.rs | 239 ------------------- rc-zip/src/format/raw.rs | 77 ------ rc-zip/src/format/version.rs | 133 ----------- 11 files changed, 3 insertions(+), 1886 deletions(-) delete mode 100644 rc-zip/src/format/archive.rs delete mode 100644 rc-zip/src/format/date_time.rs delete mode 100644 rc-zip/src/format/directory_header.rs delete mode 100644 rc-zip/src/format/eocd.rs delete mode 100644 rc-zip/src/format/extra_field.rs delete mode 100644 rc-zip/src/format/local.rs delete mode 100644 rc-zip/src/format/mod.rs delete mode 100644 rc-zip/src/format/mode.rs delete mode 100644 rc-zip/src/format/raw.rs delete mode 100644 rc-zip/src/format/version.rs diff --git a/rc-zip-sync/src/read_zip.rs b/rc-zip-sync/src/read_zip.rs index ad1ba43..f98e1e8 100644 --- a/rc-zip-sync/src/read_zip.rs +++ b/rc-zip-sync/src/read_zip.rs @@ -259,6 +259,9 @@ where Ok(header) => { let consumed = input.as_bytes().offset_from(&buf.data()); tracing::trace!(?header, %consumed, "Got local file header record!"); + // write extra bytes to `/tmp/extra.bin` for debugging + std::fs::write("/tmp/extra.bin", input.as_bytes()).unwrap(); + tracing::trace!("wrote extra bytes to /tmp/extra.bin"); break header; } // TODO: keep reading if we don't have enough data diff --git a/rc-zip/src/format/archive.rs b/rc-zip/src/format/archive.rs deleted file mode 100644 index dc2bb67..0000000 --- a/rc-zip/src/format/archive.rs +++ /dev/null @@ -1,326 +0,0 @@ -use crate::format::*; -use num_enum::{FromPrimitive, IntoPrimitive}; - -/// An Archive contains general information about a zip files, -/// along with a list of [entries][StoredEntry]. -/// -/// It is obtained via an [ArchiveReader](crate::reader::ArchiveReader), or via a higher-level API -/// like the [ReadZip](crate::reader::sync::ReadZip) trait. -pub struct Archive { - pub(crate) size: u64, - pub(crate) encoding: Encoding, - pub(crate) entries: Vec, - pub(crate) comment: Option, -} - -impl Archive { - /// The size of .zip file that was read, in bytes. - pub fn size(&self) -> u64 { - self.size - } - - /// Iterate over all files in this zip, read from the central directory. - pub fn entries(&self) -> impl Iterator { - self.entries.iter() - } - - /// Attempts to look up an entry by name. This is usually a bad idea, - /// as names aren't necessarily normalized in zip archives. - pub fn by_name>(&self, name: N) -> Option<&StoredEntry> { - self.entries.iter().find(|&x| x.name() == name.as_ref()) - } - - /// Returns the detected character encoding for text fields - /// (names, comments) inside this zip archive. - pub fn encoding(&self) -> Encoding { - self.encoding - } - - /// Returns the comment for this archive, if any. When reading - /// a zip file with an empty comment field, this will return None. - pub fn comment(&self) -> Option<&String> { - self.comment.as_ref() - } -} - -/// Describes a zip archive entry (a file, a directory, a symlink) -/// -/// `Entry` contains normalized metadata fields, that can be set when -/// writing a zip archive. Additional metadata, along with the information -/// required to extract an entry, are available in [StoredEntry][] instead. -#[derive(Clone)] -pub struct Entry { - /// Name of the file - /// Must be a relative path, not start with a drive letter (e.g. C:), - /// and must use forward slashes instead of back slashes - pub name: String, - - /// Compression method - /// - /// See [Method][] for more details. - pub method: Method, - - /// Comment is any arbitrary user-defined string shorter than 64KiB - pub comment: Option, - - /// Modified timestamp - pub modified: chrono::DateTime, - - /// Created timestamp - pub created: Option>, - - /// Accessed timestamp - pub accessed: Option>, -} - -/// An entry as stored into an Archive. Contains additional metadata and offset information. -/// -/// Whereas [Entry][] is archive-independent, [StoredEntry][] contains information that is tied to -/// a specific archive. -/// -/// When reading archives, one deals with a list of [StoredEntry][], whereas when writing one, one -/// typically only specifies an [Entry][] and provides the entry's contents: fields like the CRC32 -/// hash, uncompressed size, and compressed size are derived automatically from the input. -#[derive(Clone)] -pub struct StoredEntry { - /// Archive-independent information - /// - /// This contains the entry's name, timestamps, comment, compression method. - pub entry: Entry, - - /// Offset of the local file header in the zip file - /// - /// ```text - /// [optional non-zip data] - /// [local file header 1] <------ header_offset points here - /// [encryption header 1] - /// [file data 1] - /// [data descriptor 1] - /// ... - /// [central directory] - /// [optional zip64 end of central directory info] - /// [end of central directory record] - /// ``` - pub header_offset: u64, - - /// External attributes (zip) - pub external_attrs: u32, - - /// Version of zip supported by the tool that crated this archive. - pub creator_version: Version, - - /// Version of zip needed to extract this archive. - pub reader_version: Version, - - /// General purpose bit flag - /// - /// In the zip format, the most noteworthy flag (bit 11) is for UTF-8 names. - /// Other flags can indicate: encryption (unsupported), various compression - /// settings (depending on the [Method] used). - /// - /// For LZMA, general-purpose bit 1 denotes the EOS marker. - pub flags: u16, - - /// Unix user ID - /// - /// Only present if a Unix extra field or New Unix extra field was found. - pub uid: Option, - - /// Unix group ID - /// - /// Only present if a Unix extra field or New Unix extra field was found. - pub gid: Option, - - /// File mode - pub mode: Mode, - - /// Any extra fields recognized while parsing the file. - /// - /// Most of these should be normalized and accessible as other fields, - /// but they are also made available here raw. - pub extra_fields: Vec, - - pub inner: StoredEntryInner, -} - -#[derive(Clone, Copy, Debug)] -pub struct StoredEntryInner { - /// CRC-32 hash as found in the central directory. - /// - /// Note that this may be zero, and the actual CRC32 might be in the local header, or (more - /// commonly) in the data descriptor instead. - pub crc32: u32, - - /// Size in bytes, after compression - pub compressed_size: u64, - - /// Size in bytes, before compression - /// - /// This will be zero for directories. - pub uncompressed_size: u64, - - /// True if this entry was read from a zip64 archive - pub is_zip64: bool, -} - -impl StoredEntry { - /// Returns the entry's name. See also - /// [sanitized_name()](StoredEntry::sanitized_name), which returns a - /// sanitized version of the name. - /// - /// This should be a relative path, separated by `/`. However, there are zip - /// files in the wild with all sorts of evil variants, so, be conservative - /// in what you accept. - pub fn name(&self) -> &str { - self.entry.name.as_ref() - } - - /// Returns a sanitized version of the entry's name, if it - /// seems safe. In particular, if this method feels like the - /// entry name is trying to do a zip slip (cf. - /// ), it'll return - /// None. - /// - /// Other than that, it will strip any leading slashes on non-Windows OSes. - pub fn sanitized_name(&self) -> Option<&str> { - let name = self.name(); - - // refuse entries with traversed/absolute path to mitigate zip slip - if name.contains("..") { - return None; - } - - #[cfg(windows)] - { - if name.contains(":\\") || name.starts_with("\\") { - return None; - } - Some(name) - } - - #[cfg(not(windows))] - { - // strip absolute prefix on entries pointing to root path - let mut entry_chars = name.chars(); - let mut name = name; - while name.starts_with('/') { - entry_chars.next(); - name = entry_chars.as_str() - } - Some(name) - } - } - - /// The entry's comment, if any. - /// - /// When reading a zip file, an empty comment results in None. - pub fn comment(&self) -> Option<&str> { - self.entry.comment.as_ref().map(|x| x.as_ref()) - } - - /// The compression method used for this entry - #[inline(always)] - pub fn method(&self) -> Method { - self.entry.method - } - - /// This entry's "last modified" timestamp - with caveats - /// - /// Due to the history of the ZIP file format, this may be inaccurate. It may be offset - /// by a few hours, if there is no extended timestamp information. It may have a resolution - /// as low as two seconds, if only MSDOS timestamps are present. It may default to the Unix - /// epoch, if something went really wrong. - /// - /// If you're reading this after the year 2038, or after the year 2108, godspeed. - #[inline(always)] - pub fn modified(&self) -> DateTime { - self.entry.modified - } - - /// This entry's "created" timestamp, if available. - /// - /// See [StoredEntry::modified()] for caveats. - #[inline(always)] - pub fn created(&self) -> Option<&DateTime> { - self.entry.created.as_ref() - } - - /// This entry's "last accessed" timestamp, if available. - /// - /// See [StoredEntry::modified()] for caveats. - #[inline(always)] - pub fn accessed(&self) -> Option<&DateTime> { - self.entry.accessed.as_ref() - } -} - -/// The contents of an entry: a directory, a file, or a symbolic link. -#[derive(Debug)] -pub enum EntryContents { - Directory, - File, - Symlink, -} - -impl StoredEntry { - pub fn contents(&self) -> EntryContents { - if self.mode.has(Mode::SYMLINK) { - EntryContents::Symlink - } else if self.mode.has(Mode::DIR) { - EntryContents::Directory - } else { - EntryContents::File - } - } -} - -/// Compression method used for a file entry. -/// -/// In archives that follow [ISO/IEC 21320-1:2015](https://www.iso.org/standard/60101.html), only -/// [Store][Method::Store] and [Deflate][Method::Deflate] should be used. -/// -/// However, in the wild, it is not too uncommon to encounter [Bzip2][Method::Bzip2], -/// [Lzma][Method::Lzma] or others. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IntoPrimitive, FromPrimitive)] -#[repr(u16)] -pub enum Method { - /// No compression is applied - Store = 0, - - /// [DEFLATE (RFC 1951)](https://www.ietf.org/rfc/rfc1951.txt) - Deflate = 8, - - /// [DEFLATE64](https://deflate64.com/) - Deflate64 = 9, - - /// [BZIP-2](https://github.com/dsnet/compress/blob/master/doc/bzip2-format.pdf) - Bzip2 = 12, - - /// [LZMA](https://github.com/jljusten/LZMA-SDK/blob/master/DOC/lzma-specification.txt) - Lzma = 14, - - /// [zstd](https://datatracker.ietf.org/doc/html/rfc8878) - Zstd = 93, - - /// [MP3](https://www.iso.org/obp/ui/#iso:std:iso-iec:11172:-3:ed-1:v1:en) - Mp3 = 94, - - /// [XZ](https://tukaani.org/xz/xz-file-format.txt) - Xz = 95, - - /// [JPEG](https://jpeg.org/jpeg/) - Jpeg = 96, - - /// [WavPack](https://www.wavpack.com/) - WavPack = 97, - - /// [PPMd](https://en.wikipedia.org/wiki/Prediction_by_partial_matching) - Ppmd = 98, - - /// AE-x encryption marker (see Appendix E of appnote) - Aex = 99, - - /// A compression method that isn't recognized by this crate. - #[num_enum(catch_all)] - Unrecognized(u16), -} diff --git a/rc-zip/src/format/date_time.rs b/rc-zip/src/format/date_time.rs deleted file mode 100644 index baeee9a..0000000 --- a/rc-zip/src/format/date_time.rs +++ /dev/null @@ -1,104 +0,0 @@ -use chrono::{ - offset::{LocalResult, TimeZone, Utc}, - DateTime, Timelike, -}; -use std::fmt; -use winnow::{ - binary::{le_u16, le_u64}, - seq, PResult, Parser, Partial, -}; - -/// A timestamp in MS-DOS format -/// -/// Represents dates from year 1980 to 2180, with 2 second precision. -#[derive(Clone, Copy, Eq, PartialEq)] -pub struct MsdosTimestamp { - pub time: u16, - pub date: u16, -} - -impl fmt::Debug for MsdosTimestamp { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.to_datetime() { - Some(dt) => write!(f, "MsdosTimestamp({})", dt), - None => write!(f, "MsdosTimestamp(?)"), - } - } -} - -impl MsdosTimestamp { - pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - seq! {Self { - time: le_u16, - date: le_u16, - }} - .parse_next(i) - } - - /// Attempts to convert to a chrono UTC date time - pub fn to_datetime(&self) -> Option> { - // see https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-dosdatetimetofiletime - let date = match { - // bits 0-4: day of the month (1-31) - let d = (self.date & 0b1_1111) as u32; - // bits 5-8: month (1 = january, 2 = february and so on) - let m = ((self.date >> 5) & 0b1111) as u32; - // bits 9-15: year offset from 1980 - let y = ((self.date >> 9) + 1980) as i32; - Utc.with_ymd_and_hms(y, m, d, 0, 0, 0) - } { - LocalResult::Single(date) => date, - _ => return None, - }; - - // bits 0-4: second divided by 2 - let s = (self.time & 0b1_1111) as u32 * 2; - // bits 5-10: minute (0-59) - let m = (self.time >> 5 & 0b11_1111) as u32; - // bits 11-15: hour (0-23 on a 24-hour clock) - let h = (self.time >> 11) as u32; - date.with_hour(h)?.with_minute(m)?.with_second(s) - } -} - -/// A timestamp in NTFS format. -#[derive(Clone, Copy, Eq, PartialEq)] -pub struct NtfsTimestamp { - pub timestamp: u64, -} - -impl fmt::Debug for NtfsTimestamp { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self.to_datetime() { - Some(dt) => write!(f, "NtfsTimestamp({})", dt), - None => write!(f, "NtfsTimestamp(?)"), - } - } -} - -impl NtfsTimestamp { - /// Parse an MS-DOS timestamp from a byte slice - pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - le_u64.map(|timestamp| Self { timestamp }).parse_next(i) - } - - /// Attempts to convert to a chrono UTC date time - pub fn to_datetime(&self) -> Option> { - // windows timestamp resolution - let ticks_per_second = 10_000_000; - let secs = (self.timestamp / ticks_per_second) as i64; - let nsecs = ((self.timestamp % ticks_per_second) * 100) as u32; - let epoch = Utc.with_ymd_and_hms(1601, 1, 1, 0, 0, 0).single()?; - match Utc.timestamp_opt(epoch.timestamp() + secs, nsecs) { - LocalResult::Single(date) => Some(date), - _ => None, - } - } -} - -pub(crate) fn zero_datetime() -> chrono::DateTime { - chrono::DateTime::from_naive_utc_and_offset( - chrono::naive::NaiveDateTime::from_timestamp_opt(0, 0).unwrap(), - chrono::offset::Utc, - ) -} diff --git a/rc-zip/src/format/directory_header.rs b/rc-zip/src/format/directory_header.rs deleted file mode 100644 index ff73cf6..0000000 --- a/rc-zip/src/format/directory_header.rs +++ /dev/null @@ -1,244 +0,0 @@ -use crate::{encoding, error::*, format::*}; -use chrono::offset::TimeZone; -use tracing::trace; -use winnow::{ - binary::{le_u16, le_u32}, - prelude::PResult, - token::tag, - Parser, Partial, -}; - -/// 4.3.12 Central directory structure: File header -pub struct DirectoryHeader { - // version made by - pub creator_version: Version, - // version needed to extract - pub reader_version: Version, - // general purpose bit flag - pub flags: u16, - // compression method - pub method: u16, - // last mod file datetime - pub modified: MsdosTimestamp, - // crc32 - pub crc32: u32, - // compressed size - pub compressed_size: u32, - // uncompressed size - pub uncompressed_size: u32, - // disk number start - pub disk_nbr_start: u16, - // internal file attributes - pub internal_attrs: u16, - // external file attributes - pub external_attrs: u32, - // relative offset of local header - pub header_offset: u32, - - // name - pub name: ZipString, - // extra - pub extra: ZipBytes, // comment - pub comment: ZipString, -} - -impl DirectoryHeader { - const SIGNATURE: &'static str = "PK\x01\x02"; - - pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - _ = tag(Self::SIGNATURE).parse_next(i)?; - let creator_version = Version::parser.parse_next(i)?; - let reader_version = Version::parser.parse_next(i)?; - let flags = le_u16.parse_next(i)?; - let method = le_u16.parse_next(i)?; - let modified = MsdosTimestamp::parser.parse_next(i)?; - let crc32 = le_u32.parse_next(i)?; - let compressed_size = le_u32.parse_next(i)?; - let uncompressed_size = le_u32.parse_next(i)?; - let name_len = le_u16.parse_next(i)?; - let extra_len = le_u16.parse_next(i)?; - let comment_len = le_u16.parse_next(i)?; - let disk_nbr_start = le_u16.parse_next(i)?; - let internal_attrs = le_u16.parse_next(i)?; - let external_attrs = le_u32.parse_next(i)?; - let header_offset = le_u32.parse_next(i)?; - - let name = ZipString::parser(name_len).parse_next(i)?; - let extra = ZipBytes::parser(extra_len).parse_next(i)?; - let comment = ZipString::parser(comment_len).parse_next(i)?; - - Ok(Self { - creator_version, - reader_version, - flags, - method, - modified, - crc32, - compressed_size, - uncompressed_size, - disk_nbr_start, - internal_attrs, - external_attrs, - header_offset, - name, - extra, - comment, - }) - } -} - -impl DirectoryHeader { - pub fn is_non_utf8(&self) -> bool { - let (valid1, require1) = encoding::detect_utf8(&self.name.0[..]); - let (valid2, require2) = encoding::detect_utf8(&self.comment.0[..]); - if !valid1 || !valid2 { - // definitely not utf-8 - return true; - } - - if !require1 && !require2 { - // name and comment only use single-byte runes that overlap with UTF-8 - return false; - } - - // Might be UTF-8, might be some other encoding; preserve existing flag. - // Some ZIP writers use UTF-8 encoding without setting the UTF-8 flag. - // Since it is impossible to always distinguish valid UTF-8 from some - // other encoding (e.g., GBK or Shift-JIS), we trust the flag. - self.flags & 0x800 == 0 - } - - pub fn as_stored_entry( - &self, - is_zip64: bool, - encoding: Encoding, - global_offset: u64, - ) -> Result { - let mut comment: Option = None; - if let Some(comment_field) = self.comment.clone().into_option() { - comment = Some(encoding.decode(&comment_field.0)?); - } - - let name = encoding.decode(&self.name.0)?; - - let mut compressed_size = self.compressed_size as u64; - let mut uncompressed_size = self.uncompressed_size as u64; - let mut header_offset = self.header_offset as u64 + global_offset; - - let mut modified: Option> = None; - let mut created: Option> = None; - let mut accessed: Option> = None; - - let mut uid: Option = None; - let mut gid: Option = None; - - let mut extra_fields: Vec = Vec::new(); - - let settings = ExtraFieldSettings { - needs_compressed_size: self.compressed_size == !0u32, - needs_uncompressed_size: self.uncompressed_size == !0u32, - needs_header_offset: self.header_offset == !0u32, - }; - - let mut slice = Partial::new(&self.extra.0[..]); - while !slice.is_empty() { - match ExtraField::mk_parser(settings).parse_next(&mut slice) { - Ok(ef) => { - match &ef { - ExtraField::Zip64(z64) => { - if let Some(n) = z64.uncompressed_size { - uncompressed_size = n; - } - if let Some(n) = z64.compressed_size { - compressed_size = n; - } - if let Some(n) = z64.header_offset { - header_offset = n; - } - } - ExtraField::Timestamp(ts) => { - modified = Utc.timestamp_opt(ts.mtime as i64, 0).single(); - } - ExtraField::Ntfs(nf) => { - for attr in &nf.attrs { - // note: other attributes are unsupported - if let NtfsAttr::Attr1(attr) = attr { - modified = attr.mtime.to_datetime(); - created = attr.ctime.to_datetime(); - accessed = attr.atime.to_datetime(); - } - } - } - ExtraField::Unix(uf) => { - modified = Utc.timestamp_opt(uf.mtime as i64, 0).single(); - if uid.is_none() { - uid = Some(uf.uid as u32); - } - if gid.is_none() { - gid = Some(uf.gid as u32); - } - } - ExtraField::NewUnix(uf) => { - uid = Some(uf.uid as u32); - gid = Some(uf.uid as u32); - } - _ => {} - }; - extra_fields.push(ef); - } - Err(e) => { - trace!("extra field error: {:#?}", e); - return Err(FormatError::InvalidExtraField.into()); - } - } - } - - let modified = match modified { - Some(m) => Some(m), - None => self.modified.to_datetime(), - }; - - let mut mode: Mode = match self.creator_version.host_system() { - HostSystem::Unix | HostSystem::Osx => UnixMode(self.external_attrs >> 16).into(), - HostSystem::WindowsNtfs | HostSystem::Vfat | HostSystem::MsDos => { - MsdosMode(self.external_attrs).into() - } - _ => Mode(0), - }; - if name.ends_with('/') { - // believe it or not, this is straight from the APPNOTE - mode |= Mode::DIR - }; - - Ok(StoredEntry { - entry: Entry { - name, - method: self.method.into(), - comment, - modified: modified.unwrap_or_else(zero_datetime), - created, - accessed, - }, - - creator_version: self.creator_version, - reader_version: self.reader_version, - flags: self.flags, - - inner: StoredEntryInner { - crc32: self.crc32, - compressed_size, - uncompressed_size, - is_zip64, - }, - header_offset, - - uid, - gid, - mode, - - extra_fields, - - external_attrs: self.external_attrs, - }) - } -} diff --git a/rc-zip/src/format/eocd.rs b/rc-zip/src/format/eocd.rs deleted file mode 100644 index cc1c7f5..0000000 --- a/rc-zip/src/format/eocd.rs +++ /dev/null @@ -1,263 +0,0 @@ -use crate::{error::*, format::*}; -use tracing::trace; -use winnow::{ - binary::{le_u16, le_u32, le_u64, length_take}, - seq, - token::tag, - PResult, Parser, Partial, -}; - -/// 4.3.16 End of central directory record: -#[derive(Debug)] -pub struct EndOfCentralDirectoryRecord { - /// number of this disk - pub disk_nbr: u16, - /// number of the disk with the start of the central directory - pub dir_disk_nbr: u16, - /// total number of entries in the central directory on this disk - pub dir_records_this_disk: u16, - /// total number of entries in the central directory - pub directory_records: u16, - // size of the central directory - pub directory_size: u32, - /// offset of start of central directory with respect to the starting disk number - pub directory_offset: u32, - /// .ZIP file comment - pub comment: ZipString, -} - -impl EndOfCentralDirectoryRecord { - /// Does not include comment size & comment data - const MIN_LENGTH: usize = 20; - const SIGNATURE: &'static str = "PK\x05\x06"; - - pub fn find_in_block(b: &[u8]) -> Option> { - for i in (0..(b.len() - Self::MIN_LENGTH + 1)).rev() { - let mut input = Partial::new(&b[i..]); - if let Ok(directory) = Self::parser.parse_next(&mut input) { - return Some(Located { - offset: i as u64, - inner: directory, - }); - } - } - None - } - - pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - let _ = tag(Self::SIGNATURE).parse_next(i)?; - seq! {Self { - disk_nbr: le_u16, - dir_disk_nbr: le_u16, - dir_records_this_disk: le_u16, - directory_records: le_u16, - directory_size: le_u32, - directory_offset: le_u32, - comment: length_take(le_u16).map(ZipString::from), - }} - .parse_next(i) - } -} - -/// 4.3.15 Zip64 end of central directory locator -#[derive(Debug)] -pub struct EndOfCentralDirectory64Locator { - /// number of the disk with the start of the zip64 end of central directory - pub dir_disk_number: u32, - /// relative offset of the zip64 end of central directory record - pub directory_offset: u64, - /// total number of disks - pub total_disks: u32, -} - -impl EndOfCentralDirectory64Locator { - pub const LENGTH: usize = 20; - const SIGNATURE: &'static str = "PK\x06\x07"; - - pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - _ = tag(Self::SIGNATURE).parse_next(i)?; - seq! {Self { - dir_disk_number: le_u32, - directory_offset: le_u64, - total_disks: le_u32, - }} - .parse_next(i) - } -} - -/// 4.3.14 Zip64 end of central directory record -#[derive(Debug)] -pub struct EndOfCentralDirectory64Record { - /// size of zip64 end of central directory record - pub record_size: u64, - /// version made by - pub creator_version: u16, - /// version needed to extract - pub reader_version: u16, - /// number of this disk - pub disk_nbr: u32, - /// number of the disk with the start of the central directory - pub dir_disk_nbr: u32, - // total number of entries in the central directory on this disk - pub dir_records_this_disk: u64, - // total number of entries in the central directory - pub directory_records: u64, - // size of the central directory - pub directory_size: u64, - // offset of the start of central directory with respect to the - // starting disk number - pub directory_offset: u64, -} - -impl EndOfCentralDirectory64Record { - const SIGNATURE: &'static str = "PK\x06\x06"; - - pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - _ = tag(Self::SIGNATURE).parse_next(i)?; - seq! {Self { - record_size: le_u64, - creator_version: le_u16, - reader_version: le_u16, - disk_nbr: le_u32, - dir_disk_nbr: le_u32, - dir_records_this_disk: le_u64, - directory_records: le_u64, - directory_size: le_u64, - directory_offset: le_u64, - }} - .parse_next(i) - } -} - -#[derive(Debug)] -pub struct Located { - pub offset: u64, - pub inner: T, -} - -impl std::ops::Deref for Located { - type Target = T; - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl std::ops::DerefMut for Located { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} - -/// Coalesces zip and zip64 "end of central directory" record info -pub struct EndOfCentralDirectory { - pub dir: Located, - pub dir64: Option>, - pub global_offset: i64, -} - -impl EndOfCentralDirectory { - pub fn new( - size: u64, - dir: Located, - dir64: Option>, - ) -> Result { - let mut res = Self { - dir, - dir64, - global_offset: 0, - }; - - // - // Pure .zip files look like this: - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // <------directory_size-----> - // [ Data 1 ][ Data 2 ][ Central directory ][ ??? ] - // ^ ^ ^ - // 0 directory_offset directory_end_offset - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // - // But there exist some valid zip archives with padding at the beginning, like so: - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // <--global_offset-> <------directory_size-----> - // [ Padding ][ Data 1 ][ Data 2 ][ Central directory ][ ??? ] - // ^ ^ ^ ^ - // 0 global_offset computed_directory_offset directory_end_offset - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // - // (e.g. https://www.icculus.org/mojosetup/ installers are ELF binaries with a .zip file appended) - // - // `directory_end_offfset` is found by scanning the file (so it accounts for padding), but - // `directory_offset` is found by reading a data structure (so it does not account for padding). - // If we just trusted `directory_offset`, we'd be reading the central directory at the wrong place: - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // <------directory_size-----> - // [ Padding ][ Data 1 ][ Data 2 ][ Central directory ][ ??? ] - // ^ ^ ^ - // 0 directory_offset - woops! directory_end_offset - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - let computed_directory_offset = res.located_directory_offset() - res.directory_size(); - - // did we find a valid offset? - if (0..size).contains(&computed_directory_offset) { - // that's different from the recorded one? - if computed_directory_offset != res.directory_offset() { - // then assume the whole file is offset - res.global_offset = - computed_directory_offset as i64 - res.directory_offset() as i64; - res.set_directory_offset(computed_directory_offset); - } - } - - // make sure directory_offset points to somewhere in our file - trace!( - "directory offset = {}, valid range = 0..{}", - res.directory_offset(), - size - ); - if !(0..size).contains(&res.directory_offset()) { - return Err(FormatError::DirectoryOffsetPointsOutsideFile.into()); - } - - Ok(res) - } - - pub fn located_directory_offset(&self) -> u64 { - match self.dir64.as_ref() { - Some(d64) => d64.offset, - None => self.dir.offset, - } - } - - pub fn directory_offset(&self) -> u64 { - match self.dir64.as_ref() { - Some(d64) => d64.directory_offset, - None => self.dir.directory_offset as u64, - } - } - - pub fn directory_size(&self) -> u64 { - match self.dir64.as_ref() { - Some(d64) => d64.directory_size, - None => self.dir.directory_size as u64, - } - } - - pub fn set_directory_offset(&mut self, offset: u64) { - match self.dir64.as_mut() { - Some(d64) => d64.directory_offset = offset, - None => self.dir.directory_offset = offset as u32, - }; - } - - pub fn directory_records(&self) -> u64 { - match self.dir64.as_ref() { - Some(d64) => d64.directory_records, - None => self.dir.directory_records as u64, - } - } - - pub fn comment(&self) -> &ZipString { - &self.dir.comment - } -} diff --git a/rc-zip/src/format/extra_field.rs b/rc-zip/src/format/extra_field.rs deleted file mode 100644 index c8d8627..0000000 --- a/rc-zip/src/format/extra_field.rs +++ /dev/null @@ -1,289 +0,0 @@ -use crate::format::*; -use tracing::trace; -use winnow::{ - binary::{le_u16, le_u32, le_u64, le_u8, length_take}, - combinator::{cond, opt, preceded, repeat_till}, - error::{ErrMode, ErrorKind, ParserError, StrContext}, - seq, - token::{tag, take}, - PResult, Parser, Partial, -}; -/// 4.4.28 extra field: (Variable) -pub(crate) struct ExtraFieldRecord<'a> { - pub(crate) tag: u16, - pub(crate) payload: &'a [u8], -} - -impl<'a> ExtraFieldRecord<'a> { - pub(crate) fn parser(i: &mut Partial<&'a [u8]>) -> PResult { - seq! {Self { - tag: le_u16, - payload: length_take(le_u16), - }} - .parse_next(i) - } -} - -// Useful because zip64 extended information extra field has fixed order *but* -// optional fields. From the appnote: -// -// If one of the size or offset fields in the Local or Central directory record -// is too small to hold the required data, a Zip64 extended information record -// is created. The order of the fields in the zip64 extended information record -// is fixed, but the fields MUST only appear if the corresponding Local or -// Central directory record field is set to 0xFFFF or 0xFFFFFFFF. -#[derive(Debug, Clone, Copy)] -pub(crate) struct ExtraFieldSettings { - pub(crate) needs_uncompressed_size: bool, - pub(crate) needs_compressed_size: bool, - pub(crate) needs_header_offset: bool, -} - -/// Information stored in the central directory header `extra` field -/// -/// This typically contains timestamps, file sizes and offsets, file mode, uid/gid, etc. -/// -/// See `extrafld.txt` in this crate's source distribution. -#[derive(Clone)] -pub enum ExtraField { - /// Zip64 extended information extra field - Zip64(ExtraZip64Field), - /// Extended timestamp - Timestamp(ExtraTimestampField), - /// UNIX & Info-Zip UNIX - Unix(ExtraUnixField), - /// New UNIX extra field - NewUnix(ExtraNewUnixField), - /// NTFS (Win9x/WinNT FileTimes) - Ntfs(ExtraNtfsField), - /// Unknown extra field, with tag - Unknown { tag: u16 }, -} - -impl ExtraField { - pub(crate) fn mk_parser( - settings: ExtraFieldSettings, - ) -> impl FnMut(&mut Partial<&'_ [u8]>) -> PResult { - move |i| { - use ExtraField as EF; - let rec = ExtraFieldRecord::parser.parse_next(i)?; - trace!("parsing extra field record, tag {:04x}", rec.tag); - let payload = &mut Partial::new(rec.payload); - - let variant = match rec.tag { - ExtraZip64Field::TAG => opt(ExtraZip64Field::mk_parser(settings).map(EF::Zip64)) - .context(StrContext::Label("zip64")) - .parse_next(payload)?, - ExtraTimestampField::TAG => opt(ExtraTimestampField::parser.map(EF::Timestamp)) - .context(StrContext::Label("timestamp")) - .parse_next(payload)?, - ExtraNtfsField::TAG => { - opt(ExtraNtfsField::parse.map(EF::Ntfs)).parse_next(payload)? - } - ExtraUnixField::TAG | ExtraUnixField::TAG_INFOZIP => { - opt(ExtraUnixField::parser.map(EF::Unix)).parse_next(payload)? - } - ExtraNewUnixField::TAG => { - opt(ExtraNewUnixField::parser.map(EF::NewUnix)).parse_next(payload)? - } - _ => None, - } - .unwrap_or(EF::Unknown { tag: rec.tag }); - - Ok(variant) - } - } -} - -/// 4.5.3 -Zip64 Extended Information Extra Field (0x0001) -#[derive(Clone, Default)] -pub struct ExtraZip64Field { - pub uncompressed_size: Option, - pub compressed_size: Option, - pub header_offset: Option, -} - -impl ExtraZip64Field { - const TAG: u16 = 0x0001; - - pub(crate) fn mk_parser( - settings: ExtraFieldSettings, - ) -> impl FnMut(&mut Partial<&'_ [u8]>) -> PResult { - move |i| { - // N.B: we ignore "disk start number" - seq! {Self { - uncompressed_size: cond(settings.needs_uncompressed_size, le_u64), - compressed_size: cond(settings.needs_compressed_size, le_u64), - header_offset: cond(settings.needs_header_offset, le_u64), - }} - .parse_next(i) - } - } -} - -/// Extended timestamp extra field -#[derive(Clone)] -pub struct ExtraTimestampField { - /// number of seconds since epoch - pub mtime: u32, -} - -impl ExtraTimestampField { - const TAG: u16 = 0x5455; - - fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - preceded( - // 1 byte of flags, if bit 0 is set, modification time is present - le_u8.verify(|x| x & 0b1 != 0), - seq! {Self { mtime: le_u32 }}, - ) - .parse_next(i) - } -} - -/// 4.5.7 -UNIX Extra Field (0x000d): -#[derive(Clone)] -pub struct ExtraUnixField { - /// file last access time - pub atime: u32, - /// file last modification time - pub mtime: u32, - /// file user id - pub uid: u16, - /// file group id - pub gid: u16, - /// variable length data field - pub data: ZipBytes, -} - -impl ExtraUnixField { - const TAG: u16 = 0x000d; - const TAG_INFOZIP: u16 = 0x5855; - - fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - let t_size = le_u16.parse_next(i)? - 12; - seq! {Self { - atime: le_u32, - mtime: le_u32, - uid: le_u16, - gid: le_u16, - data: ZipBytes::parser(t_size), - }} - .parse_next(i) - } -} - -/// Info-ZIP New Unix Extra Field: -/// ==================================== -/// -/// Currently stores Unix UIDs/GIDs up to 32 bits. -/// (Last Revision 20080509) -/// -/// ```text -/// Value Size Description -/// ----- ---- ----------- -/// 0x7875 Short tag for this extra block type ("ux") -/// TSize Short total data size for this block -/// Version 1 byte version of this extra field, currently 1 -/// UIDSize 1 byte Size of UID field -/// UID Variable UID for this entry -/// GIDSize 1 byte Size of GID field -/// GID Variable GID for this entry -/// ``` -#[derive(Clone)] -pub struct ExtraNewUnixField { - pub uid: u64, - pub gid: u64, -} - -impl ExtraNewUnixField { - const TAG: u16 = 0x7875; - - fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - let _ = tag("\x01").parse_next(i)?; - seq! {Self { - uid: Self::parse_variable_length_integer, - gid: Self::parse_variable_length_integer, - }} - .parse_next(i) - } - - fn parse_variable_length_integer(i: &mut Partial<&'_ [u8]>) -> PResult { - let slice = length_take(le_u8).parse_next(i)?; - if let Some(u) = match slice.len() { - 1 => Some(le_u8.parse_peek(slice)?.1 as u64), - 2 => Some(le_u16.parse_peek(slice)?.1 as u64), - 4 => Some(le_u32.parse_peek(slice)?.1 as u64), - 8 => Some(le_u64.parse_peek(slice)?.1), - _ => None, - } { - Ok(u) - } else { - Err(ErrMode::from_error_kind(i, ErrorKind::Alt)) - } - } -} - -/// 4.5.5 -NTFS Extra Field (0x000a): -#[derive(Clone)] -pub struct ExtraNtfsField { - pub attrs: Vec, -} - -impl ExtraNtfsField { - const TAG: u16 = 0x000a; - - fn parse(i: &mut Partial<&'_ [u8]>) -> PResult { - let _ = take(4_usize).parse_next(i)?; // reserved (unused) - seq! {Self { - // from the winnow docs: - // Parsers like repeat do not know when an eof is from insufficient - // data or the end of the stream, causing them to always report - // Incomplete. - // using repeat_till with eof combinator to work around this: - attrs: repeat_till(0.., NtfsAttr::parse, winnow::combinator::eof).map(|x| x.0), - }} - .parse_next(i) - } -} - -/// NTFS attribute for zip entries (mostly timestamps) -#[derive(Clone)] -pub enum NtfsAttr { - Attr1(NtfsAttr1), - Unknown { tag: u16 }, -} - -impl NtfsAttr { - fn parse(i: &mut Partial<&'_ [u8]>) -> PResult { - let tag = le_u16.parse_next(i)?; - trace!("parsing NTFS attribute, tag {:04x}", tag); - let payload = length_take(le_u16).parse_next(i)?; - - match tag { - 0x0001 => NtfsAttr1::parser - .parse_peek(Partial::new(payload)) - .map(|(_, attr)| NtfsAttr::Attr1(attr)), - _ => Ok(NtfsAttr::Unknown { tag }), - } - } -} - -#[derive(Clone)] -pub struct NtfsAttr1 { - pub mtime: NtfsTimestamp, - pub atime: NtfsTimestamp, - pub ctime: NtfsTimestamp, -} - -impl NtfsAttr1 { - fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - trace!("parsing NTFS attr 1, input len is {}", i.len()); - seq! {Self { - mtime: NtfsTimestamp::parser, - atime: NtfsTimestamp::parser, - ctime: NtfsTimestamp::parser, - }} - .parse_next(i) - } -} diff --git a/rc-zip/src/format/local.rs b/rc-zip/src/format/local.rs deleted file mode 100644 index 2c43c43..0000000 --- a/rc-zip/src/format/local.rs +++ /dev/null @@ -1,188 +0,0 @@ -use crate::{format::*, Error, UnsupportedError}; -use winnow::{ - binary::{le_u16, le_u32, le_u64, le_u8}, - combinator::opt, - error::{ContextError, ErrMode, ErrorKind, FromExternalError}, - seq, - token::tag, - PResult, Parser, Partial, -}; - -#[derive(Debug)] -/// 4.3.7 Local file header -pub struct LocalFileHeaderRecord { - /// version needed to extract - pub reader_version: Version, - /// general purpose bit flag - pub flags: u16, - /// compression method - pub method: Method, - /// last mod file datetime - pub modified: MsdosTimestamp, - /// crc-32 - pub crc32: u32, - /// compressed size - pub compressed_size: u32, - /// uncompressed size - pub uncompressed_size: u32, - // file name - pub name: ZipString, - // extra field - pub extra: ZipBytes, - - // method-specific fields - pub method_specific: MethodSpecific, -} - -#[derive(Debug)] -/// Method-specific properties following the local file header -pub enum MethodSpecific { - None, - Lzma(LzmaProperties), -} - -impl LocalFileHeaderRecord { - pub const SIGNATURE: &'static str = "PK\x03\x04"; - - pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - let _ = tag(Self::SIGNATURE).parse_next(i)?; - - let reader_version = Version::parser.parse_next(i)?; - let flags = le_u16.parse_next(i)?; - let method = le_u16.parse_next(i).map(Method::from)?; - let modified = MsdosTimestamp::parser.parse_next(i)?; - let crc32 = le_u32.parse_next(i)?; - let compressed_size = le_u32.parse_next(i)?; - let uncompressed_size = le_u32.parse_next(i)?; - - let name_len = le_u16.parse_next(i)?; - let extra_len = le_u16.parse_next(i)?; - - let name = ZipString::parser(name_len).parse_next(i)?; - let extra = ZipBytes::parser(extra_len).parse_next(i)?; - - let method_specific = match method { - Method::Lzma => { - let lzma_properties = LzmaProperties::parser.parse_next(i)?; - if let Err(e) = lzma_properties.error_if_unsupported() { - return Err(ErrMode::Cut(ContextError::from_external_error( - i, - ErrorKind::Verify, - e, - ))); - } - MethodSpecific::Lzma(lzma_properties) - } - _ => MethodSpecific::None, - }; - - Ok(Self { - reader_version, - flags, - method, - modified, - crc32, - compressed_size, - uncompressed_size, - name, - extra, - method_specific, - }) - } - - pub fn has_data_descriptor(&self) -> bool { - // 4.3.9.1 This descriptor MUST exist if bit 3 of the general - // purpose bit flag is set (see below). - self.flags & 0b1000 != 0 - } -} - -/// 4.3.9 Data descriptor: -#[derive(Debug)] -pub struct DataDescriptorRecord { - /// CRC32 checksum - pub crc32: u32, - /// Compressed size - pub compressed_size: u64, - /// Uncompressed size - pub uncompressed_size: u64, -} - -impl DataDescriptorRecord { - const SIGNATURE: &'static str = "PK\x07\x08"; - - pub fn mk_parser(is_zip64: bool) -> impl FnMut(&mut Partial<&'_ [u8]>) -> PResult { - move |i| { - // From appnote.txt: - // - // 4.3.9.3 Although not originally assigned a signature, the value - // 0x08074b50 has commonly been adopted as a signature value for the - // data descriptor record. Implementers SHOULD be aware that ZIP files - // MAY be encountered with or without this signature marking data - // descriptors and SHOULD account for either case when reading ZIP files - // to ensure compatibility. - let _ = opt(tag(Self::SIGNATURE)).parse_next(i)?; - - if is_zip64 { - seq! {Self { - crc32: le_u32, - compressed_size: le_u64, - uncompressed_size: le_u64, - }} - .parse_next(i) - } else { - seq! {Self { - crc32: le_u32, - compressed_size: le_u32.map(|x| x as u64), - uncompressed_size: le_u32.map(|x| x as u64), - }} - .parse_next(i) - } - } - } -} - -/// 5.8.5 LZMA Properties header -#[derive(Debug)] -pub struct LzmaProperties { - /// major version - pub major: u8, - /// minor version - pub minor: u8, - /// properties size - pub properties_size: u16, -} - -impl LzmaProperties { - pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - seq! {Self { - major: le_u8, - minor: le_u8, - properties_size: le_u16, - }} - .parse_next(i) - } - - pub fn error_if_unsupported(&self) -> Result<(), Error> { - if (self.major, self.minor) != (2, 0) { - return Err(Error::Unsupported( - UnsupportedError::LzmaVersionUnsupported { - minor: self.minor, - major: self.major, - }, - )); - } - - const LZMA_PROPERTIES_SIZE: u16 = 5; - if self.properties_size != LZMA_PROPERTIES_SIZE { - return Err(Error::Unsupported( - UnsupportedError::LzmaPropertiesHeaderWrongSize { - expected: 5, - actual: self.properties_size, - }, - )); - } - - Ok(()) - } -} diff --git a/rc-zip/src/format/mod.rs b/rc-zip/src/format/mod.rs deleted file mode 100644 index 541edc8..0000000 --- a/rc-zip/src/format/mod.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Contain winnow parsers for most elements that make up a ZIP file, like -//! the end-of-central-directory record, local file headers, and central -//! directory headers. -//! -//! Everything in there is based off of the appnote, which you can find in the -//! source repository. - -pub use crate::encoding::Encoding; - -mod archive; -mod extra_field; -mod mode; -mod version; -pub use self::{archive::*, extra_field::*, mode::*, version::*}; - -mod date_time; -mod directory_header; -mod eocd; -mod local; -mod raw; -pub use self::{date_time::*, directory_header::*, eocd::*, local::*, raw::*}; - -use chrono::{offset::Utc, DateTime}; diff --git a/rc-zip/src/format/mode.rs b/rc-zip/src/format/mode.rs deleted file mode 100644 index 1baff51..0000000 --- a/rc-zip/src/format/mode.rs +++ /dev/null @@ -1,239 +0,0 @@ -use std::fmt; - -/// Mode represents a file's mode and permission bits. -/// The bits have the same definition on all systems, -/// but not all bits apply to all systems. -/// -/// It is modelled after Go's `os.FileMode`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Mode(pub u32); - -impl Mode { - /// d: is a directory - pub const DIR: Self = Self(1 << 31); - /// a: append-only - pub const APPEND: Self = Self(1 << 30); - /// l: exclusive use - pub const EXCLUSIVE: Self = Self(1 << 29); - /// T: temporary file; Plan 9 only - pub const TEMPORARY: Self = Self(1 << 28); - /// L: symbolic link - pub const SYMLINK: Self = Self(1 << 27); - /// D: device file - pub const DEVICE: Self = Self(1 << 26); - /// p: named pipe (FIFO) - pub const NAMED_PIPE: Self = Self(1 << 25); - /// S: Unix domain socket - pub const SOCKET: Self = Self(1 << 24); - /// u: setuid - pub const SETUID: Self = Self(1 << 23); - /// g: setgid - pub const SETGID: Self = Self(1 << 22); - /// c: Unix character device, when DEVICE is set - pub const CHAR_DEVICE: Self = Self(1 << 21); - /// t: sticky - pub const STICKY: Self = Self(1 << 20); - /// ?: non-regular file; nothing else is known - pub const IRREGULAR: Self = Self(1 << 19); -} - -impl fmt::Display for Mode { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let mut w = 0; - if self.has(Self::DIR) { - write!(f, "d")?; - w += 1; - } - if self.has(Self::APPEND) { - write!(f, "a")?; - w += 1; - } - if self.has(Self::EXCLUSIVE) { - write!(f, "l")?; - w += 1; - } - if self.has(Self::TEMPORARY) { - write!(f, "T")?; - w += 1; - } - if self.has(Self::SYMLINK) { - write!(f, "L")?; - w += 1; - } - if self.has(Self::DEVICE) { - write!(f, "D")?; - w += 1; - } - if self.has(Self::NAMED_PIPE) { - write!(f, "p")?; - w += 1; - } - if self.has(Self::SOCKET) { - write!(f, "S")?; - w += 1; - } - if self.has(Self::SETUID) { - write!(f, "u")?; - w += 1; - } - if self.has(Self::SETGID) { - write!(f, "g")?; - w += 1; - } - if self.has(Self::CHAR_DEVICE) { - write!(f, "c")?; - w += 1; - } - if self.has(Self::STICKY) { - write!(f, "t")?; - w += 1; - } - if self.has(Self::IRREGULAR) { - write!(f, "?")?; - w += 1; - } - if w == 0 { - write!(f, "-")?; - } - - let rwx = "rwxrwxrwx"; - for (i, c) in rwx.char_indices() { - if self.has(Mode(1 << (9 - 1 - i))) { - write!(f, "{}", c)?; - } else { - write!(f, "-")?; - } - } - - Ok(()) - } -} - -impl From for Mode { - fn from(m: UnixMode) -> Self { - let mut mode = Mode(m.0 & 0o777); - - match m & UnixMode::IFMT { - UnixMode::IFBLK => mode |= Mode::DEVICE, - UnixMode::IFCHR => mode |= Mode::DEVICE & Mode::CHAR_DEVICE, - UnixMode::IFDIR => mode |= Mode::DIR, - UnixMode::IFIFO => mode |= Mode::NAMED_PIPE, - UnixMode::IFLNK => mode |= Mode::SYMLINK, - UnixMode::IFREG => { /* nothing to do */ } - UnixMode::IFSOCK => mode |= Mode::SOCKET, - _ => {} - } - - if m.has(UnixMode::ISGID) { - mode |= Mode::SETGID - } - if m.has(UnixMode::ISUID) { - mode |= Mode::SETUID - } - if m.has(UnixMode::ISVTX) { - mode |= Mode::STICKY - } - - mode - } -} - -impl From for Mode { - fn from(m: MsdosMode) -> Self { - let mut mode = if m.has(MsdosMode::DIR) { - Mode::DIR | Mode(0o777) - } else { - Mode(0o666) - }; - if m.has(MsdosMode::READ_ONLY) { - mode &= Mode(0o222); - } - - mode - } -} - -impl From for Mode { - fn from(u: u32) -> Self { - Mode(u) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct UnixMode(pub u32); - -impl UnixMode { - pub const IFMT: Self = Self(0xf000); - pub const IFSOCK: Self = Self(0xc000); - pub const IFLNK: Self = Self(0xa000); - pub const IFREG: Self = Self(0x8000); - pub const IFBLK: Self = Self(0x6000); - pub const IFDIR: Self = Self(0x4000); - pub const IFCHR: Self = Self(0x2000); - pub const IFIFO: Self = Self(0x1000); - pub const ISUID: Self = Self(0x800); - pub const ISGID: Self = Self(0x400); - pub const ISVTX: Self = Self(0x200); -} - -impl From for UnixMode { - fn from(u: u32) -> Self { - UnixMode(u) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct MsdosMode(pub u32); - -impl MsdosMode { - pub const DIR: Self = Self(0x10); - pub const READ_ONLY: Self = Self(0x01); -} - -impl From for MsdosMode { - fn from(u: u32) -> Self { - MsdosMode(u) - } -} - -macro_rules! derive_bitops { - ($T: ty) => { - impl std::ops::BitOr for $T { - type Output = Self; - - fn bitor(self, rhs: Self) -> Self { - Self(self.0 | rhs.0) - } - } - - impl std::ops::BitOrAssign for $T { - fn bitor_assign(&mut self, rhs: Self) { - self.0 |= rhs.0; - } - } - - impl std::ops::BitAnd for $T { - type Output = Self; - - fn bitand(self, rhs: Self) -> Self { - Self(self.0 & rhs.0) - } - } - - impl std::ops::BitAndAssign for $T { - fn bitand_assign(&mut self, rhs: Self) { - self.0 &= rhs.0; - } - } - - impl $T { - pub fn has(&self, rhs: Self) -> bool { - self.0 & rhs.0 != 0 - } - } - }; -} - -derive_bitops!(Mode); -derive_bitops!(UnixMode); -derive_bitops!(MsdosMode); diff --git a/rc-zip/src/format/raw.rs b/rc-zip/src/format/raw.rs deleted file mode 100644 index fb978ab..0000000 --- a/rc-zip/src/format/raw.rs +++ /dev/null @@ -1,77 +0,0 @@ -use pretty_hex::PrettyHex; -use std::fmt; -use winnow::{stream::ToUsize, token::take, PResult, Parser, Partial}; - -/// A raw zip string, with no specific encoding. -/// -/// This is used while parsing a zip archive's central directory, -/// before we know what encoding is used. -#[derive(Clone)] -pub struct ZipString(pub Vec); - -impl<'a> From<&'a [u8]> for ZipString { - fn from(slice: &'a [u8]) -> Self { - Self(slice.into()) - } -} - -impl fmt::Debug for ZipString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match std::str::from_utf8(&self.0) { - Ok(s) => write!(f, "{:?}", s), - Err(_) => write!(f, "[non-utf8 string: {}]", self.0.hex_dump()), - } - } -} - -impl ZipString { - pub(crate) fn parser(count: C) -> impl FnMut(&mut Partial<&'_ [u8]>) -> PResult - where - C: ToUsize, - { - let count = count.to_usize(); - move |i| (take(count).map(|slice: &[u8]| Self(slice.into()))).parse_next(i) - } - - pub(crate) fn into_option(self) -> Option { - if !self.0.is_empty() { - Some(self) - } else { - None - } - } -} - -/// A raw u8 slice, with no specific structure. -/// -/// This is used while parsing a zip archive, when we want -/// to retain an owned slice to be parsed later. -#[derive(Clone)] -pub struct ZipBytes(pub Vec); - -impl fmt::Debug for ZipBytes { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - const MAX_SHOWN_SIZE: usize = 10; - let data = &self.0[..]; - let (slice, extra) = if data.len() > MAX_SHOWN_SIZE { - (&self.0[..MAX_SHOWN_SIZE], Some(data.len() - MAX_SHOWN_SIZE)) - } else { - (&self.0[..], None) - }; - write!(f, "{}", slice.hex_dump())?; - if let Some(extra) = extra { - write!(f, " (+ {} bytes)", extra)?; - } - Ok(()) - } -} - -impl ZipBytes { - pub(crate) fn parser(count: C) -> impl FnMut(&mut Partial<&'_ [u8]>) -> PResult - where - C: ToUsize, - { - let count = count.to_usize(); - move |i| (take(count).map(|slice: &[u8]| Self(slice.into()))).parse_next(i) - } -} diff --git a/rc-zip/src/format/version.rs b/rc-zip/src/format/version.rs deleted file mode 100644 index 1b9ac8f..0000000 --- a/rc-zip/src/format/version.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::fmt; -use winnow::{binary::le_u16, PResult, Parser, Partial}; - -/// A zip version (either created by, or required when reading an archive). -/// -/// Versions determine which features are supported by a tool, and -/// which features are required when reading a file. -/// -/// For more information, see the [.ZIP Application Note](https://support.pkware.com/display/PKZIP/APPNOTE), section 4.4.2. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct Version(pub u16); - -impl fmt::Debug for Version { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{:?} v{}.{}", - self.host_system(), - self.major(), - self.minor() - ) - } -} - -impl Version { - /// Parse a version from a byte slice - pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - le_u16.map(Self).parse_next(i) - } - - /// Identifies the host system on which the zip attributes are compatible. - pub fn host_system(&self) -> HostSystem { - match self.host() { - 0 => HostSystem::MsDos, - 1 => HostSystem::Amiga, - 2 => HostSystem::OpenVms, - 3 => HostSystem::Unix, - 4 => HostSystem::VmCms, - 5 => HostSystem::AtariSt, - 6 => HostSystem::Os2Hpfs, - 7 => HostSystem::Macintosh, - 8 => HostSystem::ZSystem, - 9 => HostSystem::CpM, - 10 => HostSystem::WindowsNtfs, - 11 => HostSystem::Mvs, - 12 => HostSystem::Vse, - 13 => HostSystem::AcornRisc, - 14 => HostSystem::Vfat, - 15 => HostSystem::AlternateMvs, - 16 => HostSystem::BeOs, - 17 => HostSystem::Tandem, - 18 => HostSystem::Os400, - 19 => HostSystem::Osx, - n => HostSystem::Unknown(n), - } - } - - /// Integer host system - pub fn host(&self) -> u8 { - (self.0 >> 8) as u8 - } - - /// Integer version, e.g. 45 for Zip version 4.5 - pub fn version(&self) -> u8 { - (self.0 & 0xff) as u8 - } - - /// ZIP specification major version - /// - /// See APPNOTE, section 4.4.2.1 - pub fn major(&self) -> u32 { - self.version() as u32 / 10 - } - - /// ZIP specification minor version - /// - /// See APPNOTE, section 4.4.2.1 - pub fn minor(&self) -> u32 { - self.version() as u32 % 10 - } -} - -/// System on which an archive was created, as encoded into a version u16. -/// -/// See APPNOTE, section 4.4.2.2 -#[derive(Debug)] -pub enum HostSystem { - /// MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems) - MsDos, - /// Amiga - Amiga, - /// OpenVMS - OpenVms, - /// UNIX - Unix, - /// VM/CMS - VmCms, - /// Atari ST - AtariSt, - /// OS/2 H.P.F.S - Os2Hpfs, - /// Macintosh (see `Osx`) - Macintosh, - /// Z-System - ZSystem, - /// CP/M - CpM, - /// Windows NTFS - WindowsNtfs, - /// MVS (OS/390 - Z/OS) - Mvs, - /// VSE - Vse, - /// Acorn Risc - AcornRisc, - /// VFAT - Vfat, - /// alternate MVS - AlternateMvs, - /// BeOS - BeOs, - /// Tandem - Tandem, - /// OS/400 - Os400, - /// OS X (Darwin) - Osx, - /// Unknown host system - /// - /// Values 20 through 255 are currently unused, as of - /// APPNOTE.TXT 6.3.6 (April 26, 2019) - Unknown(u8), -} From ac6319c37cb912b795481c0cc717949ca2bb0ad0 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 16:56:38 +0100 Subject: [PATCH 08/23] Extra fields are present in the local file header too, neat! --- rc-zip-sync/src/entry_reader.rs | 12 ++- rc-zip-sync/src/read_zip.rs | 104 +++++++++++++++++++--- rc-zip-sync/src/streaming_entry_reader.rs | 4 +- rc-zip/src/lib.rs | 3 + rc-zip/src/parse/extra_field.rs | 33 ++++--- 5 files changed, 129 insertions(+), 27 deletions(-) diff --git a/rc-zip-sync/src/entry_reader.rs b/rc-zip-sync/src/entry_reader.rs index 2e078d2..0032d79 100644 --- a/rc-zip-sync/src/entry_reader.rs +++ b/rc-zip-sync/src/entry_reader.rs @@ -3,6 +3,7 @@ use rc_zip::{ parse::StoredEntry, }; use std::io; +use tracing::trace; pub(crate) struct EntryReader where @@ -35,19 +36,23 @@ where }; if fsm.wants_read() { - tracing::trace!("fsm wants read"); + trace!("fsm wants read"); let n = self.rd.read(fsm.space())?; - tracing::trace!("giving fsm {} bytes", n); + trace!("giving fsm {} bytes", n); fsm.fill(n); } else { - tracing::trace!("fsm does not want read"); + trace!("fsm does not want read"); } match fsm.process(buf)? { FsmResult::Continue((fsm, outcome)) => { self.fsm = Some(fsm); + if outcome.bytes_written > 0 { Ok(outcome.bytes_written) + } else if outcome.bytes_read == 0 { + // that's EOF, baby! + Ok(0) } else { // loop, it happens self.read(buf) @@ -55,6 +60,7 @@ where } FsmResult::Done(_) => { // neat! + trace!("fsm done"); Ok(0) } } diff --git a/rc-zip-sync/src/read_zip.rs b/rc-zip-sync/src/read_zip.rs index f98e1e8..845fe9b 100644 --- a/rc-zip-sync/src/read_zip.rs +++ b/rc-zip-sync/src/read_zip.rs @@ -1,8 +1,12 @@ +use rc_zip::chrono::{DateTime, TimeZone, Utc}; use rc_zip::{ - error::Error, + error::{Error, FormatError}, fsm::{ArchiveFsm, FsmResult}, - parse::{Archive, LocalFileHeaderRecord, StoredEntry}, + parse::{ + Archive, ExtraField, ExtraFieldSettings, LocalFileHeaderRecord, NtfsAttr, StoredEntry, + }, }; +use tracing::trace; use winnow::{ error::ErrMode, stream::{AsBytes, Offset}, @@ -43,14 +47,14 @@ where type File = F; fn read_zip_with_size(&self, size: u64) -> Result, Error> { - tracing::trace!(%size, "read_zip_with_size"); + trace!(%size, "read_zip_with_size"); let mut fsm = ArchiveFsm::new(size); loop { if let Some(offset) = fsm.wants_read() { - tracing::trace!(%offset, "read_zip_with_size: wants_read, space len = {}", fsm.space().len()); + trace!(%offset, "read_zip_with_size: wants_read, space len = {}", fsm.space().len()); match self.cursor_at(offset).read(fsm.space()) { Ok(read_bytes) => { - tracing::trace!(%read_bytes, "read_zip_with_size: read"); + trace!(%read_bytes, "read_zip_with_size: read"); if read_bytes == 0 { return Err(Error::IO(std::io::ErrorKind::UnexpectedEof.into())); } @@ -62,7 +66,7 @@ where fsm = match fsm.process()? { FsmResult::Done(archive) => { - tracing::trace!("read_zip_with_size: done"); + trace!("read_zip_with_size: done"); return Ok(SyncArchive { file: self, archive, @@ -251,17 +255,97 @@ where let header = loop { let n = self.read(buf.space())?; - tracing::trace!("read {} bytes into buf for first zip entry", n); + trace!("read {} bytes into buf for first zip entry", n); buf.fill(n); let mut input = Partial::new(buf.data()); match LocalFileHeaderRecord::parser.parse_next(&mut input) { Ok(header) => { let consumed = input.as_bytes().offset_from(&buf.data()); - tracing::trace!(?header, %consumed, "Got local file header record!"); + trace!(?header, %consumed, "Got local file header record!"); // write extra bytes to `/tmp/extra.bin` for debugging - std::fs::write("/tmp/extra.bin", input.as_bytes()).unwrap(); - tracing::trace!("wrote extra bytes to /tmp/extra.bin"); + std::fs::write("/tmp/extra.bin", &header.extra.0).unwrap(); + trace!("wrote extra bytes to /tmp/extra.bin"); + + let mut modified: Option> = None; + let mut created: Option> = None; + let mut accessed: Option> = None; + + let mut compressed_size = header.compressed_size as u64; + let mut uncompressed_size = header.uncompressed_size as u64; + + let mut uid: Option = None; + let mut gid: Option = None; + + let mut extra_fields: Vec = Vec::new(); + + let settings = ExtraFieldSettings { + needs_compressed_size: header.compressed_size == !0u32, + needs_uncompressed_size: header.uncompressed_size == !0u32, + needs_header_offset: false, + }; + + let mut slice = Partial::new(&header.extra.0[..]); + while !slice.is_empty() { + match ExtraField::mk_parser(settings).parse_next(&mut slice) { + Ok(ef) => { + match &ef { + ExtraField::Zip64(z64) => { + if let Some(n) = z64.uncompressed_size { + uncompressed_size = n; + } + if let Some(n) = z64.compressed_size { + compressed_size = n; + } + } + ExtraField::Timestamp(ts) => { + modified = Utc.timestamp_opt(ts.mtime as i64, 0).single(); + } + ExtraField::Ntfs(nf) => { + for attr in &nf.attrs { + // note: other attributes are unsupported + if let NtfsAttr::Attr1(attr) = attr { + modified = attr.mtime.to_datetime(); + created = attr.ctime.to_datetime(); + accessed = attr.atime.to_datetime(); + } + } + } + ExtraField::Unix(uf) => { + modified = Utc.timestamp_opt(uf.mtime as i64, 0).single(); + if uid.is_none() { + uid = Some(uf.uid as u32); + } + if gid.is_none() { + gid = Some(uf.gid as u32); + } + } + ExtraField::NewUnix(uf) => { + uid = Some(uf.uid as u32); + gid = Some(uf.uid as u32); + } + _ => {} + }; + extra_fields.push(ef); + } + Err(e) => { + trace!("extra field error: {:#?}", e); + return Err(FormatError::InvalidExtraField.into()); + } + } + } + + trace!( + ?modified, + ?created, + ?accessed, + ?compressed_size, + ?uncompressed_size, + ?uid, + ?gid, + "parsed extra fields" + ); + break header; } // TODO: keep reading if we don't have enough data diff --git a/rc-zip-sync/src/streaming_entry_reader.rs b/rc-zip-sync/src/streaming_entry_reader.rs index 238e25c..9705987 100644 --- a/rc-zip-sync/src/streaming_entry_reader.rs +++ b/rc-zip-sync/src/streaming_entry_reader.rs @@ -81,6 +81,9 @@ where if outcome.bytes_written > 0 { Ok(outcome.bytes_written) + } else if outcome.bytes_read == 0 { + // that's EOF, baby! + Ok(0) } else { // loop, it happens self.read(buf) @@ -96,7 +99,6 @@ where } // FIXME: read the next local file header here - self.state = State::Finished { remain: fsm_remain }; // neat! diff --git a/rc-zip/src/lib.rs b/rc-zip/src/lib.rs index 50fda8c..adadb93 100644 --- a/rc-zip/src/lib.rs +++ b/rc-zip/src/lib.rs @@ -19,3 +19,6 @@ pub mod parse; #[cfg(any(test, feature = "corpus"))] pub mod corpus; + +// dependencies re-exports +pub use chrono; diff --git a/rc-zip/src/parse/extra_field.rs b/rc-zip/src/parse/extra_field.rs index 9b3693b..fd7434b 100644 --- a/rc-zip/src/parse/extra_field.rs +++ b/rc-zip/src/parse/extra_field.rs @@ -26,19 +26,24 @@ impl<'a> ExtraFieldRecord<'a> { } } -// Useful because zip64 extended information extra field has fixed order *but* -// optional fields. From the appnote: -// -// If one of the size or offset fields in the Local or Central directory record -// is too small to hold the required data, a Zip64 extended information record -// is created. The order of the fields in the zip64 extended information record -// is fixed, but the fields MUST only appear if the corresponding Local or -// Central directory record field is set to 0xFFFF or 0xFFFFFFFF. +/// Useful because zip64 extended information extra field has fixed order *but* +/// optional fields. From the appnote: +/// +/// If one of the size or offset fields in the Local or Central directory record +/// is too small to hold the required data, a Zip64 extended information record +/// is created. The order of the fields in the zip64 extended information record +/// is fixed, but the fields MUST only appear if the corresponding Local or +/// Central directory record field is set to 0xFFFF or 0xFFFFFFFF. #[derive(Debug, Clone, Copy)] -pub(crate) struct ExtraFieldSettings { - pub(crate) needs_uncompressed_size: bool, - pub(crate) needs_compressed_size: bool, - pub(crate) needs_header_offset: bool, +pub struct ExtraFieldSettings { + /// Whether the "zip64 extra field" uncompressed size field is needed/present + pub needs_uncompressed_size: bool, + + /// Whether the "zip64 extra field" compressed size field is needed/present + pub needs_compressed_size: bool, + + /// Whether the "zip64 extra field" header offset field is needed/present + pub needs_header_offset: bool, } /// Information stored in the central directory header `extra` field @@ -66,7 +71,9 @@ pub enum ExtraField { } impl ExtraField { - pub(crate) fn mk_parser( + /// Make a parser for extra fields, given the settings for the zip64 extra + /// field (which depend on whether the u32 values are 0xFFFF_FFFF or not) + pub fn mk_parser( settings: ExtraFieldSettings, ) -> impl FnMut(&mut Partial<&'_ [u8]>) -> PResult { move |i| { From d713ee8f81642b62d2bacf2875e4db9cce2426f8 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 19:22:13 +0100 Subject: [PATCH 09/23] Start unifying local / central file headers --- rc-zip-sync/examples/jean.rs | 20 +- rc-zip-sync/src/lib.rs | 2 +- rc-zip-sync/src/read_zip.rs | 20 +- rc-zip-sync/src/streaming_entry_reader.rs | 6 +- rc-zip/src/corpus/mod.rs | 18 +- rc-zip/src/fsm/archive.rs | 12 +- rc-zip/src/fsm/entry/mod.rs | 19 +- rc-zip/src/parse/archive.rs | 217 ++++++++---------- ...er.rs => central_directory_file_header.rs} | 168 ++++---------- rc-zip/src/parse/extra_field.rs | 71 ++++-- .../src/parse/{local.rs => local_headers.rs} | 4 +- rc-zip/src/parse/mod.rs | 8 +- rc-zip/src/parse/raw.rs | 8 - rc-zip/src/parse/version.rs | 135 +++++------ 14 files changed, 303 insertions(+), 405 deletions(-) rename rc-zip/src/parse/{directory_header.rs => central_directory_file_header.rs} (51%) rename rc-zip/src/parse/{local.rs => local_headers.rs} (98%) diff --git a/rc-zip-sync/examples/jean.rs b/rc-zip-sync/examples/jean.rs index be5458a..cff7351 100644 --- a/rc-zip-sync/examples/jean.rs +++ b/rc-zip-sync/examples/jean.rs @@ -1,7 +1,7 @@ use cfg_if::cfg_if; use clap::{Parser, Subcommand}; use humansize::{format_size, BINARY}; -use rc_zip::parse::{Archive, EntryContents, Method, Version}; +use rc_zip::parse::{Archive, EntryKind, Method, Version}; use rc_zip_sync::{ReadZip, ReadZipEntriesStreaming}; use std::{ @@ -102,14 +102,14 @@ fn do_main(cli: Cli) -> Result<(), Box> { for entry in archive.entries() { creator_versions.insert(entry.creator_version); reader_versions.insert(entry.reader_version); - match entry.contents() { - EntryContents::Symlink => { + match entry.kind() { + EntryKind::Symlink => { num_symlinks += 1; } - EntryContents::Directory => { + EntryKind::Directory => { num_dirs += 1; } - EntryContents::File => { + EntryKind::File => { methods.insert(entry.method()); num_files += 1; compressed_size += entry.inner.compressed_size; @@ -166,7 +166,7 @@ fn do_main(cli: Cli) -> Result<(), Box> { gid = Optional(entry.gid), ); - if let EntryContents::Symlink = entry.contents() { + if let EntryKind::Symlink = entry.contents() { let mut target = String::new(); entry.reader().read_to_string(&mut target).unwrap(); print!("\t{target}", target = target); @@ -193,7 +193,7 @@ fn do_main(cli: Cli) -> Result<(), Box> { let mut num_symlinks = 0; let mut uncompressed_size: u64 = 0; for entry in reader.entries() { - if let EntryContents::File = entry.contents() { + if let EntryKind::File = entry.contents() { uncompressed_size += entry.inner.uncompressed_size; } } @@ -220,7 +220,7 @@ fn do_main(cli: Cli) -> Result<(), Box> { pbar.set_message(entry_name.to_string()); match entry.contents() { - EntryContents::Symlink => { + EntryKind::Symlink => { num_symlinks += 1; cfg_if! { @@ -256,7 +256,7 @@ fn do_main(cli: Cli) -> Result<(), Box> { } } } - EntryContents::Directory => { + EntryKind::Directory => { num_dirs += 1; let path = dir.join(entry_name); std::fs::create_dir_all( @@ -264,7 +264,7 @@ fn do_main(cli: Cli) -> Result<(), Box> { .expect("all full entry paths should have parent paths"), )?; } - EntryContents::File => { + EntryKind::File => { num_files += 1; let path = dir.join(entry_name); std::fs::create_dir_all( diff --git a/rc-zip-sync/src/lib.rs b/rc-zip-sync/src/lib.rs index 1bb0ef1..469f489 100644 --- a/rc-zip-sync/src/lib.rs +++ b/rc-zip-sync/src/lib.rs @@ -14,5 +14,5 @@ mod streaming_entry_reader; // re-exports pub use rc_zip; pub use read_zip::{ - HasCursor, ReadZip, ReadZipEntriesStreaming, ReadZipWithSize, SyncArchive, SyncStoredEntry, + HasCursor, ReadZip, ReadZipEntriesStreaming, ReadZipWithSize, SyncArchive, SyncEntry, }; diff --git a/rc-zip-sync/src/read_zip.rs b/rc-zip-sync/src/read_zip.rs index 845fe9b..16ade0b 100644 --- a/rc-zip-sync/src/read_zip.rs +++ b/rc-zip-sync/src/read_zip.rs @@ -2,9 +2,7 @@ use rc_zip::chrono::{DateTime, TimeZone, Utc}; use rc_zip::{ error::{Error, FormatError}, fsm::{ArchiveFsm, FsmResult}, - parse::{ - Archive, ExtraField, ExtraFieldSettings, LocalFileHeaderRecord, NtfsAttr, StoredEntry, - }, + parse::{Archive, ExtraField, ExtraFieldSettings, LocalFileHeader, NtfsAttr, StoredEntry}, }; use tracing::trace; use winnow::{ @@ -123,8 +121,8 @@ where F: HasCursor, { /// Iterate over all files in this zip, read from the central directory. - pub fn entries(&self) -> impl Iterator> { - self.archive.entries().map(move |entry| SyncStoredEntry { + pub fn entries(&self) -> impl Iterator> { + self.archive.entries().map(move |entry| SyncEntry { file: self.file, entry, }) @@ -132,11 +130,11 @@ where /// Attempts to look up an entry by name. This is usually a bad idea, /// as names aren't necessarily normalized in zip archives. - pub fn by_name>(&self, name: N) -> Option> { + pub fn by_name>(&self, name: N) -> Option> { self.archive .entries() .find(|&x| x.name() == name.as_ref()) - .map(|entry| SyncStoredEntry { + .map(|entry| SyncEntry { file: self.file, entry, }) @@ -144,12 +142,12 @@ where } /// A zip entry, read synchronously from a file or other I/O resource. -pub struct SyncStoredEntry<'a, F> { +pub struct SyncEntry<'a, F> { file: &'a F, entry: &'a StoredEntry, } -impl Deref for SyncStoredEntry<'_, F> { +impl Deref for SyncEntry<'_, F> { type Target = StoredEntry; fn deref(&self) -> &Self::Target { @@ -157,7 +155,7 @@ impl Deref for SyncStoredEntry<'_, F> { } } -impl<'a, F> SyncStoredEntry<'a, F> +impl<'a, F> SyncEntry<'a, F> where F: HasCursor, { @@ -259,7 +257,7 @@ where buf.fill(n); let mut input = Partial::new(buf.data()); - match LocalFileHeaderRecord::parser.parse_next(&mut input) { + match LocalFileHeader::parser.parse_next(&mut input) { Ok(header) => { let consumed = input.as_bytes().offset_from(&buf.data()); trace!(?header, %consumed, "Got local file header record!"); diff --git a/rc-zip-sync/src/streaming_entry_reader.rs b/rc-zip-sync/src/streaming_entry_reader.rs index 9705987..62c8f3a 100644 --- a/rc-zip-sync/src/streaming_entry_reader.rs +++ b/rc-zip-sync/src/streaming_entry_reader.rs @@ -1,7 +1,7 @@ use oval::Buffer; use rc_zip::{ fsm::{EntryFsm, FsmResult}, - parse::LocalFileHeaderRecord, + parse::LocalFileHeader, }; use std::{ io::{self, Write}, @@ -10,7 +10,7 @@ use std::{ use tracing::trace; pub struct StreamingEntryReader { - header: LocalFileHeaderRecord, + header: LocalFileHeader, rd: R, state: State, } @@ -34,7 +34,7 @@ impl StreamingEntryReader where R: io::Read, { - pub(crate) fn new(remain: Buffer, header: LocalFileHeaderRecord, rd: R) -> Self { + pub(crate) fn new(remain: Buffer, header: LocalFileHeader, rd: R) -> Self { Self { rd, header, diff --git a/rc-zip/src/corpus/mod.rs b/rc-zip/src/corpus/mod.rs index 2cc8cc9..cab0c15 100644 --- a/rc-zip/src/corpus/mod.rs +++ b/rc-zip/src/corpus/mod.rs @@ -9,7 +9,7 @@ use chrono::{DateTime, FixedOffset, TimeZone, Timelike, Utc}; use crate::{ encoding::Encoding, error::Error, - parse::{Archive, EntryContents, StoredEntry}, + parse::{Archive, Entry, EntryKind}, }; pub struct Case { @@ -246,14 +246,12 @@ pub fn check_case(test: &Case, archive: Result<&Archive, &Error>) { // then each implementation should check individual files } -pub fn check_file_against(file: &CaseFile, entry: &StoredEntry, actual_bytes: &[u8]) { +pub fn check_file_against(file: &CaseFile, entry: &Entry, actual_bytes: &[u8]) { if let Some(expected) = file.modified { assert_eq!( - expected, - entry.modified(), + expected, entry.modified, "entry {} should have modified = {:?}", - entry.name(), - expected + entry.name, expected ) } @@ -262,10 +260,10 @@ pub fn check_file_against(file: &CaseFile, entry: &StoredEntry, actual_bytes: &[ } // I have honestly yet to see a zip file _entry_ with a comment. - assert!(entry.comment().is_none()); + assert!(entry.comment.is_empty()); - match entry.contents() { - EntryContents::File => { + match entry.kind() { + EntryKind::File => { match &file.content { FileContent::Unchecked => { // ah well @@ -283,7 +281,7 @@ pub fn check_file_against(file: &CaseFile, entry: &StoredEntry, actual_bytes: &[ } } } - EntryContents::Symlink | EntryContents::Directory => { + EntryKind::Symlink | EntryKind::Directory => { assert!(matches!(file.content, FileContent::Unchecked)); } } diff --git a/rc-zip/src/fsm/archive.rs b/rc-zip/src/fsm/archive.rs index 6641d59..080994c 100644 --- a/rc-zip/src/fsm/archive.rs +++ b/rc-zip/src/fsm/archive.rs @@ -3,8 +3,8 @@ use crate::{ encoding::Encoding, error::{Error, FormatError}, parse::{ - Archive, DirectoryHeader, EndOfCentralDirectory, EndOfCentralDirectory64Locator, - EndOfCentralDirectory64Record, EndOfCentralDirectoryRecord, Located, StoredEntry, + Archive, CentralDirectoryFileHeader, EndOfCentralDirectory, EndOfCentralDirectory64Locator, + EndOfCentralDirectory64Record, EndOfCentralDirectoryRecord, Entry, Located, }, }; @@ -66,7 +66,7 @@ enum State { /// Reading all headers from the central directory ReadCentralDirectory { eocd: EndOfCentralDirectory, - directory_headers: Vec, + directory_headers: Vec, }, #[default] @@ -256,7 +256,7 @@ impl ArchiveFsm { "initial offset & len" ); 'read_headers: while !input.is_empty() { - match DirectoryHeader::parser.parse_next(&mut input) { + match CentralDirectoryFileHeader::parser.parse_next(&mut input) { Ok(dh) => { trace!( input_empty_now = input.is_empty(), @@ -336,9 +336,9 @@ impl ArchiveFsm { let is_zip64 = eocd.dir64.is_some(); let global_offset = eocd.global_offset as u64; - let entries: Result, Error> = directory_headers + let entries: Result, Error> = directory_headers .iter() - .map(|x| x.as_stored_entry(is_zip64, encoding, global_offset)) + .map(|x| x.as_entry(is_zip64, encoding, global_offset)) .collect(); let entries = entries?; diff --git a/rc-zip/src/fsm/entry/mod.rs b/rc-zip/src/fsm/entry/mod.rs index 39c2769..90f1ca3 100644 --- a/rc-zip/src/fsm/entry/mod.rs +++ b/rc-zip/src/fsm/entry/mod.rs @@ -27,7 +27,7 @@ mod zstd_dec; use crate::{ error::{Error, FormatError, UnsupportedError}, - parse::{DataDescriptorRecord, LocalFileHeaderRecord, Method, StoredEntryInner}, + parse::{DataDescriptorRecord, Entry, LocalFileHeader, Method}, }; use super::FsmResult; @@ -43,7 +43,7 @@ enum State { ReadData { /// The local file header for this entry - header: LocalFileHeaderRecord, + header: LocalFileHeader, /// Entry compressed size compressed_size: u64, @@ -63,7 +63,7 @@ enum State { ReadDataDescriptor { /// The local file header for this entry - header: LocalFileHeaderRecord, + header: LocalFileHeader, /// Size we've decompressed + crc32 hash we've computed metrics: EntryReadMetrics, @@ -71,7 +71,7 @@ enum State { Validate { /// The local file header for this entry - header: LocalFileHeaderRecord, + header: LocalFileHeader, /// Size we've decompressed + crc32 hash we've computed metrics: EntryReadMetrics, @@ -87,14 +87,14 @@ enum State { /// A state machine that can parse a zip entry pub struct EntryFsm { state: State, - entry: Option, + entry: Option, buffer: Buffer, eof: bool, } impl EntryFsm { /// Create a new state machine for decompressing a zip entry - pub fn new(entry: Option) -> Self { + pub fn new(entry: Option) -> Self { Self { state: State::ReadLocalHeader, entry, @@ -119,6 +119,11 @@ impl EntryFsm { } } + /// Like `process`, but only processes the header: + pub fn process_header_only(&mut self) -> Option<&LocalFileHeader> { + todo!() + } + /// Process the input and write the output to the given buffer /// /// This function will return `FsmResult::Continue` if it needs more input @@ -148,7 +153,7 @@ impl EntryFsm { match &mut self.state { S::ReadLocalHeader => { let mut input = Partial::new(self.buffer.data()); - match LocalFileHeaderRecord::parser.parse_next(&mut input) { + match LocalFileHeader::parser.parse_next(&mut input) { Ok(header) => { let consumed = input.as_bytes().offset_from(&self.buffer.data()); tracing::trace!(local_file_header = ?header, consumed, "parsed local file header"); diff --git a/rc-zip/src/parse/archive.rs b/rc-zip/src/parse/archive.rs index 4b464eb..a6ee74e 100644 --- a/rc-zip/src/parse/archive.rs +++ b/rc-zip/src/parse/archive.rs @@ -1,11 +1,14 @@ -use chrono::{DateTime, Utc}; +use chrono::{offset::Utc, DateTime, TimeZone}; use num_enum::{FromPrimitive, IntoPrimitive}; +use winnow::{binary::le_u16, PResult, Partial}; use crate::{ encoding::Encoding, - parse::{ExtraField, Mode, Version}, + parse::{Mode, Version}, }; +use super::{zero_datetime, ExtraField, NtfsAttr}; + /// An Archive contains general information about a zip files, along with a list /// of [entries][StoredEntry]. /// @@ -17,7 +20,7 @@ use crate::{ pub struct Archive { pub(crate) size: u64, pub(crate) encoding: Encoding, - pub(crate) entries: Vec, + pub(crate) entries: Vec, pub(crate) comment: Option, } @@ -28,14 +31,14 @@ impl Archive { } /// Iterate over all files in this zip, read from the central directory. - pub fn entries(&self) -> impl Iterator { + pub fn entries(&self) -> impl Iterator { self.entries.iter() } /// Attempts to look up an entry by name. This is usually a bad idea, /// as names aren't necessarily normalized in zip archives. - pub fn by_name>(&self, name: N) -> Option<&StoredEntry> { - self.entries.iter().find(|&x| x.name() == name.as_ref()) + pub fn by_name>(&self, name: N) -> Option<&Entry> { + self.entries.iter().find(|&x| x.name == name.as_ref()) } /// Returns the detected character encoding for text fields @@ -59,42 +62,40 @@ impl Archive { #[derive(Clone)] pub struct Entry { /// Name of the file - /// Must be a relative path, not start with a drive letter (e.g. C:), - /// and must use forward slashes instead of back slashes + /// + /// This should be a relative path, separated by `/`. However, there are zip + /// files in the wild with all sorts of evil variants, so, be conservative + /// in what you accept. + /// + /// See also [Self::sanitized_name], which returns a sanitized version of + /// the name, working around zip slip vulnerabilities. pub name: String, - /// Compression method - /// - /// See [Method][] for more details. + /// Compression method: Store, Deflate, Bzip2, etc. pub method: Method, /// Comment is any arbitrary user-defined string shorter than 64KiB - pub comment: Option, + pub comment: String, - /// Modified timestamp - pub modified: chrono::DateTime, - - /// Created timestamp - pub created: Option>, + /// This entry's "last modified" timestamp - with caveats + /// + /// Due to the history of the ZIP file format, this may be inaccurate. It may be offset + /// by a few hours, if there is no extended timestamp information. It may have a resolution + /// as low as two seconds, if only MSDOS timestamps are present. It may default to the Unix + /// epoch, if something went really wrong. + /// + /// If you're reading this after the year 2038, or after the year 2108, godspeed. + pub modified: DateTime, - /// Accessed timestamp - pub accessed: Option>, -} + /// This entry's "created" timestamp, if available. + /// + /// See [Self::modified] for caveats. + pub created: Option>, -/// An entry as stored into an Archive. Contains additional metadata and offset information. -/// -/// Whereas [Entry][] is archive-independent, [StoredEntry][] contains information that is tied to -/// a specific archive. -/// -/// When reading archives, one deals with a list of [StoredEntry][], whereas when writing one, one -/// typically only specifies an [Entry][] and provides the entry's contents: fields like the CRC32 -/// hash, uncompressed size, and compressed size are derived automatically from the input. -#[derive(Clone)] -pub struct StoredEntry { - /// Archive-independent information + /// This entry's "last accessed" timestamp, if available. /// - /// This contains the entry's name, timestamps, comment, compression method. - pub entry: Entry, + /// See [Self::accessed] for caveats. + pub accessed: Option>, /// Offset of the local file header in the zip file /// @@ -111,12 +112,6 @@ pub struct StoredEntry { /// ``` pub header_offset: u64, - /// External attributes (zip) - pub external_attrs: u32, - - /// Version of zip supported by the tool that crated this archive. - pub creator_version: Version, - /// Version of zip needed to extract this archive. pub reader_version: Version, @@ -139,24 +134,6 @@ pub struct StoredEntry { /// Only present if a Unix extra field or New Unix extra field was found. pub gid: Option, - /// File mode - pub mode: Mode, - - /// Any extra fields recognized while parsing the file. - /// - /// Most of these should be normalized and accessible as other fields, - /// but they are also made available here raw. - pub extra_fields: Vec, - - /// These fields are cheap to clone and needed for entry readers, - /// hence them being in a separate struct - pub inner: StoredEntryInner, -} - -/// Fields required to read an entry properly, typically cloned into owned entry -/// readers. -#[derive(Clone, Copy, Debug)] -pub struct StoredEntryInner { /// CRC-32 hash as found in the central directory. /// /// Note that this may be zero, and the actual CRC32 might be in the local header, or (more @@ -171,22 +148,11 @@ pub struct StoredEntryInner { /// This will be zero for directories. pub uncompressed_size: u64, - /// True if this entry was read from a zip64 archive - pub is_zip64: bool, + /// File mode. + pub mode: Mode, } -impl StoredEntry { - /// Returns the entry's name. See also - /// [sanitized_name()](StoredEntry::sanitized_name), which returns a - /// sanitized version of the name. - /// - /// This should be a relative path, separated by `/`. However, there are zip - /// files in the wild with all sorts of evil variants, so, be conservative - /// in what you accept. - pub fn name(&self) -> &str { - self.entry.name.as_ref() - } - +impl Entry { /// Returns a sanitized version of the entry's name, if it /// seems safe. In particular, if this method feels like the /// entry name is trying to do a zip slip (cf. @@ -195,7 +161,7 @@ impl StoredEntry { /// /// Other than that, it will strip any leading slashes on non-Windows OSes. pub fn sanitized_name(&self) -> Option<&str> { - let name = self.name(); + let name = self.name.as_str(); // refuse entries with traversed/absolute path to mitigate zip slip if name.contains("..") { @@ -223,52 +189,56 @@ impl StoredEntry { } } - /// The entry's comment, if any. - /// - /// When reading a zip file, an empty comment results in None. - pub fn comment(&self) -> Option<&str> { - self.entry.comment.as_ref().map(|x| x.as_ref()) - } - - /// The compression method used for this entry - #[inline(always)] - pub fn method(&self) -> Method { - self.entry.method - } - - /// This entry's "last modified" timestamp - with caveats - /// - /// Due to the history of the ZIP file format, this may be inaccurate. It may be offset - /// by a few hours, if there is no extended timestamp information. It may have a resolution - /// as low as two seconds, if only MSDOS timestamps are present. It may default to the Unix - /// epoch, if something went really wrong. - /// - /// If you're reading this after the year 2038, or after the year 2108, godspeed. - #[inline(always)] - pub fn modified(&self) -> DateTime { - self.entry.modified - } - - /// This entry's "created" timestamp, if available. - /// - /// See [StoredEntry::modified()] for caveats. - #[inline(always)] - pub fn created(&self) -> Option<&DateTime> { - self.entry.created.as_ref() - } - - /// This entry's "last accessed" timestamp, if available. - /// - /// See [StoredEntry::modified()] for caveats. - #[inline(always)] - pub fn accessed(&self) -> Option<&DateTime> { - self.entry.accessed.as_ref() + /// Apply the extra field to the entry, updating its metadata. + pub(crate) fn set_extra_field(&mut self, ef: &ExtraField) { + match &ef { + ExtraField::Zip64(z64) => { + self.uncompressed_size = z64.uncompressed_size; + self.compressed_size = z64.compressed_size; + self.header_offset = z64.header_offset; + } + ExtraField::Timestamp(ts) => { + self.modified = Utc + .timestamp_opt(ts.mtime as i64, 0) + .single() + .unwrap_or_else(zero_datetime); + } + ExtraField::Ntfs(nf) => { + for attr in &nf.attrs { + // note: other attributes are unsupported + if let NtfsAttr::Attr1(attr) = attr { + self.modified = attr.mtime.to_datetime().unwrap_or_else(zero_datetime); + self.created = attr.ctime.to_datetime(); + self.accessed = attr.atime.to_datetime(); + } + } + } + ExtraField::Unix(uf) => { + self.modified = Utc + .timestamp_opt(uf.mtime as i64, 0) + .single() + .unwrap_or_else(zero_datetime); + + if self.uid.is_none() { + self.uid = Some(uf.uid as u32); + } + + if self.gid.is_none() { + self.gid = Some(uf.gid as u32); + } + } + ExtraField::NewUnix(uf) => { + self.uid = Some(uf.uid as u32); + self.gid = Some(uf.uid as u32); + } + _ => {} + }; } } -/// The contents of an entry: a directory, a file, or a symbolic link. +/// The entry's file type: a directory, a file, or a symbolic link. #[derive(Debug)] -pub enum EntryContents { +pub enum EntryKind { /// The entry is a directory Directory, @@ -279,15 +249,15 @@ pub enum EntryContents { Symlink, } -impl StoredEntry { - /// Determine [EntryContents] of this entry based on its mode. - pub fn contents(&self) -> EntryContents { +impl Entry { + /// Determine the kind of this entry based on its mode. + pub fn kind(&self) -> EntryKind { if self.mode.has(Mode::SYMLINK) { - EntryContents::Symlink + EntryKind::Symlink } else if self.mode.has(Mode::DIR) { - EntryContents::Directory + EntryKind::Directory } else { - EntryContents::File + EntryKind::File } } } @@ -342,3 +312,10 @@ pub enum Method { #[num_enum(catch_all)] Unrecognized(u16), } + +impl Method { + /// Parse a method from a byte slice + pub fn parser(i: &mut Partial<&[u8]>) -> PResult { + le_u16(i).map(From::from) + } +} diff --git a/rc-zip/src/parse/directory_header.rs b/rc-zip/src/parse/central_directory_file_header.rs similarity index 51% rename from rc-zip/src/parse/directory_header.rs rename to rc-zip/src/parse/central_directory_file_header.rs index db38717..4a03735 100644 --- a/rc-zip/src/parse/directory_header.rs +++ b/rc-zip/src/parse/central_directory_file_header.rs @@ -13,13 +13,14 @@ use crate::{ error::{Error, FormatError}, parse::{ zero_datetime, Entry, ExtraField, ExtraFieldSettings, HostSystem, Mode, MsdosMode, - MsdosTimestamp, NtfsAttr, StoredEntry, StoredEntryInner, UnixMode, Version, ZipBytes, - ZipString, + MsdosTimestamp, NtfsAttr, UnixMode, Version, ZipBytes, ZipString, }, }; +use super::{EntryCdFields, Method}; + /// 4.3.12 Central directory structure: File header -pub struct DirectoryHeader { +pub struct CentralDirectoryFileHeader { /// version made by pub creator_version: Version, @@ -30,7 +31,7 @@ pub struct DirectoryHeader { pub flags: u16, /// compression method - pub method: u16, + pub method: Method, /// last mod file datetime pub modified: MsdosTimestamp, @@ -57,16 +58,16 @@ pub struct DirectoryHeader { pub header_offset: u32, /// name - pub name: ZipString, // FIXME: should this be Cow? + pub name: ZipString, /// extra - pub extra: ZipBytes, // FIXME: should this be Cow<[u8]>? + pub extra: ZipBytes, /// comment pub comment: ZipString, } -impl DirectoryHeader { +impl CentralDirectoryFileHeader { const SIGNATURE: &'static str = "PK\x01\x02"; /// Parser for the central directory file header @@ -75,7 +76,7 @@ impl DirectoryHeader { let creator_version = Version::parser.parse_next(i)?; let reader_version = Version::parser.parse_next(i)?; let flags = le_u16.parse_next(i)?; - let method = le_u16.parse_next(i)?; + let method = Method::parser.parse_next(i)?; let modified = MsdosTimestamp::parser.parse_next(i)?; let crc32 = le_u32.parse_next(i)?; let compressed_size = le_u32.parse_next(i)?; @@ -112,7 +113,7 @@ impl DirectoryHeader { } } -impl DirectoryHeader { +impl CentralDirectoryFileHeader { /// Returns true if the name or comment is not valid UTF-8 pub fn is_non_utf8(&self) -> bool { let (valid1, require1) = detect_utf8(&self.name.0[..]); @@ -136,83 +137,48 @@ impl DirectoryHeader { /// Converts the directory header into a stored entry: this involves /// parsing the extra fields and converting the timestamps. - pub fn as_stored_entry( - &self, - is_zip64: bool, - encoding: Encoding, - global_offset: u64, - ) -> Result { - let mut comment: Option = None; - if let Some(comment_field) = self.comment.clone().into_option() { - comment = Some(encoding.decode(&comment_field.0)?); - } - - let name = encoding.decode(&self.name.0)?; - - let mut compressed_size = self.compressed_size as u64; - let mut uncompressed_size = self.uncompressed_size as u64; - let mut header_offset = self.header_offset as u64 + global_offset; - - let mut modified: Option> = None; - let mut created: Option> = None; - let mut accessed: Option> = None; - - let mut uid: Option = None; - let mut gid: Option = None; + pub fn as_entry(&self, encoding: Encoding, global_offset: u64) -> Result { + let mut entry = Entry { + name: encoding.decode(&self.name.0)?, + method: self.method, + comment: encoding.decode(&self.comment.0)?, + modified: self.modified.to_datetime().unwrap_or_else(zero_datetime), + created: None, + accessed: None, + header_offset: self.header_offset as u64 + global_offset, + reader_version: self.reader_version, + flags: self.flags, + uid: None, + gid: None, + crc32: self.crc32, + compressed_size: self.compressed_size as _, + uncompressed_size: self.uncompressed_size as _, + mode: Mode(0), + }; - let mut extra_fields: Vec = Vec::new(); + entry.mode = match self.creator_version.host_system { + HostSystem::Unix | HostSystem::Osx => UnixMode(self.external_attrs >> 16).into(), + HostSystem::WindowsNtfs | HostSystem::Vfat | HostSystem::MsDos => { + MsdosMode(self.external_attrs).into() + } + _ => Mode(0), + }; + if entry.name.ends_with('/') { + // believe it or not, this is straight from the APPNOTE + entry.mode |= Mode::DIR + }; let settings = ExtraFieldSettings { - needs_compressed_size: self.compressed_size == !0u32, - needs_uncompressed_size: self.uncompressed_size == !0u32, - needs_header_offset: self.header_offset == !0u32, + uncompressed_size_u32: self.uncompressed_size, + compressed_size_u32: self.compressed_size, + header_offset_u32: self.header_offset, }; let mut slice = Partial::new(&self.extra.0[..]); while !slice.is_empty() { match ExtraField::mk_parser(settings).parse_next(&mut slice) { Ok(ef) => { - match &ef { - ExtraField::Zip64(z64) => { - if let Some(n) = z64.uncompressed_size { - uncompressed_size = n; - } - if let Some(n) = z64.compressed_size { - compressed_size = n; - } - if let Some(n) = z64.header_offset { - header_offset = n; - } - } - ExtraField::Timestamp(ts) => { - modified = Utc.timestamp_opt(ts.mtime as i64, 0).single(); - } - ExtraField::Ntfs(nf) => { - for attr in &nf.attrs { - // note: other attributes are unsupported - if let NtfsAttr::Attr1(attr) = attr { - modified = attr.mtime.to_datetime(); - created = attr.ctime.to_datetime(); - accessed = attr.atime.to_datetime(); - } - } - } - ExtraField::Unix(uf) => { - modified = Utc.timestamp_opt(uf.mtime as i64, 0).single(); - if uid.is_none() { - uid = Some(uf.uid as u32); - } - if gid.is_none() { - gid = Some(uf.gid as u32); - } - } - ExtraField::NewUnix(uf) => { - uid = Some(uf.uid as u32); - gid = Some(uf.uid as u32); - } - _ => {} - }; - extra_fields.push(ef); + entry.set_extra_field(&ef); } Err(e) => { trace!("extra field error: {:#?}", e); @@ -221,52 +187,6 @@ impl DirectoryHeader { } } - let modified = match modified { - Some(m) => Some(m), - None => self.modified.to_datetime(), - }; - - let mut mode: Mode = match self.creator_version.host_system() { - HostSystem::Unix | HostSystem::Osx => UnixMode(self.external_attrs >> 16).into(), - HostSystem::WindowsNtfs | HostSystem::Vfat | HostSystem::MsDos => { - MsdosMode(self.external_attrs).into() - } - _ => Mode(0), - }; - if name.ends_with('/') { - // believe it or not, this is straight from the APPNOTE - mode |= Mode::DIR - }; - - Ok(StoredEntry { - entry: Entry { - name, - method: self.method.into(), - comment, - modified: modified.unwrap_or_else(zero_datetime), - created, - accessed, - }, - - creator_version: self.creator_version, - reader_version: self.reader_version, - flags: self.flags, - - inner: StoredEntryInner { - crc32: self.crc32, - compressed_size, - uncompressed_size, - is_zip64, - }, - header_offset, - - uid, - gid, - mode, - - extra_fields, - - external_attrs: self.external_attrs, - }) + Ok(entry) } } diff --git a/rc-zip/src/parse/extra_field.rs b/rc-zip/src/parse/extra_field.rs index fd7434b..6513b4e 100644 --- a/rc-zip/src/parse/extra_field.rs +++ b/rc-zip/src/parse/extra_field.rs @@ -1,7 +1,7 @@ use tracing::trace; use winnow::{ binary::{le_u16, le_u32, le_u64, le_u8, length_take}, - combinator::{cond, opt, preceded, repeat_till}, + combinator::{opt, preceded, repeat_till}, error::{ErrMode, ErrorKind, ParserError, StrContext}, seq, token::{tag, take}, @@ -36,14 +36,20 @@ impl<'a> ExtraFieldRecord<'a> { /// Central directory record field is set to 0xFFFF or 0xFFFFFFFF. #[derive(Debug, Clone, Copy)] pub struct ExtraFieldSettings { - /// Whether the "zip64 extra field" uncompressed size field is needed/present - pub needs_uncompressed_size: bool, - - /// Whether the "zip64 extra field" compressed size field is needed/present - pub needs_compressed_size: bool, - - /// Whether the "zip64 extra field" header offset field is needed/present - pub needs_header_offset: bool, + /// The uncompressed size field read from a local or central directory record + /// If this is 0xFFFF_FFFF, then the zip64 extra field uncompressed size + /// field will be present. + pub uncompressed_size_u32: u32, + + /// The compressed size field read from a local or central directory record + /// If this is 0xFFFF_FFFF, then the zip64 extra field compressed size + /// field will be present. + pub compressed_size_u32: u32, + + /// The header offset field read from a central directory record (or zero + /// for local directory records). If this is 0xFFFF_FFFF, then the zip64 + /// extra field header offset field will be present. + pub header_offset_u32: u32, } /// Information stored in the central directory header `extra` field @@ -90,7 +96,7 @@ impl ExtraField { .context(StrContext::Label("timestamp")) .parse_next(payload)?, ExtraNtfsField::TAG => { - opt(ExtraNtfsField::parse.map(EF::Ntfs)).parse_next(payload)? + opt(ExtraNtfsField::parser.map(EF::Ntfs)).parse_next(payload)? } ExtraUnixField::TAG | ExtraUnixField::TAG_INFOZIP => { opt(ExtraUnixField::parser.map(EF::Unix)).parse_next(payload)? @@ -111,13 +117,16 @@ impl ExtraField { #[derive(Clone, Default)] pub struct ExtraZip64Field { /// 64-bit uncompressed size - pub uncompressed_size: Option, + pub uncompressed_size: u64, /// 64-bit compressed size - pub compressed_size: Option, + pub compressed_size: u64, /// 64-bit header offset - pub header_offset: Option, + pub header_offset: u64, + + /// 32-bit disk start number + pub disk_start: u32, } impl ExtraZip64Field { @@ -127,13 +136,29 @@ impl ExtraZip64Field { settings: ExtraFieldSettings, ) -> impl FnMut(&mut Partial<&'_ [u8]>) -> PResult { move |i| { - // N.B: we ignore "disk start number" - seq! {Self { - uncompressed_size: cond(settings.needs_uncompressed_size, le_u64), - compressed_size: cond(settings.needs_compressed_size, le_u64), - header_offset: cond(settings.needs_header_offset, le_u64), - }} - .parse_next(i) + let uncompressed_size = if settings.uncompressed_size_u32 == 0xFFFF_FFFF { + le_u64.parse_next(i)? + } else { + settings.uncompressed_size_u32 as u64 + }; + let compressed_size = if settings.compressed_size_u32 == 0xFFFF_FFFF { + le_u64.parse_next(i)? + } else { + settings.compressed_size_u32 as u64 + }; + let header_offset = if settings.header_offset_u32 == 0xFFFF_FFFF { + le_u64.parse_next(i)? + } else { + settings.header_offset_u32 as u64 + }; + let disk_start = le_u32.parse_next(i)?; + + Ok(Self { + uncompressed_size, + compressed_size, + header_offset, + disk_start, + }) } } } @@ -254,7 +279,7 @@ pub struct ExtraNtfsField { impl ExtraNtfsField { const TAG: u16 = 0x000a; - fn parse(i: &mut Partial<&'_ [u8]>) -> PResult { + fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { let _ = take(4_usize).parse_next(i)?; // reserved (unused) seq! {Self { // from the winnow docs: @@ -262,7 +287,7 @@ impl ExtraNtfsField { // data or the end of the stream, causing them to always report // Incomplete. // using repeat_till with eof combinator to work around this: - attrs: repeat_till(0.., NtfsAttr::parse, winnow::combinator::eof).map(|x| x.0), + attrs: repeat_till(0.., NtfsAttr::parser, winnow::combinator::eof).map(|x| x.0), }} .parse_next(i) } @@ -282,7 +307,7 @@ pub enum NtfsAttr { } impl NtfsAttr { - fn parse(i: &mut Partial<&'_ [u8]>) -> PResult { + fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { let tag = le_u16.parse_next(i)?; trace!("parsing NTFS attribute, tag {:04x}", tag); let payload = length_take(le_u16).parse_next(i)?; diff --git a/rc-zip/src/parse/local.rs b/rc-zip/src/parse/local_headers.rs similarity index 98% rename from rc-zip/src/parse/local.rs rename to rc-zip/src/parse/local_headers.rs index fc73ef6..6b835b0 100644 --- a/rc-zip/src/parse/local.rs +++ b/rc-zip/src/parse/local_headers.rs @@ -14,7 +14,7 @@ use winnow::{ #[derive(Debug)] /// 4.3.7 Local file header -pub struct LocalFileHeaderRecord { +pub struct LocalFileHeader { /// version needed to extract pub reader_version: Version, @@ -56,7 +56,7 @@ pub enum MethodSpecific { Lzma(LzmaProperties), } -impl LocalFileHeaderRecord { +impl LocalFileHeader { /// The signature for a local file header pub const SIGNATURE: &'static str = "PK\x03\x04"; diff --git a/rc-zip/src/parse/mod.rs b/rc-zip/src/parse/mod.rs index 962c24e..fc41699 100644 --- a/rc-zip/src/parse/mod.rs +++ b/rc-zip/src/parse/mod.rs @@ -22,14 +22,14 @@ pub use version::*; mod date_time; pub use date_time::*; -mod directory_header; -pub use directory_header::*; +mod central_directory_file_header; +pub use central_directory_file_header::*; mod eocd; pub use eocd::*; -mod local; -pub use local::*; +mod local_headers; +pub use local_headers::*; mod raw; pub use raw::*; diff --git a/rc-zip/src/parse/raw.rs b/rc-zip/src/parse/raw.rs index fb978ab..9a86943 100644 --- a/rc-zip/src/parse/raw.rs +++ b/rc-zip/src/parse/raw.rs @@ -32,14 +32,6 @@ impl ZipString { let count = count.to_usize(); move |i| (take(count).map(|slice: &[u8]| Self(slice.into()))).parse_next(i) } - - pub(crate) fn into_option(self) -> Option { - if !self.0.is_empty() { - Some(self) - } else { - None - } - } } /// A raw u8 slice, with no specific structure. diff --git a/rc-zip/src/parse/version.rs b/rc-zip/src/parse/version.rs index 1b9ac8f..2348bdf 100644 --- a/rc-zip/src/parse/version.rs +++ b/rc-zip/src/parse/version.rs @@ -1,5 +1,6 @@ +use num_enum::{FromPrimitive, IntoPrimitive}; use std::fmt; -use winnow::{binary::le_u16, PResult, Parser, Partial}; +use winnow::{binary::le_u8, seq, PResult, Parser, Partial}; /// A zip version (either created by, or required when reading an archive). /// @@ -8,7 +9,14 @@ use winnow::{binary::le_u16, PResult, Parser, Partial}; /// /// For more information, see the [.ZIP Application Note](https://support.pkware.com/display/PKZIP/APPNOTE), section 4.4.2. #[derive(Clone, Copy, PartialEq, Eq, Hash)] -pub struct Version(pub u16); +pub struct Version { + /// The host system on which + pub host_system: HostSystem, + + /// Integer version, e.g. 45 for Zip version 4.5 + /// See APPNOTE, section 4.4.2.1 + pub version: u8, +} impl fmt::Debug for Version { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { @@ -25,109 +33,84 @@ impl fmt::Debug for Version { impl Version { /// Parse a version from a byte slice pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { - le_u16.map(Self).parse_next(i) - } - - /// Identifies the host system on which the zip attributes are compatible. - pub fn host_system(&self) -> HostSystem { - match self.host() { - 0 => HostSystem::MsDos, - 1 => HostSystem::Amiga, - 2 => HostSystem::OpenVms, - 3 => HostSystem::Unix, - 4 => HostSystem::VmCms, - 5 => HostSystem::AtariSt, - 6 => HostSystem::Os2Hpfs, - 7 => HostSystem::Macintosh, - 8 => HostSystem::ZSystem, - 9 => HostSystem::CpM, - 10 => HostSystem::WindowsNtfs, - 11 => HostSystem::Mvs, - 12 => HostSystem::Vse, - 13 => HostSystem::AcornRisc, - 14 => HostSystem::Vfat, - 15 => HostSystem::AlternateMvs, - 16 => HostSystem::BeOs, - 17 => HostSystem::Tandem, - 18 => HostSystem::Os400, - 19 => HostSystem::Osx, - n => HostSystem::Unknown(n), - } - } - - /// Integer host system - pub fn host(&self) -> u8 { - (self.0 >> 8) as u8 - } - - /// Integer version, e.g. 45 for Zip version 4.5 - pub fn version(&self) -> u8 { - (self.0 & 0xff) as u8 - } - - /// ZIP specification major version - /// - /// See APPNOTE, section 4.4.2.1 - pub fn major(&self) -> u32 { - self.version() as u32 / 10 - } - - /// ZIP specification minor version - /// - /// See APPNOTE, section 4.4.2.1 - pub fn minor(&self) -> u32 { - self.version() as u32 % 10 + seq! {Self { + host_system: le_u8.map(HostSystem::from_u8), + version: le_u8, + }} + .parse_next(i) } } /// System on which an archive was created, as encoded into a version u16. /// /// See APPNOTE, section 4.4.2.2 -#[derive(Debug)] +#[derive(Debug, Clone, Copy, IntoPrimitive, FromPrimitive)] +#[repr(u8)] pub enum HostSystem { /// MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems) - MsDos, + MsDos = 0, + /// Amiga - Amiga, + Amiga = 1, + /// OpenVMS - OpenVms, + OpenVms = 2, + /// UNIX - Unix, + Unix = 3, + /// VM/CMS - VmCms, + VmCms = 4, + /// Atari ST - AtariSt, + AtariSt = 5, + /// OS/2 H.P.F.S - Os2Hpfs, + Os2Hpfs = 6, + /// Macintosh (see `Osx`) - Macintosh, + Macintosh = 7, + /// Z-System - ZSystem, + ZSystem = 8, + /// CP/M - CpM, + CpM = 9, + /// Windows NTFS - WindowsNtfs, + WindowsNtfs = 10, + /// MVS (OS/390 - Z/OS) - Mvs, + Mvs = 11, + /// VSE - Vse, + Vse = 12, + /// Acorn Risc - AcornRisc, + AcornRisc = 13, + /// VFAT - Vfat, + Vfat = 14, + /// alternate MVS - AlternateMvs, + AlternateMvs = 15, + /// BeOS - BeOs, + BeOs = 16, + /// Tandem - Tandem, + Tandem = 17, + /// OS/400 - Os400, + Os400 = 18, + /// OS X (Darwin) - Osx, + Osx = 19, + /// Unknown host system /// /// Values 20 through 255 are currently unused, as of - /// APPNOTE.TXT 6.3.6 (April 26, 2019) + /// APPNOTE.TXT 6.3.10 + #[num_enum(catch_all)] Unknown(u8), } From 33cda194d3bbbb5257e9e80c1b837312da000b57 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 19:27:43 +0100 Subject: [PATCH 10/23] More unification --- rc-zip-sync/src/entry_reader.rs | 6 +++--- rc-zip-sync/src/read_zip.rs | 15 +++++++------ rc-zip/src/fsm/archive.rs | 3 +-- rc-zip/src/fsm/entry/mod.rs | 21 ++++++++++--------- .../parse/central_directory_file_header.rs | 5 ++--- rc-zip/src/parse/version.rs | 12 +++-------- 6 files changed, 29 insertions(+), 33 deletions(-) diff --git a/rc-zip-sync/src/entry_reader.rs b/rc-zip-sync/src/entry_reader.rs index 0032d79..5a276f6 100644 --- a/rc-zip-sync/src/entry_reader.rs +++ b/rc-zip-sync/src/entry_reader.rs @@ -1,6 +1,6 @@ use rc_zip::{ fsm::{EntryFsm, FsmResult}, - parse::StoredEntry, + parse::Entry, }; use std::io; use tracing::trace; @@ -17,10 +17,10 @@ impl EntryReader where R: io::Read, { - pub(crate) fn new(entry: &StoredEntry, rd: R) -> Self { + pub(crate) fn new(entry: &Entry, rd: R) -> Self { Self { rd, - fsm: Some(EntryFsm::new(Some(entry.inner))), + fsm: Some(EntryFsm::new(Some(entry.clone()))), } } } diff --git a/rc-zip-sync/src/read_zip.rs b/rc-zip-sync/src/read_zip.rs index 16ade0b..951884c 100644 --- a/rc-zip-sync/src/read_zip.rs +++ b/rc-zip-sync/src/read_zip.rs @@ -1,8 +1,11 @@ -use rc_zip::chrono::{DateTime, TimeZone, Utc}; +use rc_zip::{ + chrono::{DateTime, TimeZone, Utc}, + parse::Entry, +}; use rc_zip::{ error::{Error, FormatError}, fsm::{ArchiveFsm, FsmResult}, - parse::{Archive, ExtraField, ExtraFieldSettings, LocalFileHeader, NtfsAttr, StoredEntry}, + parse::{Archive, ExtraField, ExtraFieldSettings, LocalFileHeader, NtfsAttr}, }; use tracing::trace; use winnow::{ @@ -96,7 +99,7 @@ impl ReadZip for Vec { /// /// This only contains metadata for the archive and its entries. Separate /// readers can be created for arbitraries entries on-demand using -/// [SyncStoredEntry::reader]. +/// [SyncEntry::reader]. pub struct SyncArchive<'a, F> where F: HasCursor, @@ -133,7 +136,7 @@ where pub fn by_name>(&self, name: N) -> Option> { self.archive .entries() - .find(|&x| x.name() == name.as_ref()) + .find(|&x| x.name == name.as_ref()) .map(|entry| SyncEntry { file: self.file, entry, @@ -144,11 +147,11 @@ where /// A zip entry, read synchronously from a file or other I/O resource. pub struct SyncEntry<'a, F> { file: &'a F, - entry: &'a StoredEntry, + entry: &'a Entry, } impl Deref for SyncEntry<'_, F> { - type Target = StoredEntry; + type Target = Entry; fn deref(&self) -> &Self::Target { self.entry diff --git a/rc-zip/src/fsm/archive.rs b/rc-zip/src/fsm/archive.rs index 080994c..92a833b 100644 --- a/rc-zip/src/fsm/archive.rs +++ b/rc-zip/src/fsm/archive.rs @@ -334,11 +334,10 @@ impl ArchiveFsm { } }; - let is_zip64 = eocd.dir64.is_some(); let global_offset = eocd.global_offset as u64; let entries: Result, Error> = directory_headers .iter() - .map(|x| x.as_entry(is_zip64, encoding, global_offset)) + .map(|x| x.as_entry(encoding, global_offset)) .collect(); let entries = entries?; diff --git a/rc-zip/src/fsm/entry/mod.rs b/rc-zip/src/fsm/entry/mod.rs index 90f1ca3..5d36659 100644 --- a/rc-zip/src/fsm/entry/mod.rs +++ b/rc-zip/src/fsm/entry/mod.rs @@ -160,11 +160,12 @@ impl EntryFsm { self.buffer.consume(consumed); let decompressor = AnyDecompressor::new( header.method, - self.entry.map(|entry| entry.uncompressed_size), + self.entry.as_ref().map(|entry| entry.uncompressed_size), )?; let compressed_size = match &self.entry { Some(entry) => entry.compressed_size, None => { + // FIXME: the zip64 extra field is here for that if header.compressed_size == u32::MAX { return Err(Error::Decompression { method: header.method, @@ -258,14 +259,11 @@ impl EntryFsm { Ok(FsmResult::Continue((self, outcome))) } - S::ReadDataDescriptor { .. } => { + S::ReadDataDescriptor { header, .. } => { let mut input = Partial::new(self.buffer.data()); - // if we don't have entry info, we're dangerously assuming the - // file isn't zip64. oh well. - // FIXME: we can just read until the next local file header and - // determine whether the file is zip64 or not from there? - let is_zip64 = self.entry.as_ref().map(|e| e.is_zip64).unwrap_or(false); + let is_zip64 = + header.compressed_size == u32::MAX || header.uncompressed_size == u32::MAX; match DataDescriptorRecord::mk_parser(is_zip64).parse_next(&mut input) { Ok(descriptor) => { @@ -288,7 +286,7 @@ impl EntryFsm { metrics, descriptor, } => { - let entry_crc32 = self.entry.map(|e| e.crc32).unwrap_or_default(); + let entry_crc32 = self.entry.as_ref().map(|e| e.crc32).unwrap_or_default(); let expected_crc32 = if entry_crc32 != 0 { entry_crc32 } else if let Some(descriptor) = descriptor.as_ref() { @@ -297,8 +295,11 @@ impl EntryFsm { header.crc32 }; - let entry_uncompressed_size = - self.entry.map(|e| e.uncompressed_size).unwrap_or_default(); + let entry_uncompressed_size = self + .entry + .as_ref() + .map(|e| e.uncompressed_size) + .unwrap_or_default(); let expected_size = if entry_uncompressed_size != 0 { entry_uncompressed_size } else if let Some(descriptor) = descriptor.as_ref() { diff --git a/rc-zip/src/parse/central_directory_file_header.rs b/rc-zip/src/parse/central_directory_file_header.rs index 4a03735..07c4f1b 100644 --- a/rc-zip/src/parse/central_directory_file_header.rs +++ b/rc-zip/src/parse/central_directory_file_header.rs @@ -1,4 +1,3 @@ -use chrono::{offset::TimeZone, DateTime, Utc}; use tracing::trace; use winnow::{ binary::{le_u16, le_u32}, @@ -13,11 +12,11 @@ use crate::{ error::{Error, FormatError}, parse::{ zero_datetime, Entry, ExtraField, ExtraFieldSettings, HostSystem, Mode, MsdosMode, - MsdosTimestamp, NtfsAttr, UnixMode, Version, ZipBytes, ZipString, + MsdosTimestamp, UnixMode, Version, ZipBytes, ZipString, }, }; -use super::{EntryCdFields, Method}; +use super::Method; /// 4.3.12 Central directory structure: File header pub struct CentralDirectoryFileHeader { diff --git a/rc-zip/src/parse/version.rs b/rc-zip/src/parse/version.rs index 2348bdf..4e5b2a6 100644 --- a/rc-zip/src/parse/version.rs +++ b/rc-zip/src/parse/version.rs @@ -8,7 +8,7 @@ use winnow::{binary::le_u8, seq, PResult, Parser, Partial}; /// which features are required when reading a file. /// /// For more information, see the [.ZIP Application Note](https://support.pkware.com/display/PKZIP/APPNOTE), section 4.4.2. -#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Clone, Copy)] pub struct Version { /// The host system on which pub host_system: HostSystem, @@ -20,13 +20,7 @@ pub struct Version { impl fmt::Debug for Version { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{:?} v{}.{}", - self.host_system(), - self.major(), - self.minor() - ) + write!(f, "{:?} v{}", self.host_system, self.version) } } @@ -34,7 +28,7 @@ impl Version { /// Parse a version from a byte slice pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { seq! {Self { - host_system: le_u8.map(HostSystem::from_u8), + host_system: le_u8.map(HostSystem::from), version: le_u8, }} .parse_next(i) From d1936550c97bef258293115cdc6e4ae6d7475095 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 20:26:10 +0100 Subject: [PATCH 11/23] Make some types borrowable/ownable, etc. --- Cargo.lock | 51 +++++ rc-zip-sync/examples/jean.rs | 206 +++++++++++------- rc-zip-sync/src/read_zip.rs | 113 +--------- rc-zip-sync/src/streaming_entry_reader.rs | 22 +- rc-zip-tokio/src/async_read_zip.rs | 22 +- rc-zip-tokio/src/entry_reader.rs | 6 +- rc-zip-tokio/src/lib.rs | 2 +- rc-zip/Cargo.toml | 1 + rc-zip/src/corpus/mod.rs | 2 +- rc-zip/src/fsm/archive.rs | 21 +- rc-zip/src/fsm/entry/mod.rs | 90 +++----- rc-zip/src/parse/archive.rs | 14 +- .../parse/central_directory_file_header.rs | 52 +++-- rc-zip/src/parse/date_time.rs | 3 +- rc-zip/src/parse/eocd.rs | 86 +++++--- rc-zip/src/parse/extra_field.rs | 25 ++- rc-zip/src/parse/local_headers.rs | 88 ++++++-- rc-zip/src/parse/mod.rs | 3 - rc-zip/src/parse/raw.rs | 69 ------ rc-zip/src/parse/version.rs | 7 +- 20 files changed, 439 insertions(+), 444 deletions(-) delete mode 100644 rc-zip/src/parse/raw.rs diff --git a/Cargo.lock b/Cargo.lock index 574cfa6..fb0db75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -660,6 +660,27 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "ownable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcba94d1536fcc470287d96fd26356c38da8215fdb9a74285b09621f35d9350" +dependencies = [ + "ownable-macro", +] + +[[package]] +name = "ownable-macro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c91d2781624dec1234581a1a01e63638f36546ad72ee82873ac1b84f41117b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "phf" version = "0.11.2" @@ -748,6 +769,29 @@ dependencies = [ "toml_edit", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.78" @@ -797,6 +841,7 @@ dependencies = [ "num_enum", "oem_cp", "oval", + "ownable", "pretty-hex", "test-log", "thiserror", @@ -1149,6 +1194,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wasm-bindgen" version = "0.2.90" diff --git a/rc-zip-sync/examples/jean.rs b/rc-zip-sync/examples/jean.rs index cff7351..5df6ad6 100644 --- a/rc-zip-sync/examples/jean.rs +++ b/rc-zip-sync/examples/jean.rs @@ -82,15 +82,10 @@ fn main() { fn do_main(cli: Cli) -> Result<(), Box> { fn info(archive: &Archive) { - if let Some(comment) = archive.comment() { - println!("Comment:\n{}", comment); - } - let has_zip64 = archive.entries().any(|entry| entry.inner.is_zip64); - if has_zip64 { - println!("Found Zip64 end of central directory locator") + if !archive.comment().is_empty() { + println!("Comment:\n{}", archive.comment()); } - let mut creator_versions = HashSet::::new(); let mut reader_versions = HashSet::::new(); let mut methods = HashSet::::new(); let mut compressed_size: u64 = 0; @@ -100,7 +95,6 @@ fn do_main(cli: Cli) -> Result<(), Box> { let mut num_files = 0; for entry in archive.entries() { - creator_versions.insert(entry.creator_version); reader_versions.insert(entry.reader_version); match entry.kind() { EntryKind::Symlink => { @@ -110,17 +104,14 @@ fn do_main(cli: Cli) -> Result<(), Box> { num_dirs += 1; } EntryKind::File => { - methods.insert(entry.method()); + methods.insert(entry.method); num_files += 1; - compressed_size += entry.inner.compressed_size; - uncompressed_size += entry.inner.uncompressed_size; + compressed_size += entry.compressed_size; + uncompressed_size += entry.uncompressed_size; } } } - println!( - "Version made by: {:?}, required: {:?}", - creator_versions, reader_versions - ); + println!("Versions: {:?}", reader_versions); println!("Encoding: {}, Methods: {:?}", archive.encoding(), methods); println!( "{} ({:.2}% compression) ({} files, {} dirs, {} symlinks)", @@ -148,36 +139,33 @@ fn do_main(cli: Cli) -> Result<(), Box> { "{mode:>9} {size:>12} {name}", mode = entry.mode, name = if verbose { - Cow::from(entry.name()) + Cow::Borrowed(&entry.name) } else { - Cow::from(entry.name().truncate_path(55)) + Cow::Owned(entry.name.truncate_path(55)) }, - size = format_size(entry.inner.uncompressed_size, BINARY), + size = format_size(entry.uncompressed_size, BINARY), ); if verbose { print!( " ({} compressed)", - format_size(entry.inner.compressed_size, BINARY) + format_size(entry.compressed_size, BINARY) ); print!( " {modified} {uid} {gid}", - modified = entry.modified(), + modified = entry.modified, uid = Optional(entry.uid), gid = Optional(entry.gid), ); - if let EntryKind::Symlink = entry.contents() { + if let EntryKind::Symlink = entry.kind() { let mut target = String::new(); entry.reader().read_to_string(&mut target).unwrap(); print!("\t{target}", target = target); } - print!("\t{:?}", entry.method()); - if entry.inner.is_zip64 { - print!("\tZip64"); - } - if let Some(comment) = entry.comment() { - print!("\t{comment}", comment = comment); + print!("\t{:?}", entry.method); + if !entry.comment.is_empty() { + print!("\t{comment}", comment = entry.comment); } } println!(); @@ -191,12 +179,10 @@ fn do_main(cli: Cli) -> Result<(), Box> { let mut num_dirs = 0; let mut num_files = 0; let mut num_symlinks = 0; - let mut uncompressed_size: u64 = 0; - for entry in reader.entries() { - if let EntryKind::File = entry.contents() { - uncompressed_size += entry.inner.uncompressed_size; - } - } + let uncompressed_size = reader + .entries() + .map(|entry| entry.uncompressed_size) + .sum::(); let mut done_bytes: u64 = 0; use indicatif::{ProgressBar, ProgressStyle}; @@ -212,14 +198,13 @@ fn do_main(cli: Cli) -> Result<(), Box> { let start_time = std::time::SystemTime::now(); for entry in reader.entries() { - let entry_name = entry.name(); - let entry_name = match sanitize_entry_name(entry_name) { + let entry_name = match entry.sanitized_name() { Some(name) => name, None => continue, }; pbar.set_message(entry_name.to_string()); - match entry.contents() { + match entry.kind() { EntryKind::Symlink => { num_symlinks += 1; @@ -274,13 +259,10 @@ fn do_main(cli: Cli) -> Result<(), Box> { let mut entry_writer = File::create(path)?; let entry_reader = entry.reader(); let before_entry_bytes = done_bytes; - let mut progress_reader = ProgressRead::new( - entry_reader, - entry.inner.uncompressed_size, - |prog| { + let mut progress_reader = + ProgressReader::new(entry_reader, entry.uncompressed_size, |prog| { pbar.set_position(before_entry_bytes + prog.done); - }, - ); + }); let copied_bytes = std::io::copy(&mut progress_reader, &mut entry_writer)?; done_bytes = before_entry_bytes + copied_bytes; @@ -300,11 +282,15 @@ fn do_main(cli: Cli) -> Result<(), Box> { let bps = (uncompressed_size as f64 / seconds) as u64; println!("Overall extraction speed: {} / s", format_size(bps, BINARY)); } - Commands::UnzipStreaming { zipfile, .. } => { + Commands::UnzipStreaming { zipfile, dir, .. } => { let zipfile = File::open(zipfile)?; + let dir = PathBuf::from(dir.unwrap_or_else(|| ".".into())); - let mut entry = zipfile.read_first_zip_entry_streaming()?; + let mut num_dirs = 0; + let mut num_files = 0; + let mut num_symlinks = 0; + let mut done_bytes: u64 = 0; use indicatif::{ProgressBar, ProgressStyle}; let pbar = ProgressBar::new(100); pbar.set_style( @@ -314,23 +300,90 @@ fn do_main(cli: Cli) -> Result<(), Box> { .progress_chars("=>-"), ); + let mut uncompressed_size = 0; pbar.enable_steady_tick(Duration::from_millis(125)); + let start_time = std::time::SystemTime::now(); + + let mut entry_reader = zipfile.read_first_zip_entry_streaming()?; loop { - let entry_name = entry.name().unwrap(); - let entry_name = match sanitize_entry_name(entry_name) { + let entry_name = match entry_reader.entry().sanitized_name() { Some(name) => name, None => continue, }; pbar.set_message(entry_name.to_string()); - let mut buf = vec![]; - entry.read_to_end(&mut buf)?; + match entry_reader.entry().kind() { + EntryKind::Symlink => { + num_symlinks += 1; - match entry.finish() { + cfg_if! { + if #[cfg(windows)] { + let path = dir.join(entry_name); + std::fs::create_dir_all( + path.parent() + .expect("all full entry paths should have parent paths"), + )?; + let mut entry_writer = File::create(path)?; + let mut entry_reader = entry.reader(); + std::io::copy(&mut entry_reader, &mut entry_writer)?; + } else { + let path = dir.join(entry_name); + std::fs::create_dir_all( + path.parent() + .expect("all full entry paths should have parent paths"), + )?; + if let Ok(metadata) = std::fs::symlink_metadata(&path) { + if metadata.is_file() { + std::fs::remove_file(&path)?; + } + } + + let mut src = String::new(); + entry_reader.read_to_string(&mut src)?; + + // validate pointing path before creating a symbolic link + if src.contains("..") { + continue; + } + std::os::unix::fs::symlink(src, &path)?; + } + } + } + EntryKind::Directory => { + num_dirs += 1; + let path = dir.join(entry_name); + std::fs::create_dir_all( + path.parent() + .expect("all full entry paths should have parent paths"), + )?; + } + EntryKind::File => { + num_files += 1; + let path = dir.join(entry_name); + std::fs::create_dir_all( + path.parent() + .expect("all full entry paths should have parent paths"), + )?; + let mut entry_writer = File::create(path)?; + let before_entry_bytes = done_bytes; + let total = entry_reader.entry().uncompressed_size; + let mut progress_reader = + ProgressReader::new(entry_reader, total, |prog| { + pbar.set_position(before_entry_bytes + prog.done); + }); + + let copied_bytes = std::io::copy(&mut progress_reader, &mut entry_writer)?; + uncompressed_size += copied_bytes; + done_bytes = before_entry_bytes + copied_bytes; + entry_reader = progress_reader.into_inner(); + } + } + + match entry_reader.finish() { Some(next_entry) => { println!("Found next entry!"); - entry = next_entry; + entry_reader = next_entry; } None => { println!("End of archive!"); @@ -339,6 +392,17 @@ fn do_main(cli: Cli) -> Result<(), Box> { } } pbar.finish(); + let duration = start_time.elapsed()?; + println!( + "Extracted {} (in {} files, {} dirs, {} symlinks)", + format_size(uncompressed_size, BINARY), + num_files, + num_dirs, + num_symlinks + ); + let seconds = (duration.as_millis() as f64) / 1000.0; + let bps = (uncompressed_size as f64 / seconds) as u64; + println!("Overall extraction speed: {} / s", format_size(bps, BINARY)); } } @@ -349,7 +413,7 @@ trait Truncate { fn truncate_path(&self, limit: usize) -> String; } -impl Truncate for &str { +impl Truncate for String { fn truncate_path(&self, limit: usize) -> String { let mut name_tokens: Vec<&str> = Vec::new(); let mut rest_tokens: std::collections::VecDeque<&str> = self.split('/').collect(); @@ -382,7 +446,7 @@ struct Progress { total: u64, } -struct ProgressRead +struct ProgressReader where R: io::Read, F: Fn(Progress), @@ -392,7 +456,7 @@ where progress: Progress, } -impl ProgressRead +impl ProgressReader where R: io::Read, F: Fn(Progress), @@ -406,7 +470,7 @@ where } } -impl io::Read for ProgressRead +impl io::Read for ProgressReader where R: io::Read, F: Fn(Progress), @@ -421,32 +485,12 @@ where } } -/// Sanitize zip entry names: skip entries with traversed/absolute path to -/// mitigate zip slip, and strip absolute prefix on entries pointing to root -/// path. -fn sanitize_entry_name(name: &str) -> Option<&str> { - // refuse entries with traversed/absolute path to mitigate zip slip - if name.contains("..") { - return None; - } - - #[cfg(windows)] - { - if name.contains(":\\") || name.starts_with("\\") { - return None; - } - Some(name) - } - - #[cfg(not(windows))] - { - // strip absolute prefix on entries pointing to root path - let mut entry_chars = name.chars(); - let mut name = name; - while name.starts_with('/') { - entry_chars.next(); - name = entry_chars.as_str() - } - Some(name) +impl ProgressReader +where + R: io::Read, + F: Fn(Progress), +{ + fn into_inner(self) -> R { + self.inner } } diff --git a/rc-zip-sync/src/read_zip.rs b/rc-zip-sync/src/read_zip.rs index 951884c..06d5217 100644 --- a/rc-zip-sync/src/read_zip.rs +++ b/rc-zip-sync/src/read_zip.rs @@ -1,18 +1,11 @@ +use rc_zip::parse::Entry; use rc_zip::{ - chrono::{DateTime, TimeZone, Utc}, - parse::Entry, -}; -use rc_zip::{ - error::{Error, FormatError}, + error::Error, fsm::{ArchiveFsm, FsmResult}, - parse::{Archive, ExtraField, ExtraFieldSettings, LocalFileHeader, NtfsAttr}, + parse::{Archive, LocalFileHeader}, }; use tracing::trace; -use winnow::{ - error::ErrMode, - stream::{AsBytes, Offset}, - Parser, Partial, -}; +use winnow::{error::ErrMode, Parser, Partial}; use crate::entry_reader::EntryReader; use crate::streaming_entry_reader::StreamingEntryReader; @@ -251,10 +244,8 @@ where R: Read, { fn read_first_zip_entry_streaming(mut self) -> Result, Error> { - // first, get enough data to read the first local file header let mut buf = oval::Buffer::with_capacity(16 * 1024); - - let header = loop { + let entry = loop { let n = self.read(buf.space())?; trace!("read {} bytes into buf for first zip entry", n); buf.fill(n); @@ -262,103 +253,15 @@ where let mut input = Partial::new(buf.data()); match LocalFileHeader::parser.parse_next(&mut input) { Ok(header) => { - let consumed = input.as_bytes().offset_from(&buf.data()); - trace!(?header, %consumed, "Got local file header record!"); - // write extra bytes to `/tmp/extra.bin` for debugging - std::fs::write("/tmp/extra.bin", &header.extra.0).unwrap(); - trace!("wrote extra bytes to /tmp/extra.bin"); - - let mut modified: Option> = None; - let mut created: Option> = None; - let mut accessed: Option> = None; - - let mut compressed_size = header.compressed_size as u64; - let mut uncompressed_size = header.uncompressed_size as u64; - - let mut uid: Option = None; - let mut gid: Option = None; - - let mut extra_fields: Vec = Vec::new(); - - let settings = ExtraFieldSettings { - needs_compressed_size: header.compressed_size == !0u32, - needs_uncompressed_size: header.uncompressed_size == !0u32, - needs_header_offset: false, - }; - - let mut slice = Partial::new(&header.extra.0[..]); - while !slice.is_empty() { - match ExtraField::mk_parser(settings).parse_next(&mut slice) { - Ok(ef) => { - match &ef { - ExtraField::Zip64(z64) => { - if let Some(n) = z64.uncompressed_size { - uncompressed_size = n; - } - if let Some(n) = z64.compressed_size { - compressed_size = n; - } - } - ExtraField::Timestamp(ts) => { - modified = Utc.timestamp_opt(ts.mtime as i64, 0).single(); - } - ExtraField::Ntfs(nf) => { - for attr in &nf.attrs { - // note: other attributes are unsupported - if let NtfsAttr::Attr1(attr) = attr { - modified = attr.mtime.to_datetime(); - created = attr.ctime.to_datetime(); - accessed = attr.atime.to_datetime(); - } - } - } - ExtraField::Unix(uf) => { - modified = Utc.timestamp_opt(uf.mtime as i64, 0).single(); - if uid.is_none() { - uid = Some(uf.uid as u32); - } - if gid.is_none() { - gid = Some(uf.gid as u32); - } - } - ExtraField::NewUnix(uf) => { - uid = Some(uf.uid as u32); - gid = Some(uf.uid as u32); - } - _ => {} - }; - extra_fields.push(ef); - } - Err(e) => { - trace!("extra field error: {:#?}", e); - return Err(FormatError::InvalidExtraField.into()); - } - } - } - - trace!( - ?modified, - ?created, - ?accessed, - ?compressed_size, - ?uncompressed_size, - ?uid, - ?gid, - "parsed extra fields" - ); - - break header; - } - // TODO: keep reading if we don't have enough data - Err(ErrMode::Incomplete(_)) => { - // read more + break header.as_entry()?; } + Err(ErrMode::Incomplete(_)) => continue, Err(e) => { panic!("{e}") } } }; - Ok(StreamingEntryReader::new(buf, header, self)) + Ok(StreamingEntryReader::new(buf, entry, self)) } } diff --git a/rc-zip-sync/src/streaming_entry_reader.rs b/rc-zip-sync/src/streaming_entry_reader.rs index 62c8f3a..a707bae 100644 --- a/rc-zip-sync/src/streaming_entry_reader.rs +++ b/rc-zip-sync/src/streaming_entry_reader.rs @@ -1,16 +1,13 @@ use oval::Buffer; use rc_zip::{ fsm::{EntryFsm, FsmResult}, - parse::LocalFileHeader, -}; -use std::{ - io::{self, Write}, - str::Utf8Error, + parse::Entry, }; +use std::io::{self, Write}; use tracing::trace; pub struct StreamingEntryReader { - header: LocalFileHeader, + entry: Entry, rd: R, state: State, } @@ -34,10 +31,10 @@ impl StreamingEntryReader where R: io::Read, { - pub(crate) fn new(remain: Buffer, header: LocalFileHeader, rd: R) -> Self { + pub(crate) fn new(remain: Buffer, entry: Entry, rd: R) -> Self { Self { rd, - header, + entry, state: State::Reading { remain, fsm: EntryFsm::new(None), @@ -117,11 +114,10 @@ where } impl StreamingEntryReader { - /// Return the name of this entry, decoded as UTF-8. - /// - /// There is no support for CP-437 in the streaming interface - pub fn name(&self) -> Result<&str, Utf8Error> { - std::str::from_utf8(&self.header.name.0) + /// Return entry information for this reader + #[inline(always)] + pub fn entry(&self) -> &Entry { + &self.entry } /// Finish reading this entry, returning the next streaming entry reader, if diff --git a/rc-zip-tokio/src/async_read_zip.rs b/rc-zip-tokio/src/async_read_zip.rs index bc68be0..a950866 100644 --- a/rc-zip-tokio/src/async_read_zip.rs +++ b/rc-zip-tokio/src/async_read_zip.rs @@ -7,7 +7,7 @@ use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf}; use rc_zip::{ error::Error, fsm::{ArchiveFsm, FsmResult}, - parse::{Archive, StoredEntry}, + parse::{Archive, Entry}, }; use crate::entry_reader::EntryReader; @@ -125,8 +125,8 @@ where F: HasAsyncCursor, { /// Iterate over all files in this zip, read from the central directory. - pub fn entries(&self) -> impl Iterator> { - self.archive.entries().map(move |entry| AsyncStoredEntry { + pub fn entries(&self) -> impl Iterator> { + self.archive.entries().map(move |entry| AsyncEntry { file: self.file, entry, }) @@ -134,11 +134,11 @@ where /// Attempts to look up an entry by name. This is usually a bad idea, /// as names aren't necessarily normalized in zip archives. - pub fn by_name>(&self, name: N) -> Option> { + pub fn by_name>(&self, name: N) -> Option> { self.archive .entries() - .find(|&x| x.name() == name.as_ref()) - .map(|entry| AsyncStoredEntry { + .find(|&x| x.name == name.as_ref()) + .map(|entry| AsyncEntry { file: self.file, entry, }) @@ -146,20 +146,20 @@ where } /// A single entry in a zip archive, read asynchronously from a file or other I/O resource. -pub struct AsyncStoredEntry<'a, F> { +pub struct AsyncEntry<'a, F> { file: &'a F, - entry: &'a StoredEntry, + entry: &'a Entry, } -impl Deref for AsyncStoredEntry<'_, F> { - type Target = StoredEntry; +impl Deref for AsyncEntry<'_, F> { + type Target = Entry; fn deref(&self) -> &Self::Target { self.entry } } -impl<'a, F> AsyncStoredEntry<'a, F> +impl<'a, F> AsyncEntry<'a, F> where F: HasAsyncCursor, { diff --git a/rc-zip-tokio/src/entry_reader.rs b/rc-zip-tokio/src/entry_reader.rs index d8eb20e..ce18641 100644 --- a/rc-zip-tokio/src/entry_reader.rs +++ b/rc-zip-tokio/src/entry_reader.rs @@ -3,7 +3,7 @@ use std::{pin::Pin, task}; use pin_project_lite::pin_project; use rc_zip::{ fsm::{EntryFsm, FsmResult}, - parse::StoredEntry, + parse::Entry, }; use tokio::io::{AsyncRead, ReadBuf}; @@ -22,13 +22,13 @@ impl EntryReader where R: AsyncRead, { - pub(crate) fn new(entry: &StoredEntry, get_reader: F) -> Self + pub(crate) fn new(entry: &Entry, get_reader: F) -> Self where F: Fn(u64) -> R, { Self { rd: get_reader(entry.header_offset), - fsm: Some(EntryFsm::new(Some(entry.inner))), + fsm: Some(EntryFsm::new(Some(entry.clone()))), } } } diff --git a/rc-zip-tokio/src/lib.rs b/rc-zip-tokio/src/lib.rs index 8666c73..61d4d5b 100644 --- a/rc-zip-tokio/src/lib.rs +++ b/rc-zip-tokio/src/lib.rs @@ -12,6 +12,6 @@ mod entry_reader; // re-exports pub use async_read_zip::{ - AsyncArchive, AsyncStoredEntry, HasAsyncCursor, ReadZipAsync, ReadZipWithSizeAsync, + AsyncArchive, AsyncEntry, HasAsyncCursor, ReadZipAsync, ReadZipWithSizeAsync, }; pub use rc_zip; diff --git a/rc-zip/Cargo.toml b/rc-zip/Cargo.toml index c2a46be..355106b 100644 --- a/rc-zip/Cargo.toml +++ b/rc-zip/Cargo.toml @@ -33,6 +33,7 @@ deflate64 = { version = "0.1.7", optional = true } bzip2 = { version = "0.4.4", optional = true } lzma-rs = { version = "0.3.0", optional = true, features = ["stream"] } zstd = { version = "0.13.0", optional = true } +ownable = "0.6.2" [features] corpus = [] diff --git a/rc-zip/src/corpus/mod.rs b/rc-zip/src/corpus/mod.rs index cab0c15..4b345b8 100644 --- a/rc-zip/src/corpus/mod.rs +++ b/rc-zip/src/corpus/mod.rs @@ -228,7 +228,7 @@ pub fn check_case(test: &Case, archive: Result<&Archive, &Error>) { assert_eq!(case_bytes.len() as u64, archive.size()); if let Some(expected) = test.comment { - assert_eq!(expected, archive.comment().expect("should have comment")) + assert_eq!(expected, archive.comment()) } if let Some(exp_encoding) = test.expected_encoding { diff --git a/rc-zip/src/fsm/archive.rs b/rc-zip/src/fsm/archive.rs index 92a833b..1118987 100644 --- a/rc-zip/src/fsm/archive.rs +++ b/rc-zip/src/fsm/archive.rs @@ -8,6 +8,7 @@ use crate::{ }, }; +use ownable::traits::IntoOwned; use tracing::trace; use winnow::{ error::ErrMode, @@ -54,19 +55,19 @@ enum State { /// Reading the zip64 end of central directory record. ReadEocd64Locator { - eocdr: Located, + eocdr: Located>, }, /// Reading the zip64 end of central directory record. ReadEocd64 { eocdr64_offset: u64, - eocdr: Located, + eocdr: Located>, }, /// Reading all headers from the central directory ReadCentralDirectory { - eocd: EndOfCentralDirectory, - directory_headers: Vec, + eocd: EndOfCentralDirectory<'static>, + directory_headers: Vec>, }, #[default] @@ -140,12 +141,13 @@ impl ArchiveFsm { EndOfCentralDirectoryRecord::find_in_block(haystack) } { None => Err(FormatError::DirectoryEndSignatureNotFound.into()), - Some(mut eocdr) => { + Some(eocdr) => { trace!( ?eocdr, size = self.size, "ReadEocd | found end of central directory record" ); + let mut eocdr = eocdr.into_owned(); self.buffer.reset(); eocdr.offset += self.size - haystack_size; @@ -264,7 +266,7 @@ impl ArchiveFsm { len = input.len(), "ReadCentralDirectory | parsed directory header" ); - directory_headers.push(dh); + directory_headers.push(dh.into_owned()); } Err(ErrMode::Incomplete(_needed)) => { // need more data to read the full header @@ -305,7 +307,7 @@ impl ArchiveFsm { directory_headers.iter().filter(|fh| fh.is_non_utf8()) { all_utf8 = false; - if !feed(&fh.name.0) || !feed(&fh.comment.0) { + if !feed(&fh.name[..]) || !feed(&fh.comment[..]) { break 'recognize_encoding; } } @@ -341,10 +343,7 @@ impl ArchiveFsm { .collect(); let entries = entries?; - let mut comment: Option = None; - if !eocd.comment().0.is_empty() { - comment = Some(encoding.decode(&eocd.comment().0)?); - } + let comment = encoding.decode(eocd.comment())?; return Ok(FsmResult::Done(Archive { size: self.size, diff --git a/rc-zip/src/fsm/entry/mod.rs b/rc-zip/src/fsm/entry/mod.rs index 5d36659..31be553 100644 --- a/rc-zip/src/fsm/entry/mod.rs +++ b/rc-zip/src/fsm/entry/mod.rs @@ -42,11 +42,14 @@ enum State { ReadLocalHeader, ReadData { - /// The local file header for this entry - header: LocalFileHeader, + /// The entry metadata + entry: Entry, - /// Entry compressed size - compressed_size: u64, + /// Whether the entry has a data descriptor + has_data_descriptor: bool, + + /// Whether the entry is zip64 (because its compressed size or uncompressed size is u32::MAX) + is_zip64: bool, /// Amount of bytes we've fed to the decompressor compressed_bytes: u64, @@ -62,16 +65,19 @@ enum State { }, ReadDataDescriptor { - /// The local file header for this entry - header: LocalFileHeader, + /// The entry metadata + entry: Entry, + + /// Whether the entry is zip64 (because its compressed size or uncompressed size is u32::MAX) + is_zip64: bool, /// Size we've decompressed + crc32 hash we've computed metrics: EntryReadMetrics, }, Validate { - /// The local file header for this entry - header: LocalFileHeader, + /// The entry metadata + entry: Entry, /// Size we've decompressed + crc32 hash we've computed metrics: EntryReadMetrics, @@ -157,34 +163,22 @@ impl EntryFsm { Ok(header) => { let consumed = input.as_bytes().offset_from(&self.buffer.data()); tracing::trace!(local_file_header = ?header, consumed, "parsed local file header"); - self.buffer.consume(consumed); let decompressor = AnyDecompressor::new( header.method, self.entry.as_ref().map(|entry| entry.uncompressed_size), )?; - let compressed_size = match &self.entry { - Some(entry) => entry.compressed_size, - None => { - // FIXME: the zip64 extra field is here for that - if header.compressed_size == u32::MAX { - return Err(Error::Decompression { - method: header.method, - msg: "This entry cannot be decompressed because its compressed size is larger than 4GiB".into(), - }); - } else { - header.compressed_size as u64 - } - } - }; self.state = S::ReadData { - header, - compressed_size, + entry: header.as_entry()?, + is_zip64: header.compressed_size == u32::MAX + || header.uncompressed_size == u32::MAX, + has_data_descriptor: header.has_data_descriptor(), compressed_bytes: 0, uncompressed_bytes: 0, hasher: crc32fast::Hasher::new(), decompressor, }; + self.buffer.consume(consumed); self.process(out) } Err(ErrMode::Incomplete(_)) => { @@ -194,7 +188,7 @@ impl EntryFsm { } } S::ReadData { - compressed_size, + entry, compressed_bytes, uncompressed_bytes, hasher, @@ -207,13 +201,13 @@ impl EntryFsm { let in_buf_max_len = cmp::min( in_buf.len(), - *compressed_size as usize - *compressed_bytes as usize, + entry.compressed_size as usize - *compressed_bytes as usize, ); let in_buf = &in_buf[..in_buf_max_len]; let fed_bytes_after_this = *compressed_bytes + in_buf.len() as u64; - let has_more_input = if fed_bytes_after_this == *compressed_size as _ { + let has_more_input = if fed_bytes_after_this == entry.compressed_size as _ { HasMoreInput::No } else { HasMoreInput::Yes @@ -231,16 +225,16 @@ impl EntryFsm { if outcome.bytes_written == 0 && self.eof { // we're done, let's read the data descriptor (if there's one) - transition!(self.state => (S::ReadData { header, uncompressed_bytes, hasher, .. }) { + transition!(self.state => (S::ReadData { entry, has_data_descriptor, is_zip64, uncompressed_bytes, hasher, .. }) { let metrics = EntryReadMetrics { uncompressed_size: uncompressed_bytes, crc32: hasher.finalize(), }; - if header.has_data_descriptor() { - S::ReadDataDescriptor { header, metrics } + if has_data_descriptor { + S::ReadDataDescriptor { entry, metrics, is_zip64 } } else { - S::Validate { header, metrics, descriptor: None } + S::Validate { entry, metrics, descriptor: None } } }); return self.process(out); @@ -259,19 +253,16 @@ impl EntryFsm { Ok(FsmResult::Continue((self, outcome))) } - S::ReadDataDescriptor { header, .. } => { + S::ReadDataDescriptor { is_zip64, .. } => { let mut input = Partial::new(self.buffer.data()); - let is_zip64 = - header.compressed_size == u32::MAX || header.uncompressed_size == u32::MAX; - - match DataDescriptorRecord::mk_parser(is_zip64).parse_next(&mut input) { + match DataDescriptorRecord::mk_parser(*is_zip64).parse_next(&mut input) { Ok(descriptor) => { self.buffer .consume(input.as_bytes().offset_from(&self.buffer.data())); trace!("data descriptor = {:#?}", descriptor); - transition!(self.state => (S::ReadDataDescriptor { metrics, header, .. }) { - S::Validate { metrics, header, descriptor: Some(descriptor) } + transition!(self.state => (S::ReadDataDescriptor { metrics, entry, .. }) { + S::Validate { entry, metrics, descriptor: Some(descriptor) } }); self.process(out) } @@ -282,7 +273,7 @@ impl EntryFsm { } } S::Validate { - header, + entry, metrics, descriptor, } => { @@ -292,25 +283,12 @@ impl EntryFsm { } else if let Some(descriptor) = descriptor.as_ref() { descriptor.crc32 } else { - header.crc32 - }; - - let entry_uncompressed_size = self - .entry - .as_ref() - .map(|e| e.uncompressed_size) - .unwrap_or_default(); - let expected_size = if entry_uncompressed_size != 0 { - entry_uncompressed_size - } else if let Some(descriptor) = descriptor.as_ref() { - descriptor.uncompressed_size - } else { - header.uncompressed_size as u64 + entry.crc32 }; - if expected_size != metrics.uncompressed_size { + if entry.uncompressed_size != metrics.uncompressed_size { return Err(Error::Format(FormatError::WrongSize { - expected: expected_size, + expected: entry.uncompressed_size, actual: metrics.uncompressed_size, })); } diff --git a/rc-zip/src/parse/archive.rs b/rc-zip/src/parse/archive.rs index a6ee74e..fb69647 100644 --- a/rc-zip/src/parse/archive.rs +++ b/rc-zip/src/parse/archive.rs @@ -1,5 +1,6 @@ use chrono::{offset::Utc, DateTime, TimeZone}; use num_enum::{FromPrimitive, IntoPrimitive}; +use ownable::{IntoOwned, ToOwned}; use winnow::{binary::le_u16, PResult, Partial}; use crate::{ @@ -21,11 +22,12 @@ pub struct Archive { pub(crate) size: u64, pub(crate) encoding: Encoding, pub(crate) entries: Vec, - pub(crate) comment: Option, + pub(crate) comment: String, } impl Archive { /// The size of .zip file that was read, in bytes. + #[inline(always)] pub fn size(&self) -> u64 { self.size } @@ -43,14 +45,16 @@ impl Archive { /// Returns the detected character encoding for text fields /// (names, comments) inside this zip archive. + #[inline(always)] pub fn encoding(&self) -> Encoding { self.encoding } /// Returns the comment for this archive, if any. When reading /// a zip file with an empty comment field, this will return None. - pub fn comment(&self) -> Option<&String> { - self.comment.as_ref() + #[inline(always)] + pub fn comment(&self) -> &str { + &self.comment } } @@ -269,7 +273,9 @@ impl Entry { /// /// However, in the wild, it is not too uncommon to encounter [Bzip2][Method::Bzip2], /// [Lzma][Method::Lzma] or others. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IntoPrimitive, FromPrimitive)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, IntoPrimitive, FromPrimitive, IntoOwned, ToOwned, +)] #[repr(u16)] pub enum Method { /// No compression is applied diff --git a/rc-zip/src/parse/central_directory_file_header.rs b/rc-zip/src/parse/central_directory_file_header.rs index 07c4f1b..9dfe7a1 100644 --- a/rc-zip/src/parse/central_directory_file_header.rs +++ b/rc-zip/src/parse/central_directory_file_header.rs @@ -1,8 +1,11 @@ +use std::borrow::Cow; + +use ownable::{IntoOwned, ToOwned}; use tracing::trace; use winnow::{ binary::{le_u16, le_u32}, prelude::PResult, - token::tag, + token::{tag, take}, Parser, Partial, }; @@ -12,14 +15,15 @@ use crate::{ error::{Error, FormatError}, parse::{ zero_datetime, Entry, ExtraField, ExtraFieldSettings, HostSystem, Mode, MsdosMode, - MsdosTimestamp, UnixMode, Version, ZipBytes, ZipString, + MsdosTimestamp, UnixMode, Version, }, }; use super::Method; /// 4.3.12 Central directory structure: File header -pub struct CentralDirectoryFileHeader { +#[derive(IntoOwned, ToOwned)] +pub struct CentralDirectoryFileHeader<'a> { /// version made by pub creator_version: Version, @@ -56,21 +60,21 @@ pub struct CentralDirectoryFileHeader { /// relative offset of local header pub header_offset: u32, - /// name - pub name: ZipString, + /// name field + pub name: Cow<'a, [u8]>, - /// extra - pub extra: ZipBytes, + /// extra field + pub extra: Cow<'a, [u8]>, - /// comment - pub comment: ZipString, + /// comment field + pub comment: Cow<'a, [u8]>, } -impl CentralDirectoryFileHeader { +impl<'a> CentralDirectoryFileHeader<'a> { const SIGNATURE: &'static str = "PK\x01\x02"; /// Parser for the central directory file header - pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { + pub fn parser(i: &mut Partial<&'a [u8]>) -> PResult { _ = tag(Self::SIGNATURE).parse_next(i)?; let creator_version = Version::parser.parse_next(i)?; let reader_version = Version::parser.parse_next(i)?; @@ -88,9 +92,9 @@ impl CentralDirectoryFileHeader { let external_attrs = le_u32.parse_next(i)?; let header_offset = le_u32.parse_next(i)?; - let name = ZipString::parser(name_len).parse_next(i)?; - let extra = ZipBytes::parser(extra_len).parse_next(i)?; - let comment = ZipString::parser(comment_len).parse_next(i)?; + let name = take(name_len).parse_next(i)?; + let extra = take(extra_len).parse_next(i)?; + let comment = take(comment_len).parse_next(i)?; Ok(Self { creator_version, @@ -105,18 +109,18 @@ impl CentralDirectoryFileHeader { internal_attrs, external_attrs, header_offset, - name, - extra, - comment, + name: Cow::Borrowed(name), + extra: Cow::Borrowed(extra), + comment: Cow::Borrowed(comment), }) } } -impl CentralDirectoryFileHeader { +impl CentralDirectoryFileHeader<'_> { /// Returns true if the name or comment is not valid UTF-8 pub fn is_non_utf8(&self) -> bool { - let (valid1, require1) = detect_utf8(&self.name.0[..]); - let (valid2, require2) = detect_utf8(&self.comment.0[..]); + let (valid1, require1) = detect_utf8(&self.name[..]); + let (valid2, require2) = detect_utf8(&self.comment[..]); if !valid1 || !valid2 { // definitely not utf-8 return true; @@ -134,13 +138,13 @@ impl CentralDirectoryFileHeader { self.flags & 0x800 == 0 } - /// Converts the directory header into a stored entry: this involves + /// Converts the directory header into a entry: this involves /// parsing the extra fields and converting the timestamps. pub fn as_entry(&self, encoding: Encoding, global_offset: u64) -> Result { let mut entry = Entry { - name: encoding.decode(&self.name.0)?, + name: encoding.decode(&self.name[..])?, method: self.method, - comment: encoding.decode(&self.comment.0)?, + comment: encoding.decode(&self.comment[..])?, modified: self.modified.to_datetime().unwrap_or_else(zero_datetime), created: None, accessed: None, @@ -173,7 +177,7 @@ impl CentralDirectoryFileHeader { header_offset_u32: self.header_offset, }; - let mut slice = Partial::new(&self.extra.0[..]); + let mut slice = Partial::new(&self.extra[..]); while !slice.is_empty() { match ExtraField::mk_parser(settings).parse_next(&mut slice) { Ok(ef) => { diff --git a/rc-zip/src/parse/date_time.rs b/rc-zip/src/parse/date_time.rs index 2ebdd87..d45ae94 100644 --- a/rc-zip/src/parse/date_time.rs +++ b/rc-zip/src/parse/date_time.rs @@ -2,6 +2,7 @@ use chrono::{ offset::{LocalResult, TimeZone, Utc}, DateTime, Timelike, }; +use ownable::{IntoOwned, ToOwned}; use std::fmt; use winnow::{ binary::{le_u16, le_u64}, @@ -11,7 +12,7 @@ use winnow::{ /// A timestamp in MS-DOS format /// /// Represents dates from year 1980 to 2180, with 2 second precision. -#[derive(Clone, Copy, Eq, PartialEq)] +#[derive(Clone, Copy, Eq, PartialEq, IntoOwned, ToOwned)] pub struct MsdosTimestamp { /// Time in 2-second intervals pub time: u16, diff --git a/rc-zip/src/parse/eocd.rs b/rc-zip/src/parse/eocd.rs index 386b091..ae1692c 100644 --- a/rc-zip/src/parse/eocd.rs +++ b/rc-zip/src/parse/eocd.rs @@ -1,3 +1,6 @@ +use std::borrow::Cow; + +use ownable::{traits as ownable_traits, IntoOwned, ToOwned}; use tracing::trace; use winnow::{ binary::{le_u16, le_u32, le_u64, length_take}, @@ -6,14 +9,11 @@ use winnow::{ PResult, Parser, Partial, }; -use crate::{ - error::{Error, FormatError}, - parse::ZipString, -}; +use crate::error::{Error, FormatError}; /// 4.3.16 End of central directory record: -#[derive(Debug)] -pub struct EndOfCentralDirectoryRecord { +#[derive(Debug, ToOwned, IntoOwned, Clone)] +pub struct EndOfCentralDirectoryRecord<'a> { /// number of this disk pub disk_nbr: u16, @@ -33,16 +33,16 @@ pub struct EndOfCentralDirectoryRecord { pub directory_offset: u32, /// .ZIP file comment - pub comment: ZipString, + pub comment: Cow<'a, [u8]>, } -impl EndOfCentralDirectoryRecord { +impl<'a> EndOfCentralDirectoryRecord<'a> { /// Does not include comment size & comment data const MIN_LENGTH: usize = 20; const SIGNATURE: &'static str = "PK\x05\x06"; /// Find the end of central directory record in a block of data - pub fn find_in_block(b: &[u8]) -> Option> { + pub fn find_in_block(b: &'a [u8]) -> Option> { for i in (0..(b.len() - Self::MIN_LENGTH + 1)).rev() { let mut input = Partial::new(&b[i..]); if let Ok(directory) = Self::parser.parse_next(&mut input) { @@ -56,7 +56,7 @@ impl EndOfCentralDirectoryRecord { } /// Parser for the end of central directory record - pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { + pub fn parser(i: &mut Partial<&'a [u8]>) -> PResult { let _ = tag(Self::SIGNATURE).parse_next(i)?; seq! {Self { disk_nbr: le_u16, @@ -65,7 +65,7 @@ impl EndOfCentralDirectoryRecord { directory_records: le_u16, directory_size: le_u32, directory_offset: le_u32, - comment: length_take(le_u16).map(ZipString::from), + comment: length_take(le_u16).map(Cow::Borrowed), }} .parse_next(i) } @@ -100,7 +100,7 @@ impl EndOfCentralDirectory64Locator { } /// 4.3.14 Zip64 end of central directory record -#[derive(Debug)] +#[derive(Debug, Clone, ToOwned, IntoOwned)] pub struct EndOfCentralDirectory64Record { /// size of zip64 end of central directory record pub record_size: u64, @@ -153,7 +153,7 @@ impl EndOfCentralDirectory64Record { } /// A zip structure and its location in the input file -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Located { /// Absolute by offset from the start of the file pub offset: u64, @@ -162,23 +162,39 @@ pub struct Located { pub inner: T, } -impl std::ops::Deref for Located { - type Target = T; - fn deref(&self) -> &Self::Target { - &self.inner +impl ownable_traits::ToOwned for Located +where + T: ownable_traits::ToOwned, +{ + type Owned = Located; + + fn to_owned(&self) -> Self::Owned { + Located { + offset: self.offset, + inner: self.inner.to_owned(), + } } } -impl std::ops::DerefMut for Located { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner +impl ownable_traits::IntoOwned for Located +where + T: ownable_traits::IntoOwned, +{ + type Owned = Located; + + fn into_owned(self) -> Self::Owned { + Located { + offset: self.offset, + inner: self.inner.into_owned(), + } } } /// Coalesces zip and zip64 "end of central directory" record info -pub struct EndOfCentralDirectory { +#[derive(ToOwned, IntoOwned)] +pub struct EndOfCentralDirectory<'a> { /// The end of central directory record - pub dir: Located, + pub dir: Located>, /// The zip64 end of central directory record pub dir64: Option>, @@ -188,10 +204,10 @@ pub struct EndOfCentralDirectory { pub global_offset: i64, } -impl EndOfCentralDirectory { +impl<'a> EndOfCentralDirectory<'a> { pub(crate) fn new( size: u64, - dir: Located, + dir: Located>, dir64: Option>, ) -> Result { let mut res = Self { @@ -219,7 +235,7 @@ impl EndOfCentralDirectory { // // (e.g. https://www.icculus.org/mojosetup/ installers are ELF binaries with a .zip file appended) // - // `directory_end_offfset` is found by scanning the file (so it accounts for padding), but + // `directory_end_offset` is found by scanning the file (so it accounts for padding), but // `directory_offset` is found by reading a data structure (so it does not account for padding). // If we just trusted `directory_offset`, we'd be reading the central directory at the wrong place: // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -266,37 +282,37 @@ impl EndOfCentralDirectory { #[inline] pub(crate) fn directory_offset(&self) -> u64 { match self.dir64.as_ref() { - Some(d64) => d64.directory_offset, - None => self.dir.directory_offset as u64, + Some(d64) => d64.inner.directory_offset, + None => self.dir.inner.directory_offset as u64, } } #[inline] pub(crate) fn directory_size(&self) -> u64 { match self.dir64.as_ref() { - Some(d64) => d64.directory_size, - None => self.dir.directory_size as u64, + Some(d64) => d64.inner.directory_size, + None => self.dir.inner.directory_size as u64, } } #[inline] pub(crate) fn set_directory_offset(&mut self, offset: u64) { match self.dir64.as_mut() { - Some(d64) => d64.directory_offset = offset, - None => self.dir.directory_offset = offset as u32, + Some(d64) => d64.inner.directory_offset = offset, + None => self.dir.inner.directory_offset = offset as u32, }; } #[inline] pub(crate) fn directory_records(&self) -> u64 { match self.dir64.as_ref() { - Some(d64) => d64.directory_records, - None => self.dir.directory_records as u64, + Some(d64) => d64.inner.directory_records, + None => self.dir.inner.directory_records as u64, } } #[inline] - pub(crate) fn comment(&self) -> &ZipString { - &self.dir.comment + pub(crate) fn comment(&self) -> &[u8] { + &self.dir.inner.comment } } diff --git a/rc-zip/src/parse/extra_field.rs b/rc-zip/src/parse/extra_field.rs index 6513b4e..c540e82 100644 --- a/rc-zip/src/parse/extra_field.rs +++ b/rc-zip/src/parse/extra_field.rs @@ -1,3 +1,6 @@ +use std::borrow::Cow; + +use ownable::{IntoOwned, ToOwned}; use tracing::trace; use winnow::{ binary::{le_u16, le_u32, le_u64, le_u8, length_take}, @@ -8,7 +11,7 @@ use winnow::{ PResult, Parser, Partial, }; -use crate::parse::{NtfsTimestamp, ZipBytes}; +use crate::parse::NtfsTimestamp; /// 4.4.28 extra field: (Variable) pub(crate) struct ExtraFieldRecord<'a> { @@ -58,13 +61,13 @@ pub struct ExtraFieldSettings { /// /// See `extrafld.txt` in this crate's source distribution. #[derive(Clone)] -pub enum ExtraField { +pub enum ExtraField<'a> { /// Zip64 extended information extra field Zip64(ExtraZip64Field), /// Extended timestamp Timestamp(ExtraTimestampField), /// UNIX & Info-Zip UNIX - Unix(ExtraUnixField), + Unix(ExtraUnixField<'a>), /// New UNIX extra field NewUnix(ExtraNewUnixField), /// NTFS (Win9x/WinNT FileTimes) @@ -76,12 +79,12 @@ pub enum ExtraField { }, } -impl ExtraField { +impl<'a> ExtraField<'a> { /// Make a parser for extra fields, given the settings for the zip64 extra /// field (which depend on whether the u32 values are 0xFFFF_FFFF or not) pub fn mk_parser( settings: ExtraFieldSettings, - ) -> impl FnMut(&mut Partial<&'_ [u8]>) -> PResult { + ) -> impl FnMut(&mut Partial<&'a [u8]>) -> PResult { move |i| { use ExtraField as EF; let rec = ExtraFieldRecord::parser.parse_next(i)?; @@ -184,8 +187,8 @@ impl ExtraTimestampField { } /// 4.5.7 -UNIX Extra Field (0x000d): -#[derive(Clone)] -pub struct ExtraUnixField { +#[derive(Clone, ToOwned, IntoOwned)] +pub struct ExtraUnixField<'a> { /// file last access time pub atime: u32, /// file last modification time @@ -195,21 +198,21 @@ pub struct ExtraUnixField { /// file group id pub gid: u16, /// variable length data field - pub data: ZipBytes, + pub data: Cow<'a, [u8]>, } -impl ExtraUnixField { +impl<'a> ExtraUnixField<'a> { const TAG: u16 = 0x000d; const TAG_INFOZIP: u16 = 0x5855; - fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { + fn parser(i: &mut Partial<&'a [u8]>) -> PResult { let t_size = le_u16.parse_next(i)? - 12; seq! {Self { atime: le_u32, mtime: le_u32, uid: le_u16, gid: le_u16, - data: ZipBytes::parser(t_size), + data: take(t_size).map(Cow::Borrowed), }} .parse_next(i) } diff --git a/rc-zip/src/parse/local_headers.rs b/rc-zip/src/parse/local_headers.rs index 6b835b0..9fb7f1e 100644 --- a/rc-zip/src/parse/local_headers.rs +++ b/rc-zip/src/parse/local_headers.rs @@ -1,20 +1,27 @@ +use std::borrow::Cow; + use crate::{ - error::{Error, UnsupportedError}, - parse::{Method, MsdosTimestamp, Version, ZipBytes, ZipString}, + encoding::Encoding, + error::{Error, FormatError, UnsupportedError}, + parse::{Method, MsdosTimestamp, Version}, }; +use ownable::{IntoOwned, ToOwned}; +use tracing::trace; use winnow::{ binary::{le_u16, le_u32, le_u64, le_u8}, combinator::opt, error::{ContextError, ErrMode, ErrorKind, FromExternalError}, seq, - token::tag, + token::{tag, take}, PResult, Parser, Partial, }; -#[derive(Debug)] +use super::{zero_datetime, Entry, ExtraField, ExtraFieldSettings, Mode}; + +#[derive(Debug, ToOwned, IntoOwned)] /// 4.3.7 Local file header -pub struct LocalFileHeader { +pub struct LocalFileHeader<'a> { /// version needed to extract pub reader_version: Version, @@ -37,16 +44,16 @@ pub struct LocalFileHeader { pub uncompressed_size: u32, /// file name - pub name: ZipString, + pub name: Cow<'a, [u8]>, /// extra field - pub extra: ZipBytes, + pub extra: Cow<'a, [u8]>, /// method-specific fields pub method_specific: MethodSpecific, } -#[derive(Debug)] +#[derive(Debug, ToOwned, IntoOwned)] /// Method-specific properties following the local file header pub enum MethodSpecific { /// No method-specific properties @@ -56,12 +63,12 @@ pub enum MethodSpecific { Lzma(LzmaProperties), } -impl LocalFileHeader { +impl<'a> LocalFileHeader<'a> { /// The signature for a local file header pub const SIGNATURE: &'static str = "PK\x03\x04"; /// Parser for the local file header - pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { + pub fn parser(i: &mut Partial<&'a [u8]>) -> PResult { let _ = tag(Self::SIGNATURE).parse_next(i)?; let reader_version = Version::parser.parse_next(i)?; @@ -75,8 +82,8 @@ impl LocalFileHeader { let name_len = le_u16.parse_next(i)?; let extra_len = le_u16.parse_next(i)?; - let name = ZipString::parser(name_len).parse_next(i)?; - let extra = ZipBytes::parser(extra_len).parse_next(i)?; + let name = take(name_len).parse_next(i).map(Cow::Borrowed)?; + let extra = take(extra_len).parse_next(i).map(Cow::Borrowed)?; let method_specific = match method { Method::Lzma => { @@ -114,6 +121,61 @@ impl LocalFileHeader { // purpose bit flag is set (see below). self.flags & 0b1000 != 0 } + + /// + pub fn as_entry(&self) -> Result { + // see APPNOTE 4.4.4: Bit 11 is the language encoding flag (EFS) + let has_utf8_flag = self.flags & 0x800 == 0; + let encoding = if has_utf8_flag { + Encoding::Utf8 + } else { + Encoding::Cp437 + }; + + let mut entry = Entry { + name: encoding.decode(&self.name[..])?, + method: self.method, + comment: Default::default(), + modified: self.modified.to_datetime().unwrap_or_else(zero_datetime), + created: None, + accessed: None, + header_offset: 0, + reader_version: self.reader_version, + flags: self.flags, + uid: None, + gid: None, + crc32: self.crc32, + compressed_size: self.compressed_size as _, + uncompressed_size: self.uncompressed_size as _, + mode: Mode(0), + }; + + if entry.name.ends_with('/') { + // believe it or not, this is straight from the APPNOTE + entry.mode |= Mode::DIR + }; + + let mut slice = Partial::new(&self.extra[..]); + let settings = ExtraFieldSettings { + compressed_size_u32: self.compressed_size, + uncompressed_size_u32: self.uncompressed_size, + header_offset_u32: 0, + }; + + while !slice.is_empty() { + match ExtraField::mk_parser(settings).parse_next(&mut slice) { + Ok(ef) => { + entry.set_extra_field(&ef); + } + Err(e) => { + trace!("extra field error: {:#?}", e); + return Err(FormatError::InvalidExtraField.into()); + } + } + } + + Ok(entry) + } } /// 4.3.9 Data descriptor: @@ -163,7 +225,7 @@ impl DataDescriptorRecord { } /// 5.8.5 LZMA Properties header -#[derive(Debug)] +#[derive(Debug, ToOwned, IntoOwned)] pub struct LzmaProperties { /// major version pub major: u8, diff --git a/rc-zip/src/parse/mod.rs b/rc-zip/src/parse/mod.rs index fc41699..6a7240e 100644 --- a/rc-zip/src/parse/mod.rs +++ b/rc-zip/src/parse/mod.rs @@ -30,6 +30,3 @@ pub use eocd::*; mod local_headers; pub use local_headers::*; - -mod raw; -pub use raw::*; diff --git a/rc-zip/src/parse/raw.rs b/rc-zip/src/parse/raw.rs deleted file mode 100644 index 9a86943..0000000 --- a/rc-zip/src/parse/raw.rs +++ /dev/null @@ -1,69 +0,0 @@ -use pretty_hex::PrettyHex; -use std::fmt; -use winnow::{stream::ToUsize, token::take, PResult, Parser, Partial}; - -/// A raw zip string, with no specific encoding. -/// -/// This is used while parsing a zip archive's central directory, -/// before we know what encoding is used. -#[derive(Clone)] -pub struct ZipString(pub Vec); - -impl<'a> From<&'a [u8]> for ZipString { - fn from(slice: &'a [u8]) -> Self { - Self(slice.into()) - } -} - -impl fmt::Debug for ZipString { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match std::str::from_utf8(&self.0) { - Ok(s) => write!(f, "{:?}", s), - Err(_) => write!(f, "[non-utf8 string: {}]", self.0.hex_dump()), - } - } -} - -impl ZipString { - pub(crate) fn parser(count: C) -> impl FnMut(&mut Partial<&'_ [u8]>) -> PResult - where - C: ToUsize, - { - let count = count.to_usize(); - move |i| (take(count).map(|slice: &[u8]| Self(slice.into()))).parse_next(i) - } -} - -/// A raw u8 slice, with no specific structure. -/// -/// This is used while parsing a zip archive, when we want -/// to retain an owned slice to be parsed later. -#[derive(Clone)] -pub struct ZipBytes(pub Vec); - -impl fmt::Debug for ZipBytes { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - const MAX_SHOWN_SIZE: usize = 10; - let data = &self.0[..]; - let (slice, extra) = if data.len() > MAX_SHOWN_SIZE { - (&self.0[..MAX_SHOWN_SIZE], Some(data.len() - MAX_SHOWN_SIZE)) - } else { - (&self.0[..], None) - }; - write!(f, "{}", slice.hex_dump())?; - if let Some(extra) = extra { - write!(f, " (+ {} bytes)", extra)?; - } - Ok(()) - } -} - -impl ZipBytes { - pub(crate) fn parser(count: C) -> impl FnMut(&mut Partial<&'_ [u8]>) -> PResult - where - C: ToUsize, - { - let count = count.to_usize(); - move |i| (take(count).map(|slice: &[u8]| Self(slice.into()))).parse_next(i) - } -} diff --git a/rc-zip/src/parse/version.rs b/rc-zip/src/parse/version.rs index 4e5b2a6..15338bd 100644 --- a/rc-zip/src/parse/version.rs +++ b/rc-zip/src/parse/version.rs @@ -1,4 +1,5 @@ use num_enum::{FromPrimitive, IntoPrimitive}; +use ownable::{IntoOwned, ToOwned}; use std::fmt; use winnow::{binary::le_u8, seq, PResult, Parser, Partial}; @@ -8,7 +9,7 @@ use winnow::{binary::le_u8, seq, PResult, Parser, Partial}; /// which features are required when reading a file. /// /// For more information, see the [.ZIP Application Note](https://support.pkware.com/display/PKZIP/APPNOTE), section 4.4.2. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, ToOwned, IntoOwned, PartialEq, Eq, Hash)] pub struct Version { /// The host system on which pub host_system: HostSystem, @@ -38,7 +39,9 @@ impl Version { /// System on which an archive was created, as encoded into a version u16. /// /// See APPNOTE, section 4.4.2.2 -#[derive(Debug, Clone, Copy, IntoPrimitive, FromPrimitive)] +#[derive( + Debug, Clone, Copy, IntoPrimitive, FromPrimitive, ToOwned, IntoOwned, PartialEq, Eq, Hash, +)] #[repr(u8)] pub enum HostSystem { /// MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems) From ebae9d923cb8598f6cf1f1d1a833bf29b3de3d98 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 20:27:26 +0100 Subject: [PATCH 12/23] Some zip64 errors, interesting --- rc-zip/src/corpus/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rc-zip/src/corpus/mod.rs b/rc-zip/src/corpus/mod.rs index 4b345b8..508a12b 100644 --- a/rc-zip/src/corpus/mod.rs +++ b/rc-zip/src/corpus/mod.rs @@ -223,7 +223,7 @@ pub fn check_case(test: &Case, archive: Result<&Archive, &Error>) { assert_eq!(expected, actual); return; } - let archive = archive.unwrap(); + let archive = archive.unwrap_or_else(|_| panic!("{} should have succeeded", test.name)); assert_eq!(case_bytes.len() as u64, archive.size()); From a09fc7d283ac1d2eb98d04d635d620587bd1e796 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 20:31:11 +0100 Subject: [PATCH 13/23] Now failing modes? --- rc-zip/src/parse/extra_field.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/rc-zip/src/parse/extra_field.rs b/rc-zip/src/parse/extra_field.rs index c540e82..40597fb 100644 --- a/rc-zip/src/parse/extra_field.rs +++ b/rc-zip/src/parse/extra_field.rs @@ -88,7 +88,11 @@ impl<'a> ExtraField<'a> { move |i| { use ExtraField as EF; let rec = ExtraFieldRecord::parser.parse_next(i)?; - trace!("parsing extra field record, tag {:04x}", rec.tag); + trace!( + "parsing extra field record, tag {:04x}, len {}", + rec.tag, + rec.payload.len() + ); let payload = &mut Partial::new(rec.payload); let variant = match rec.tag { @@ -129,7 +133,7 @@ pub struct ExtraZip64Field { pub header_offset: u64, /// 32-bit disk start number - pub disk_start: u32, + pub disk_start: Option, } impl ExtraZip64Field { @@ -154,7 +158,7 @@ impl ExtraZip64Field { } else { settings.header_offset_u32 as u64 }; - let disk_start = le_u32.parse_next(i)?; + let disk_start = opt(le_u32.complete_err()).parse_next(i)?; Ok(Self { uncompressed_size, From 72a9afcabd638e295b0aeac4d8554b8a59ec23e9 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 20:35:14 +0100 Subject: [PATCH 14/23] Woops, version fields are the other way around --- rc-zip/src/parse/version.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rc-zip/src/parse/version.rs b/rc-zip/src/parse/version.rs index 15338bd..4a46204 100644 --- a/rc-zip/src/parse/version.rs +++ b/rc-zip/src/parse/version.rs @@ -29,8 +29,8 @@ impl Version { /// Parse a version from a byte slice pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult { seq! {Self { - host_system: le_u8.map(HostSystem::from), version: le_u8, + host_system: le_u8.map(HostSystem::from), }} .parse_next(i) } From bdcf1091932d3a60797cb2a09a8c72a053d87789 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 20:38:58 +0100 Subject: [PATCH 15/23] Truncated errors now --- rc-zip-sync/tests/integration_tests.rs | 2 ++ rc-zip/src/parse/local_headers.rs | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/rc-zip-sync/tests/integration_tests.rs b/rc-zip-sync/tests/integration_tests.rs index a459318..e371611 100644 --- a/rc-zip-sync/tests/integration_tests.rs +++ b/rc-zip-sync/tests/integration_tests.rs @@ -15,10 +15,12 @@ fn check_case(test: &Case, archive: Result, Err }; for file in &test.files { + tracing::info!("checking file {}", file.name); let entry = archive .by_name(file.name) .unwrap_or_else(|| panic!("entry {} should exist", file.name)); + tracing::info!("got entry for {}", file.name); corpus::check_file_against(file, &entry, &entry.bytes().unwrap()[..]) } } diff --git a/rc-zip/src/parse/local_headers.rs b/rc-zip/src/parse/local_headers.rs index 9fb7f1e..debf98a 100644 --- a/rc-zip/src/parse/local_headers.rs +++ b/rc-zip/src/parse/local_headers.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use crate::{ - encoding::Encoding, + encoding::{detect_utf8, Encoding}, error::{Error, FormatError, UnsupportedError}, parse::{Method, MsdosTimestamp, Version}, }; @@ -122,18 +122,19 @@ impl<'a> LocalFileHeader<'a> { self.flags & 0b1000 != 0 } - /// + /// Converts the local file header into an entry. pub fn as_entry(&self) -> Result { // see APPNOTE 4.4.4: Bit 11 is the language encoding flag (EFS) let has_utf8_flag = self.flags & 0x800 == 0; - let encoding = if has_utf8_flag { + let encoding = if has_utf8_flag && detect_utf8(&self.name[..]).0 { Encoding::Utf8 } else { Encoding::Cp437 }; + let name = encoding.decode(&self.name[..])?; let mut entry = Entry { - name: encoding.decode(&self.name[..])?, + name, method: self.method, comment: Default::default(), modified: self.modified.to_datetime().unwrap_or_else(zero_datetime), From 9fb6d4e1a27380024f06d5849191993b5be1d941 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 20:42:45 +0100 Subject: [PATCH 16/23] Tests pass again --- rc-zip/src/fsm/entry/mod.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/rc-zip/src/fsm/entry/mod.rs b/rc-zip/src/fsm/entry/mod.rs index 31be553..51d0c4d 100644 --- a/rc-zip/src/fsm/entry/mod.rs +++ b/rc-zip/src/fsm/entry/mod.rs @@ -169,7 +169,10 @@ impl EntryFsm { )?; self.state = S::ReadData { - entry: header.as_entry()?, + entry: match &self.entry { + Some(entry) => entry.clone(), + None => header.as_entry()?, + }, is_zip64: header.compressed_size == u32::MAX || header.uncompressed_size == u32::MAX, has_data_descriptor: header.has_data_descriptor(), @@ -206,12 +209,21 @@ impl EntryFsm { let in_buf = &in_buf[..in_buf_max_len]; let fed_bytes_after_this = *compressed_bytes + in_buf.len() as u64; - let has_more_input = if fed_bytes_after_this == entry.compressed_size as _ { HasMoreInput::No } else { HasMoreInput::Yes }; + + trace!( + compressed_bytes = *compressed_bytes, + uncompressed_bytes = *uncompressed_bytes, + fed_bytes_after_this, + in_buf_len = in_buf.len(), + ?has_more_input, + "decompressing" + ); + let outcome = decompressor.decompress(in_buf, out, has_more_input)?; trace!( ?outcome, @@ -355,6 +367,8 @@ pub struct DecompressOutcome { pub bytes_written: usize, } +/// Returns whether there's more input to be fed to the decompressor +#[derive(Debug)] pub enum HasMoreInput { Yes, No, From 9795d55b4089951cc8fc33133caff50fceb45fb2 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 20:50:34 +0100 Subject: [PATCH 17/23] Read larger archives properly (many entries) --- rc-zip/src/fsm/archive.rs | 146 ++++++++++++++++++++------------------ 1 file changed, 78 insertions(+), 68 deletions(-) diff --git a/rc-zip/src/fsm/archive.rs b/rc-zip/src/fsm/archive.rs index 1118987..656af7d 100644 --- a/rc-zip/src/fsm/archive.rs +++ b/rc-zip/src/fsm/archive.rs @@ -251,6 +251,7 @@ impl ArchiveFsm { "ReadCentralDirectory | process(), available: {}", self.buffer.available_data() ); + let mut valid_consumed = 0; let mut input = Partial::new(self.buffer.data()); trace!( initial_offset = input.as_bytes().offset_from(&self.buffer.data()), @@ -266,6 +267,8 @@ impl ArchiveFsm { len = input.len(), "ReadCentralDirectory | parsed directory header" ); + valid_consumed = + input.as_bytes().offset_from(&self.buffer.data()) as usize; directory_headers.push(dh.into_owned()); } Err(ErrMode::Incomplete(_needed)) => { @@ -273,7 +276,7 @@ impl ArchiveFsm { trace!("ReadCentralDirectory | incomplete!"); break 'read_headers; } - Err(ErrMode::Backtrack(_err)) | Err(ErrMode::Cut(_err)) => { + Err(ErrMode::Backtrack(err)) | Err(ErrMode::Cut(err)) => { // this is the normal end condition when reading // the central directory (due to 65536-entries non-zip64 files) // let's just check a few numbers first. @@ -282,88 +285,95 @@ impl ArchiveFsm { let expected_records = directory_headers.len() as u16; let actual_records = eocd.directory_records() as u16; - if expected_records == actual_records { - let mut detectorng = chardetng::EncodingDetector::new(); - let mut all_utf8 = true; - let mut had_suspicious_chars_for_cp437 = false; + if expected_records != actual_records { + tracing::trace!( + "error while reading central records: we read {} records, but EOCD announced {}. the last failed with: {err:?} (display: {err}). at that point, input had length {}", + expected_records, + actual_records, + input.len() + ); - { - let max_feed: usize = 4096; - let mut total_fed: usize = 0; - let mut feed = |slice: &[u8]| { - detectorng.feed(slice, false); - for b in slice { - if (0xB0..=0xDF).contains(b) { - // those are, like, box drawing characters - had_suspicious_chars_for_cp437 = true; - } + // if we read the wrong number of directory entries, + // error out. + return Err(FormatError::InvalidCentralRecord { + expected: expected_records, + actual: actual_records, + } + .into()); + } + + let mut detectorng = chardetng::EncodingDetector::new(); + let mut all_utf8 = true; + let mut had_suspicious_chars_for_cp437 = false; + + { + let max_feed: usize = 4096; + let mut total_fed: usize = 0; + let mut feed = |slice: &[u8]| { + detectorng.feed(slice, false); + for b in slice { + if (0xB0..=0xDF).contains(b) { + // those are, like, box drawing characters + had_suspicious_chars_for_cp437 = true; } + } - total_fed += slice.len(); - total_fed < max_feed - }; + total_fed += slice.len(); + total_fed < max_feed + }; - 'recognize_encoding: for fh in - directory_headers.iter().filter(|fh| fh.is_non_utf8()) - { - all_utf8 = false; - if !feed(&fh.name[..]) || !feed(&fh.comment[..]) { - break 'recognize_encoding; - } + 'recognize_encoding: for fh in + directory_headers.iter().filter(|fh| fh.is_non_utf8()) + { + all_utf8 = false; + if !feed(&fh.name[..]) || !feed(&fh.comment[..]) { + break 'recognize_encoding; } } + } - let encoding = { - if all_utf8 { - Encoding::Utf8 - } else { - let encoding = detectorng.guess(None, true); - if encoding == encoding_rs::SHIFT_JIS { - // well hold on, sometimes Codepage 437 is detected as - // Shift-JIS by chardetng. If we have any characters - // that aren't valid DOS file names, then okay it's probably - // Shift-JIS. Otherwise, assume it's CP437. - if had_suspicious_chars_for_cp437 { - Encoding::ShiftJis - } else { - Encoding::Cp437 - } - } else if encoding == encoding_rs::UTF_8 { - Encoding::Utf8 + let encoding = { + if all_utf8 { + Encoding::Utf8 + } else { + let encoding = detectorng.guess(None, true); + if encoding == encoding_rs::SHIFT_JIS { + // well hold on, sometimes Codepage 437 is detected as + // Shift-JIS by chardetng. If we have any characters + // that aren't valid DOS file names, then okay it's probably + // Shift-JIS. Otherwise, assume it's CP437. + if had_suspicious_chars_for_cp437 { + Encoding::ShiftJis } else { Encoding::Cp437 } + } else if encoding == encoding_rs::UTF_8 { + Encoding::Utf8 + } else { + Encoding::Cp437 } - }; - - let global_offset = eocd.global_offset as u64; - let entries: Result, Error> = directory_headers - .iter() - .map(|x| x.as_entry(encoding, global_offset)) - .collect(); - let entries = entries?; - - let comment = encoding.decode(eocd.comment())?; - - return Ok(FsmResult::Done(Archive { - size: self.size, - comment, - entries, - encoding, - })); - } else { - // if we read the wrong number of directory entries, - // error out. - return Err(FormatError::InvalidCentralRecord { - expected: expected_records, - actual: actual_records, } - .into()); - } + }; + + let global_offset = eocd.global_offset as u64; + let entries: Result, Error> = directory_headers + .iter() + .map(|x| x.as_entry(encoding, global_offset)) + .collect(); + let entries = entries?; + + let comment = encoding.decode(eocd.comment())?; + + return Ok(FsmResult::Done(Archive { + size: self.size, + comment, + entries, + encoding, + })); } } } - let consumed = input.as_bytes().offset_from(&self.buffer.data()); + let consumed = valid_consumed; tracing::trace!(%consumed, "ReadCentralDirectory total consumed"); self.buffer.consume(consumed); From f57968416ff9521265e3062609be3e23d0c3781f Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 21:30:04 +0100 Subject: [PATCH 18/23] Add zip with a large amount of entries to avoid regressions --- Cargo.lock | 7 ++ Justfile | 2 +- rc-zip-sync/src/lib.rs | 2 + rc-zip-sync/src/streaming_entry_reader.rs | 5 + rc-zip-sync/tests/integration_tests.rs | 25 +++-- rc-zip-tokio/src/async_read_zip.rs | 7 +- rc-zip-tokio/tests/integration_tests.rs | 21 ++-- rc-zip/Cargo.toml | 3 +- rc-zip/src/corpus/mod.rs | 118 ++++++++++++++++------ rc-zip/src/fsm/archive.rs | 3 +- rc-zip/src/parse/archive.rs | 6 +- rc-zip/tests/integration_tests.rs | 11 +- testdata/wine-zeroed.zip.bz2 | Bin 0 -> 140790 bytes 13 files changed, 141 insertions(+), 69 deletions(-) create mode 100644 testdata/wine-zeroed.zip.bz2 diff --git a/Cargo.lock b/Cargo.lock index fb0db75..d6df0af 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -843,6 +843,7 @@ dependencies = [ "oval", "ownable", "pretty-hex", + "temp-dir", "test-log", "thiserror", "tracing", @@ -1017,6 +1018,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "temp-dir" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd16aa9ffe15fe021c6ee3766772132c6e98dfa395a167e16864f61a9cfb71d6" + [[package]] name = "test-log" version = "0.2.14" diff --git a/Justfile b/Justfile index 1e869da..0b3e771 100644 --- a/Justfile +++ b/Justfile @@ -6,7 +6,7 @@ _default: check: cargo hack clippy --each-feature -docs: +doc: RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps # Run all tests locally diff --git a/rc-zip-sync/src/lib.rs b/rc-zip-sync/src/lib.rs index 469f489..5c295fb 100644 --- a/rc-zip-sync/src/lib.rs +++ b/rc-zip-sync/src/lib.rs @@ -9,7 +9,9 @@ mod entry_reader; mod read_zip; + mod streaming_entry_reader; +pub use streaming_entry_reader::StreamingEntryReader; // re-exports pub use rc_zip; diff --git a/rc-zip-sync/src/streaming_entry_reader.rs b/rc-zip-sync/src/streaming_entry_reader.rs index a707bae..d7e1c53 100644 --- a/rc-zip-sync/src/streaming_entry_reader.rs +++ b/rc-zip-sync/src/streaming_entry_reader.rs @@ -6,6 +6,11 @@ use rc_zip::{ use std::io::{self, Write}; use tracing::trace; +/// Reads a zip entry based on a local header. Some information is missing, +/// not all name encodings may work, and only by reading it in its entirety +/// can you move on to the next entry. +/// +/// However, it only requires an [io::Read], and does not need to seek. pub struct StreamingEntryReader { entry: Entry, rd: R, diff --git a/rc-zip-sync/tests/integration_tests.rs b/rc-zip-sync/tests/integration_tests.rs index e371611..7b22513 100644 --- a/rc-zip-sync/tests/integration_tests.rs +++ b/rc-zip-sync/tests/integration_tests.rs @@ -1,5 +1,5 @@ use rc_zip::{ - corpus::{self, zips_dir, Case}, + corpus::{self, zips_dir, Case, Files}, error::Error, parse::Archive, }; @@ -14,14 +14,16 @@ fn check_case(test: &Case, archive: Result, Err Err(_) => return, }; - for file in &test.files { - tracing::info!("checking file {}", file.name); - let entry = archive - .by_name(file.name) - .unwrap_or_else(|| panic!("entry {} should exist", file.name)); + if let Files::ExhaustiveList(files) = &test.files { + for file in files { + tracing::info!("checking file {}", file.name); + let entry = archive + .by_name(file.name) + .unwrap_or_else(|| panic!("entry {} should exist", file.name)); - tracing::info!("got entry for {}", file.name); - corpus::check_file_against(file, &entry, &entry.bytes().unwrap()[..]) + tracing::info!("got entry for {}", file.name); + corpus::check_file_against(file, &entry, &entry.bytes().unwrap()[..]) + } } } @@ -45,9 +47,10 @@ fn real_world_files() { for case in corpus::test_cases() { tracing::info!("============ testing {}", case.name); - let file = File::open(case.absolute_path()).unwrap(); + let guarded_path = case.absolute_path(); + let file = File::open(&guarded_path.path).unwrap(); let archive = file.read_zip().map_err(Error::from); - - check_case(&case, archive) + check_case(&case, archive); + drop(guarded_path) } } diff --git a/rc-zip-tokio/src/async_read_zip.rs b/rc-zip-tokio/src/async_read_zip.rs index a950866..427ab78 100644 --- a/rc-zip-tokio/src/async_read_zip.rs +++ b/rc-zip-tokio/src/async_read_zip.rs @@ -1,4 +1,4 @@ -use std::{io, ops::Deref, pin::Pin, sync::Arc, task}; +use std::{cmp, io, ops::Deref, pin::Pin, sync::Arc, task}; use futures::future::BoxFuture; use positioned_io::{RandomAccessFile, ReadAt, Size}; @@ -31,7 +31,7 @@ pub trait ReadZipWithSizeAsync { /// /// This only contains metadata for the archive and its entries. Separate /// readers can be created for arbitraries entries on-demand using -/// [AsyncStoredEntry::reader]. +/// [AsyncEntry::reader]. pub trait ReadZipAsync { /// The type of the file to read from. type File: HasAsyncCursor; @@ -259,9 +259,10 @@ impl AsyncRead for AsyncRandomAccessFileCursor { ARAFCState::Idle(core) => core, _ => unreachable!(), }; + let read_len = cmp::min(buf.remaining(), core.inner_buf.len()); let pos = self.pos; let fut = Box::pin(tokio::task::spawn_blocking(move || { - let read = core.file.read_at(pos, &mut core.inner_buf); + let read = core.file.read_at(pos, &mut core.inner_buf[..read_len]); (read, core) })); self.state = ARAFCState::Reading { fut }; diff --git a/rc-zip-tokio/tests/integration_tests.rs b/rc-zip-tokio/tests/integration_tests.rs index 7cf49c2..0993e13 100644 --- a/rc-zip-tokio/tests/integration_tests.rs +++ b/rc-zip-tokio/tests/integration_tests.rs @@ -1,6 +1,6 @@ use positioned_io::RandomAccessFile; use rc_zip::{ - corpus::{self, zips_dir, Case}, + corpus::{self, zips_dir, Case, Files}, error::Error, parse::Archive, }; @@ -15,12 +15,14 @@ async fn check_case(test: &Case, archive: Result return, }; - for file in &test.files { - let entry = archive - .by_name(file.name) - .unwrap_or_else(|| panic!("entry {} should exist", file.name)); + if let Files::ExhaustiveList(files) = &test.files { + for file in files { + let entry = archive + .by_name(file.name) + .unwrap_or_else(|| panic!("entry {} should exist", file.name)); - corpus::check_file_against(file, &entry, &entry.bytes().await.unwrap()[..]) + corpus::check_file_against(file, &entry, &entry.bytes().await.unwrap()[..]) + } } } @@ -44,9 +46,10 @@ async fn real_world_files() { for case in corpus::test_cases() { tracing::info!("============ testing {}", case.name); - let file = Arc::new(RandomAccessFile::open(case.absolute_path()).unwrap()); + let guarded_path = case.absolute_path(); + let file = Arc::new(RandomAccessFile::open(&guarded_path.path).unwrap()); let archive = file.read_zip_async().await; - - check_case(&case, archive).await + check_case(&case, archive).await; + drop(guarded_path) } } diff --git a/rc-zip/Cargo.toml b/rc-zip/Cargo.toml index 355106b..8551ceb 100644 --- a/rc-zip/Cargo.toml +++ b/rc-zip/Cargo.toml @@ -34,9 +34,10 @@ bzip2 = { version = "0.4.4", optional = true } lzma-rs = { version = "0.3.0", optional = true, features = ["stream"] } zstd = { version = "0.13.0", optional = true } ownable = "0.6.2" +temp-dir = { version = "0.1.12", optional = true } [features] -corpus = [] +corpus = ["dep:temp-dir", "dep:bzip2"] deflate = ["dep:miniz_oxide"] deflate64 = ["dep:deflate64"] bzip2 = ["dep:bzip2"] diff --git a/rc-zip/src/corpus/mod.rs b/rc-zip/src/corpus/mod.rs index 508a12b..0161e63 100644 --- a/rc-zip/src/corpus/mod.rs +++ b/rc-zip/src/corpus/mod.rs @@ -2,9 +2,10 @@ //! A corpus of zip files for testing. -use std::path::PathBuf; +use std::{fs::File, path::PathBuf}; use chrono::{DateTime, FixedOffset, TimeZone, Timelike, Utc}; +use temp_dir::TempDir; use crate::{ encoding::Encoding, @@ -16,25 +17,67 @@ pub struct Case { pub name: &'static str, pub expected_encoding: Option, pub comment: Option<&'static str>, - pub files: Vec, + pub files: Files, pub error: Option, } +pub enum Files { + ExhaustiveList(Vec), + NumFiles(usize), +} + +impl Files { + fn len(&self) -> usize { + match self { + Self::ExhaustiveList(list) => list.len(), + Self::NumFiles(n) => *n, + } + } +} + impl Default for Case { fn default() -> Self { Self { name: "test.zip", expected_encoding: None, comment: None, - files: vec![], + files: Files::NumFiles(0), error: None, } } } +/// This path may disappear on drop (if the zip is bz2-compressed), so be +/// careful +pub struct GuardedPath { + pub path: PathBuf, + _guard: Option, +} + impl Case { - pub fn absolute_path(&self) -> PathBuf { - zips_dir().join(self.name) + pub fn absolute_path(&self) -> GuardedPath { + let path = zips_dir().join(self.name); + if let Some(dec_name) = self.name.strip_suffix(".bz2") { + let dir = TempDir::new().unwrap(); + let dec_path = dir.path().join(dec_name); + std::io::copy( + &mut File::open(&path).unwrap(), + &mut bzip2::write::BzDecoder::new(File::create(&dec_path).unwrap()), + ) + .unwrap(); + tracing::trace!("decompressed {} to {}", path.display(), dec_path.display()); + GuardedPath { + path: dec_path, + _guard: Some(dir), + } + } else { + GuardedPath { path, _guard: None } + } + } + + pub fn bytes(&self) -> Vec { + let gp = self.absolute_path(); + std::fs::read(gp.path).unwrap() } } @@ -92,21 +135,21 @@ pub fn test_cases() -> Vec { vec![ Case { name: "zip64.zip", - files: vec![CaseFile { + files: Files::ExhaustiveList(vec![CaseFile { name: "README", content: FileContent::Bytes( "This small file is in ZIP64 format.\n".as_bytes().into(), ), modified: Some(date((2012, 8, 10), (14, 33, 32), 0, time_zone(0)).unwrap()), mode: Some(0o644), - }], + }]), ..Default::default() }, Case { name: "test.zip", comment: Some("This is a zipfile comment."), expected_encoding: Some(Encoding::Utf8), - files: vec![ + files: Files::ExhaustiveList(vec![ CaseFile { name: "test.txt", content: FileContent::Bytes("This is a test text file.\n".as_bytes().into()), @@ -119,22 +162,22 @@ pub fn test_cases() -> Vec { modified: Some(date((2010, 9, 5), (15, 52, 58), 0, time_zone(10)).unwrap()), mode: Some(0o644), }, - ], + ]), ..Default::default() }, Case { name: "cp-437.zip", expected_encoding: Some(Encoding::Cp437), - files: vec![CaseFile { + files: Files::ExhaustiveList(vec![CaseFile { name: "français", ..Default::default() - }], + }]), ..Default::default() }, Case { name: "shift-jis.zip", expected_encoding: Some(Encoding::ShiftJis), - files: vec![ + files: Files::ExhaustiveList(vec![ CaseFile { name: "should-be-jis/", ..Default::default() @@ -143,42 +186,48 @@ pub fn test_cases() -> Vec { name: "should-be-jis/ot_運命のワルツネぞなぞ小さな楽しみ遊びま.longboi", ..Default::default() }, - ], + ]), ..Default::default() }, Case { name: "utf8-winrar.zip", expected_encoding: Some(Encoding::Utf8), - files: vec![CaseFile { + files: Files::ExhaustiveList(vec![CaseFile { name: "世界", content: FileContent::Bytes(vec![]), modified: Some(date((2017, 11, 6), (21, 9, 27), 867862500, time_zone(0)).unwrap()), ..Default::default() - }], + }]), + ..Default::default() + }, + Case { + name: "wine-zeroed.zip.bz2", + expected_encoding: Some(Encoding::Utf8), + files: Files::NumFiles(11372), ..Default::default() }, #[cfg(feature = "lzma")] Case { name: "found-me-lzma.zip", expected_encoding: Some(Encoding::Utf8), - files: vec![CaseFile { + files: Files::ExhaustiveList(vec![CaseFile { name: "found-me.txt", content: FileContent::Bytes("Oh no, you found me\n".repeat(5000).into()), modified: Some(date((2024, 1, 26), (16, 14, 35), 46003100, time_zone(0)).unwrap()), ..Default::default() - }], + }]), ..Default::default() }, #[cfg(feature = "deflate64")] Case { name: "found-me-deflate64.zip", expected_encoding: Some(Encoding::Utf8), - files: vec![CaseFile { + files: Files::ExhaustiveList(vec![CaseFile { name: "found-me.txt", content: FileContent::Bytes("Oh no, you found me\n".repeat(5000).into()), modified: Some(date((2024, 1, 26), (16, 14, 35), 46003100, time_zone(0)).unwrap()), ..Default::default() - }], + }]), ..Default::default() }, // same with bzip2 @@ -186,12 +235,12 @@ pub fn test_cases() -> Vec { Case { name: "found-me-bzip2.zip", expected_encoding: Some(Encoding::Utf8), - files: vec![CaseFile { + files: Files::ExhaustiveList(vec![CaseFile { name: "found-me.txt", content: FileContent::Bytes("Oh no, you found me\n".repeat(5000).into()), modified: Some(date((2024, 1, 26), (16, 14, 35), 46003100, time_zone(0)).unwrap()), ..Default::default() - }], + }]), ..Default::default() }, // same with zstd @@ -199,21 +248,21 @@ pub fn test_cases() -> Vec { Case { name: "found-me-zstd.zip", expected_encoding: Some(Encoding::Utf8), - files: vec![CaseFile { + files: Files::ExhaustiveList(vec![CaseFile { name: "found-me.txt", content: FileContent::Bytes("Oh no, you found me\n".repeat(5000).into()), modified: Some(date((2024, 1, 31), (6, 10, 25), 800491400, time_zone(0)).unwrap()), ..Default::default() - }], + }]), ..Default::default() }, ] } -pub fn check_case(test: &Case, archive: Result<&Archive, &Error>) { - let case_bytes = std::fs::read(test.absolute_path()).unwrap(); +pub fn check_case(case: &Case, archive: Result<&Archive, &Error>) { + let case_bytes = case.bytes(); - if let Some(expected) = &test.error { + if let Some(expected) = &case.error { let actual = match archive { Err(e) => e, Ok(_) => panic!("should have failed"), @@ -223,24 +272,29 @@ pub fn check_case(test: &Case, archive: Result<&Archive, &Error>) { assert_eq!(expected, actual); return; } - let archive = archive.unwrap_or_else(|_| panic!("{} should have succeeded", test.name)); + let archive = archive.unwrap_or_else(|e| { + panic!( + "{} should have succeeded, but instead: {e:?} ({e})", + case.name + ) + }); assert_eq!(case_bytes.len() as u64, archive.size()); - if let Some(expected) = test.comment { + if let Some(expected) = case.comment { assert_eq!(expected, archive.comment()) } - if let Some(exp_encoding) = test.expected_encoding { + if let Some(exp_encoding) = case.expected_encoding { assert_eq!(archive.encoding(), exp_encoding); } assert_eq!( - test.files.len(), + case.files.len(), archive.entries().count(), "{} should have {} entries files", - test.name, - test.files.len() + case.name, + case.files.len() ); // then each implementation should check individual files diff --git a/rc-zip/src/fsm/archive.rs b/rc-zip/src/fsm/archive.rs index 656af7d..68c8b1f 100644 --- a/rc-zip/src/fsm/archive.rs +++ b/rc-zip/src/fsm/archive.rs @@ -267,8 +267,7 @@ impl ArchiveFsm { len = input.len(), "ReadCentralDirectory | parsed directory header" ); - valid_consumed = - input.as_bytes().offset_from(&self.buffer.data()) as usize; + valid_consumed = input.as_bytes().offset_from(&self.buffer.data()); directory_headers.push(dh.into_owned()); } Err(ErrMode::Incomplete(_needed)) => { diff --git a/rc-zip/src/parse/archive.rs b/rc-zip/src/parse/archive.rs index fb69647..009e43e 100644 --- a/rc-zip/src/parse/archive.rs +++ b/rc-zip/src/parse/archive.rs @@ -11,7 +11,7 @@ use crate::{ use super::{zero_datetime, ExtraField, NtfsAttr}; /// An Archive contains general information about a zip files, along with a list -/// of [entries][StoredEntry]. +/// of [entries][Entry]. /// /// It is obtained through a state machine like /// [ArchiveFsm](crate::fsm::ArchiveFsm), although end-users tend to use @@ -59,10 +59,6 @@ impl Archive { } /// Describes a zip archive entry (a file, a directory, a symlink) -/// -/// `Entry` contains normalized metadata fields, that can be set when -/// writing a zip archive. Additional metadata, along with the information -/// required to extract an entry, are available in [StoredEntry][] instead. #[derive(Clone)] pub struct Entry { /// Name of the file diff --git a/rc-zip/tests/integration_tests.rs b/rc-zip/tests/integration_tests.rs index 8078b1c..2e61a95 100644 --- a/rc-zip/tests/integration_tests.rs +++ b/rc-zip/tests/integration_tests.rs @@ -9,17 +9,18 @@ use rc_zip::{ fn state_machine() { let cases = corpus::test_cases(); let case = cases.iter().find(|x| x.name == "zip64.zip").unwrap(); - let bs = std::fs::read(case.absolute_path()).unwrap(); - let mut fsm = ArchiveFsm::new(bs.len() as u64); + let bytes = case.bytes(); + + let mut fsm = ArchiveFsm::new(bytes.len() as u64); let archive = 'read_zip: loop { if let Some(offset) = fsm.wants_read() { let increment = 128usize; let offset = offset as usize; - let slice = if offset + increment > bs.len() { - &bs[offset..] + let slice = if offset + increment > bytes.len() { + &bytes[offset..] } else { - &bs[offset..offset + increment] + &bytes[offset..offset + increment] }; let len = cmp::min(slice.len(), fsm.space().len()); diff --git a/testdata/wine-zeroed.zip.bz2 b/testdata/wine-zeroed.zip.bz2 new file mode 100644 index 0000000000000000000000000000000000000000..1515dfbb7e6b7d1301a25f97ab551173e0485fb6 GIT binary patch literal 140790 zcmafZ18`-}_U?%$nAo;$+nCs%U}8*c+fF97jT2)g6Wg{iCpKT^cklhb`>I~O_toxR zz1R8{cJ;2>efqShjs*{kh&H9FHnsU`8APP@`~Mf);ex_5sQF9SY5?Nhoh-l(t!#xl zcnIfD`6hJgd^KeNIx5)$wODCQvPz=8Ki~ssL6LbX$zUR0KmXn-$^wvYFOY=cEbZmw z+4~Hf@&Ewl=QHM=ieeNfH85fsS}cj$2B#7!nh7gOEJe zS6vo2Hoxv7o_9@^qD#4K`kc8=0I52E&z{%I(f|O+fGI%y-zWMDsN*^S7-~^+1-L&z z9WKe=iz_riSgGVZOJM!%+-IY8wClW1cd?Vpi>b>|{m-?}*k}i1&be!+=pW`^oaK*_x;0NUTf^#lM101qIBSgmi;BUxTFw_-5yFz2ewAaM6Y;8{1I zfF_+dOQblr+F{t3xH#VO#A6Zmk`b%?aqqF-*|pGRwsCMYzy8x8b9xI}F3tMf&7N`o zZev>D?A$)C4Ybu9(^229mfde!tsG!q#izRJ!ZIa5-N66?mECa2=B(zsc3y6nXhxg5 zo*CDC>h0tOrxQM>EAT&wL@&y-mx^=0?X*V|u=grBO?47Qy)LW*xdoWgcJj4wt;c=# z;_PT$7_V4YtLY9CVEUXjsL8-$SyfG?rQiLyJMWWzH!6_D1XdaUAjmHm$mz4L!GlMqAI;?9ME*1c*(CK62&feRLn~zhMCYkWWj86C@4!c3d1*1YC=s>Fl_cB^jS$z{$g5VDpG6{lEBAQU+1_;~KMr z+k$^n98N-`giV6{8cMVEMdl zA~IzlD-30cif@H)*-cJqk#CuAq`fl2N5O=7xBvkAzian7d80!5VfAoFi zxO-ytz29(l$~=F4xW5whNJp=1szck{{5;N9ZvA6|&bkTRCZ80oBl9yI1979a%mwZA zSfnPA}y!%-NEQ@pz-O3C5#%>BuWmRFx`Q)Y|DZ4%BU!MVRzl zn4*Bk9th5?3|>mHeA;T?+I~0QBxf*KccpT+3MGZs6N5qQZSj~PDuYFs!3cPzuiARt z2so`)@fAgN+7lE-J14nm^3(QkUDENRnVzu0%Mx4+=apluCwKU7!N5Y|1;eo+`A3Tt zSqYFOQ7%n5JvfJO6kz~v%Tj4%DTC%#TmyRK$M5zJT&ueipWuROV3fEaz++p%U0P-b zq6R1I$IL<7$)f|Ec%2SE4o`hbZJ*nHI(^}$1_PT{`Mf=_J$4Q8CJOfR5|EOjT^H#2?%V`$_!fhd8~eyR_&Of4?;eLs>EvI?2_kW}=aO^q*>qy;eO5%& z^<|<=vI$Bz_06d;FwvwNqn9(Z+jD8#ub_F%aP%nX?*;M8n<n$NH_rYI-N_ZI!_A%Q zw+pXmg+{IP^z>XIMUnICmW-PF<61=z)cQZEuSF)O=LZF(0#+6OFSL8L??AMe@pv3B>pcUn;4h6v!^T)$()0CET^vl26-gVItNgPd0D~JN5_G4=(Nz zw}6W4fUJfN{P1=1ovDolV+5OQAA9&4PuJ=oXH~hLzNXPHmV{(s7;>%_w~g)?+aZ5m zYO{XqA$5(!I66IS@@-gXPBw9p*s^7A+_xYFBe8a5bYXWl8ZkzU758T%_SLAz;NpKN z*7xtWl+I@CA6Rb(xr2IFbuFxQRxMWTT%R2JdsY1}tpu2WrVIAS_0dQi728ljNY+A(XYENEiXyg~HC_I^&tx z$aO+D)>`h~wOjx2`*-hynPW}%C)8{a_!~2wmH7*J$`!n7nNfC$3XQ$tL$w&>g3FDB zFV~I*zv1{eCX}sZS)#1731ewqww-TqSWnI?pEt!F9%r#BtQ0ad;Ae%spiFHz6t&A= zamxh5c8fY#E{>~>CgQCUmb7cMZzXTbR_0c86_fC+C(0D%cvj0saE58bV-+brUII36 z*h=jhc$0a8TtRuRM6^`WJCU(amJG#+35`jPwfx~-1F)E1m&IK!paxKnEq*G>+!!I(T6~MTzOy?=-|NoCV}!OO zsoaRO=)Cb=yX{DcTGpnew4Uxe-5hQ{?+~te=J_a}nRf)89hM7h;Suh6Nadj4i_H-p zAes?3baUmDnM&RQ9y44$Y zg&*4auHkdvc{Y{z#qb4@isGisbFAsJKtzO4G+x*qUFKt$CM@^&qzs-O;~w^0O|$VF z^TxeCk7Tcs-)N?)2d|3eXM)k1SE#!*U)_W!`2p;#S%{K@CLq7oBiVO#+Ev!q0D`uIrjK{ z=tUt}(=amh(Xjw-SkqwOizS1c_M8p;PpPYIFI`mac#&bnoi9bBJ`a(kB))6{c$*?c zL{7kpCtw$j-pY@@G#M@pVpXlqc}yPSF!|vL)dX_-J8&o$+jLNR5zwUvZ%M*b0%+Bo z#J*P;bZ81=uiiRbupHdEp1EPC{e2mD)DJtsRy`>emDNJqR7-Q;H~_~=CyaeLuFWDYd+I@hBzIM`ZZ3JXUV>l|wG=w^9oXbx%h@$Homg&yWUU;# z;5bkUW!IFo>^H(vnTesA*3#KS6bs5Q)!z z-Mi|7&z#Wf_w!_sD_41?*3W@%k43+nx5?9EKX&54vJMY4kPwEZWay-^DJBrnWPm82 zOktLWq;5RYlmxmZ(|KN2MlNOSn3|dfAu;sC$Xs$Vp{EC5v_M| zUeaNI2K#rq`mFx1aS74N>Fe#x9$Pl{BE&$B?=|e1*mY(m*o&Ks80zZfA73+rP7~|_ z=6Bp&fv$?2Z<>P!@4sK@?n?}OTxM-9srCBVlzoVVBFtuFrmR|M?$I)y|ab&oIzgNs+hkK`n=w176Q&X6B9Lt0+q-+ zB9%XEI&|Z-P&j_769&D4oDj>=y`sx@UEITWcBz(Ak?RXIq2~;;p?^f6k;6-E&HGw~ zATqJUU=5AAkhmz(>$QpvnyP6Ap{VP3x{FX0ySb(kHHi&wzuK2COxYpQVMM`QiJMVtiAaq@w^5ayHXbAQGyHLm4XR*DX(K+6RJ(*l4;Cg9&yTHRIc|H zfYm1L<3Lq3+X9{oX1unXxktO-2dS)+-}S{2%1)2%Zv(vDDs%z|W=}Z;77JI+Yt5jj z=V_*7NbSrfvo;D&WXY9_m^p$c$G%STUvWRU6bLW{ta#m(>C+G*;7f++(B)5?$yH}8 z1hHA)NY1QK@!r%Qh|=@oNU@cZ-Y!p3tcR5I>|?5G%TjF02(>>^HSYT0j1zfZ1|C;x zk6Sw?BjMEtPkeWVJy)2Lcb6V+upu^jr9e^^=pavD%abYC%xW?%rlQ`a`{h!^oK;n> zGpLhgt%FZeL=e~{LbHxNnn9zj0j;AIih9LWV|Jh1Tyedjr7~SA$}Mua0MBK0XOV$z zj4YW1STRceSaGfZ#i!*tGK2$bOj5!Rb;}JA>r;=NtAS7?jdcJQb(K9KY z>&W+0sY9Y9C`fLb0nynd-wV}!a+jw!eW3=UtvwR>tQKm=efo{M#Iz{I0*g$A(+Hysf8(4} zFs{()#_rWlap6?~KN~L_f0dWZ(a8}J3hZlS9n4REVq%z6JNKO3C2#3G*_%Fj?rCjg z)qqNAQL6w!WfR7Ml!LE%Dzy?{9DbU8^Ti2(E;L=;b3%{|1)k%N=+n&*XntNvWmAcr z)F|pOTib?V>v7?nn1qBIl;xRen6%X#!Bf7_SS5_idYp;dP6ZBp<2$yw-mv0{JZq&$ zaBXJFKhB}SsE~nP@8ob{xG&{Mut!Kh%zw-#DVJ+=dvT#sM=!>*l{o0gi&hU4!tAN6 z@VYY;yPVgRxxQBaUUjW0IFJ>6IGCqQc@obS{KE!bN_brctVZj2oMrK8;O*A?s)l8KDOpDg$ zx7rNUr)vMU>T%9OqgEw97=mAJev5kNenr;2yuAvnh#)9il)Cd(GHk=JJu_9e0S|Zl z6tq3U$Gwgg- zB1?Hf!ULeoOsjI^fLd~_vaS;q89oR{h5~zdeBZ)h8vpS7u8Orf`aITj3n_dO^tjdrrMlnX1a-7-fnSKS}o+zv{60@9obFJApzjy0<MIDmXAWy3v-`K^K)s}E;V~bHN z?QOo~4z~mCRda2&#FagHIPxrdrO^2vwzxgcj>;ueuewQiBjqv;5*cPaYzpT7c%`bI z+|H%VKijknZoly3yERz+Npj}#m|J^tpJUJwEpE622_w9JRJ%30cLQ)IIs^KM>+7Gth9sCf{IR^C%dRK)EejU-(+z90=1m}ui9LZW5>ID8I1c=c z;>DSm5M0Do+RJing$`*5q`nfSMPdOZ&oi(0p^j#OhlSm)Rl@O%JOiSH&P-i=g6DKb2{<# z)R`a?Gu5iHZjA*4!EC`5^ZSGL_QZT4x8FWn{^o)~Zo@UtB4k19fNMGKLfMU9Ee#l?%{qZD}#- zq2bVRr+Yr1cGC#OV#0>35LO?p-3RT&|hj)zkzA&WOHHrc$*@ zidc|Af{Xh0n+_LibCV2ner+k~L@dRsfObBER??Ta1{PHeXrkimhL(B@Anzry(*uj> zEU@^@T>Mz1oVpdxxL9D-!u`*(FLf6liVVbgAW`GA zV=(YspP1K*iEwvz|VQmM2ZnNd=ehYE#hX27+tt1SPXUe zPtigJzVm41zTc6f-AfnG)(FjN!@5jegC};iIgF)~ImR((49hz#$Fb|(F6YPHgSNjm zixeo4e|+kNH%oC!t&7datFTi=#$w>H<24)=TxDZ2X+Dbs&JDItj3po{-7Toy?m{tb zF%r7Z4#=F>>^XHf5Hb2fVyzlNGKoUH6}@a88Agt2J4u~{%-OqDzMSEUzXe?&QxMJb zAc8~M9JRxe_n3J}6aZ^TC9O$FW#;&?SoH#(v$|m09gwA;u8C${UqKIxs;r4}HIVjf z9%IqJSn!{N`AypA52pegZ>@Ku%w-xF?^9v733sQD=)nm~s~{Mf?d2ip^8uG-5s!ghM1Y{Q|4s@a8-cq$kM}S+f$J?RR%~ zLl!}hQ8Z!YhZeUoYDn%-Tl3#*#f+fqim1?)m63459bPo^jEnwUQxp|^GZODP ze~Kenf18{F4Qz&8kaSN4g(F+|h`_;QV}pezJXBT$iLS_BDA|diH}AZ(-P~TMaqjv@ z?#Avtf>wE`8i-&a6N!ryHVvy_q*D?|HWJBczV9XCCwiKeVz(vQL2PM-F~b#Iv7uAy z*rWJw_ZNdBKry0^7(yIBdJqW)&3hGn4wAuKX?gB+589zMJ>9jr-n0H4N7Cnkn#CUo zPmdcu6irLpi!&f_^CLGzobTx^@pg7r5dk-X-|dVqL#*Fwl!;0EblBJGp5t`o8X?aw zEG+0{@qj!>P>?U}dlT`TBCdm;%C@BN7|QuPG^sA9r6adxlKhcT_VNN8rHoZHE325! zvTRyNFERs5zHQ>Y6>h2smruxJ5IzKfq06#Q`O|9VDdCEDn^m*h`R1amr@HB*dhOl> z&-ox!8~l(v3g#<+*rfMTphszsTf37~0BwN_K_~Ble%glEkXNmSr=g1O8y|I^sv>cPj3;k%#T z)OeHgfC+dsW(jxXeb82ju#axq&7;O2KZ6r5K`V`)dnJ4K6eEhWk zY2!tF5FSPt``BDDA+6`Qedh1cSM%+2Kder~&gU$3?hB$88=)jmjf$+^Xp6h5f}?KT z!6il8>SWxjd>XABj*)PbTh4YI(ZS}F%D`o`HvV`#^@3?h9=1eSnFdpvd_r^#v0UUC zXio{|hw-&4VAFU?jiozIF7Gi6Lz^hZ_rvy_Yv=r$VpH~o3f^T~ASCMN>gNH&IaeG< zJ?7(nw~!(kLF<)~mPSn-_L;U`Mn_Oc!-7+7@=;~ip9!z{lBhAO17;>22FPMW;9a`* z3>3$-c6z!0u>Y`x7b*+=?*)VbWFEMuu!qfa>`kYiy5_cZCrLZyn;D4+`c$B*Q{?$8 zFDURF|NdbI?_(-kC{g~e_Oi_dZF@;d@$^%B7{6>oe#U6Ehy+FMdZi}G{XRi}@&to}PDBl@$$ezVPRNOD!ES|p6h-bX1WWUf3ZUn2py!8t z2!rFQ;@`;f1nUxR^T|%Z4RuhT6Uff?f=-Kw`h&YqAkOjxTcOtj4P7A~4Fc|K0+_l0 z&j7*4DWSL{BBLoEZ~vzYAAh7ha-!iOpD`#H}~WGZdzr=KXq6EXBQEYWn(MV_C` zRN75G*@eGQ3D_$)(h0y98+!ZKgFn%n|63W+y#HGr^lhF{4cMzYQlJ0Z&xF@-q<)A? zVj)b3XJVpd|FGhNC zNMnF^YOpsgq%r@ODY=)QL{oy^g(@UQ@|Gs- z8xDQ+(>GDn@Yw%dMdYn2=+zSX=?du%;MV|nrAC@b6k37x{Q>s2Om?%)bPsNL7TmJ| z>(`C?Dvb01Ai5Y5+JU$g05_~4_ZuXB)q=PUV0r}jjX=MwgBxD^zgi$Y0n%QHh4dia z3ZzbliEb0$>nNT9X@A7-^datiAnvlEL8e4^LqfL1udPC7V9yam_x|rckzPEB9-S$k z6Q$l9k=CV!ZsL9afIZg{Jq-!DL-yQ3pPLdr56QjzzaQo^y#*S+CJK2=zh8;G{|P&P zf_`6z)wNc)zQ?UpmcSrG!GKa+eD({w=8{l`M0zDI3wQwpR=x<;KW*OK3DtMp`5Amm z`}56jb`<{ITHczCP1kopk`tpczErZnX|HriTta`N=ZyNoZY2I>QBL1(t%Jz{5CuYodGAvbt*Ivi(_v!tuo4OU@quF^VG16^b zp7v^J}5}M}?U&*96S>y~~++$2kQ$rS7^-oPQ=d8xi z^eH*2$SRhzqS%P2YvWR0s|5{5clV9$-GvVhuCl4F7EsbS`aw|&3aR>7vB zcR6GaGMP?HDOcNBvZ$8YbHA4kXa=K{np~@`Gab#xoL3sG;LpX8T@c7|=u+b>$?ulV zy{(xD6VS>pn}-=^qj|7m9xpNSgN9EK(n@z;@#CVB7dE+d?)NQ39(KSoEXpk&Nv(>7 z(Q$0fwWF^0_%?G~&*}cSL@tB0JoRe269iJ8wYpk4t%z*?%w+cCw*4CFKbnxuF^56d z4Q}<(#$3+2ToxI65mOeHMXRJP zA8CT=SeIE*srcCvy_%EClM;(dhds;EQaA8xG!gaqSz9OGp1pw9@Im-M3aF}NyMzn| zDjp-$A^>BmmQ?$tgV%ZjCRRNDq*Dss&MvB!IX6^9#8nbr$jY1`60AygwtEqMq@!O; z&jr?ldS}b>2R&F}KBJc2PraAcq&RytpOg?U>hA{@$Aw0{ui?w;Q z`x>EwOY5LFqH!C`GymO*y$xBfqq)<(?T(i=(03YrLWY(*m^ezknDDfVj5ePEQ!d2p z=K=YQLd9o|AquqUZ{ojSW+wMZ@FdyzETZ}Mo-l`HMi!M?^CtyL$c4ByJrz1!BO_uO zBef6hI2P7y55Mw+EF(r8XB)ZoTM4D}DnAbM?<#+qu*-n4$Uqq$%eOn|=2~!!Dk_wP z!mEbexO;1=yM8FG1#a#f)?r#dQ!rkkpzdXofxfGtKF5LMrV^pW{V%OuTF{iPIO<|621A_$DCPVfD3e}Lto9#hK^Mw@DUuxo3sbD(DR$7TW}5qbbEiC6-9`fSrQ6cQmR zV8g{0x0on~1x9i}cU+`V&qen{fttl0s7_MKpuLLH_S=IjUFwj71WkHQlkQ}+kP4<~ z_^d!6J}FpRh9h3DpjP2TYOG4WhB~$3T(Wt%K#KohxGK41c~DdE6Y7+2O;K|Sq|MaO zR1R=z4=ZGcibRv!Cp_DwX-o0}&tYP*E4F9;9q;is8({1+8wwja@N)&KrD^=U!6Yy0QfL|0PKGZ{(yfN(*IUN1`wk{`u~fD3V`(g8|xz? zDgcc5gXjDNpGx)Ihe_YaFB*ds6@gM_;0UrP({-FLR!(ZOtQa%#>P4##DR|Dz) zSMfI@WWZngU;BRn(jNe!QHeY`0t*|!9v=m!f=Zn@1ZxqlB4e5;T}Le($f6EI8<_wk zrG?9FtpGP?I(T1t4}7b9^(3VdMU|ls6hAWBx^U+wM1l@_7`&W>4*K|uEzHW`+K#81d}2r&_sfyABT&Hvzs{zmii@_O{Tdmnd1bv}5W z+wJ@KQvXuE^=2THD&&9R$MiSyV^H6}QvCW)u3uZ1A30H8E&j=k_Kfmj$U#E;ztH~r z|Cif8{ELLGcleKpWPc5D&=EU->kt3eWBgkL_4yV4&G4^;{}%snh(IR)iuaEE;bl)OW-;q(!gEAJ2wK_lnH4Ffhlk<47irX=6{B zZ+@%Zi@&M35UYI2}!zf|_?X{&QD$Xfu1OxV#?@SP^KmCAo8hllrB zhcPj%y>W48NhObe^P2_*?pXH3zM8SGw@#gOZM(s{zwT_Wkym$r{_uKnTz2niE;Hj|^$Ts_06nSH zP0O$56FVrP06E?#F2JL>aDej^PEDzjD` zj)d`~RBcFS*Pm?x93YL^70{Tc%V2qTGB+>^BT5?uatO0UC5?ToDk{o)1&V{hh#^%1 z{}3Z}Cfqt3y;?%eUz$=~h6L84fd&SV+n4Ie=^fMKAo1I6Cr)1#ofiFWX8B2c0V!$w zY+UFHmO56r8v1^P1DTDQ@dj7WyW;h$X4l(dt0=Oh1T2cFdH@uwZPl`_O0lik+bGQ4&LHHD{(SM@G5XLgb3PZPCJ5bD@p3cDZf2m0BXbXgob>UwmC+ zJ}GSiK&$Mh`d3mB4C6p85@QVV5Li=b6^UR>5mOOyZB%*+QMKd*?dDP06}6R729I5j3PJsI&!RYa7AtLk<%H$2C z;mIM1(&e9FRODdFMNDanGtI2cG8)F~F|;`=IGd-NSDI3kKMioMVlK$(8UxEYD#ogH z!lmnt$<=5{wW$-;v_yYB<-u}mLjX}q zS*p4lL(5mWx3IEzq@+A-@He;OXTSVbZ^av-%$s&bIH5PsY{4jatfeQvuBS09D9u}; z-9MWg&O|E@em!r8<(I_Ll`!JvrWE1zu*x*Vb@YKKDB<~F#Ge6?G#HVek-t*+OH;FG z!1Wum5=+DT4^_bg5Hk+;k3sqp9lO3{&-u9#9lQrU0GY^*SO8MD0?%hczB|xI&+qH+ zpaSb~;+||!uI*Q>Td#M`TOG?!UnEc-p_H)py@~t|&R|IVi-eWD{YD<_L*^YsuucV2 zHxw_vG7>r+_H-ovAfdiQ=9EuefKe&5|};^$66 z5iy*5?6JGW0uW_xp8L9KYSr~C18#+A9t_>SL%rJTzB293eHVs$1qoblGUoME&%RAY z&1RsYa_3cCdvWdfjQ2|+6};3QX*mZRgF$`lqNDBg2<>&}oB;fw7v2<0=Ulz6{Hf<< z;N#I1tsiR#&h(KLH=k?eec16u)D)keCLg^S=G2~;lwW&#rssmf0RcCL`EPMO?Zoe@ zx4x(SSGUXPmjO_k>=FCka}NyJ6w}~_B%oh5jUJi-g!f}!=0)So$dC_;T;p%2;Z!wuJp$Jm zNHmdUf?o4plY_64a7Z-|aYpY`(LbdO;wGYwAZ%BA!J}sA^)ni{E?41zU z+31@vq_7mkpkRwogwR;awV~IYt1P?BXLWBxtVewOaO)K8`EP-g*jOVtsp$tF8}fuj z!a}<}6MiQrkD$%$JDuECKse0RI}->W{;}O6?{(ATthZRSSU8wEY+~}fFfcoupwt=U z7qp*5kRiN`wqOOHVaHLg@;ZS#Dtj*XtnrpxqX~+Cr|nT8`D5m&Qddl5MoOAUt!%he zKtr6wW1WCHcyW5Awi=(qPQHvBpL=X<+CXZtw!#aAJ9;|9~z2;3>Hr8 zwD0bGbljit&W9024(1u)i2gV&!4hpk{kOv&;E!)88t}efo|R{rHZ#@3;^cTqsByj+ zd$))y+~Qej`OQ&y4-DV&Hy_$k`-I~Vab6&MHyrQtC+tgh9zEiWY9*D;kMPuR3@kd8 zOuLzFFo8-q@T*HxT@j}si&ld)2>$x^F=_|W9lat`@o4Sf{4c~qHWA6X5()N_%=@X! z=^R7V`4`%|`|l`kuLEws6ZsE0&~hg0EvuggO>d`lmTx+`mv%WgWezO|@lEh~ChgY> zQ^W?)|M*u&dsHC}ori4nLEv0g*WWPu34J=Y9BknWCv)lv=OD~+Sxl>~aqu}+C3f3J zq-uO16+!1fo?g1Y2&>lMT4(zl;tf*Q zIIRgJ{hPu?ljON7IJ0l&*THd;Id3blnI}O#T0fM;=2{H3#-9Kc7;Ptm4z8>pe&r&t>#9y1Qy9w_>ZE;^(2HssK(QGH^ zhUPoiEGH#xvX$R_$k9@hqL-o%fyyE_3j+B(NkddVroyws*!=?BF)6N6OMKIVot}cL z3xlZa`1zG27|m;GS=b4QqL({^Nd|5*>NAHNBCWw2*L2Sc>zW2a^5>wjhik>&I>kg4 zQl#vGeCd=vZkNyX7;J*xWp}a_l{~x?iNLqS^1M=aM+9!)#Yv5&=d8U zOGfrYM!46dPmQII(_*nFtpUpb&rk~60JN=%PM|mC${|N>nol9&gOZ2O@@*VR!E`5d7DZkj;n4WJ> zyg0k=iySx`#WU7BSH?*uN2h|B-!@U3qaL|-E^~aliPVF<>Ot0U2aIavdjVZ@8EKFx zUk{jYyR4M=&X!7)tGmc1buPXiV)vUhpCakf2;40!d!NR%qSX2|w+9GF8dbw0DmX_g zmx|6@ASbkSNB3pO*%03qEBgkf3y>|`_z^zd%v}+kmZwH-5>?sU-i(hKWA=A*R$d6$ z+YhcFlKRDRXf37nGqC<*h~>nR)P|6G60$17@6rv>(5Og?jaLbdXOhP-7nzLXZ~vv3 zS>BXSNk*qOQPH$LRDfTG>?}j}Ny=nq!m5~uTw@%3?{2DAEY!i*C^D~}H<84d41{px zPD@52ph`Iqz``M(ODSH{pPHUAQJbxSmg*&mZ)f5-p_P0DEJj~xbY}^DiLGPj;#Yy2 zP!7P*F35Q?1HnZSk7Onr>Ffe0zlp$uTaVa}9kRh8=?7+PiBpt1d)i{2c{8YF>kvWLePsWi?pk={(yT)t(AL9%>H53YPT31ii{ zyc=>@=qT`ih3C2oPh|g@(%ZrCKDGGsSXkh_#=sV>XD&i?w_e@DVPP{{XQ+C zc8n7g2y0eMMRqBN(W-c!Luz2JF6L!qb3{mp=e2~2R&OO>ZZmFRgpL!8+7b~)o$5yS zCJfrzgYKfebev3%S#bOc`yb%y;2Cc(V;jri%?!0##2?=L40;1SN&r8v6tI zog`nI*&IHJX!m}xs~ZarXHut|tv*QK)nf5}=_YEP7X1uTdG%49esaFlmTVnoylk<4 zNMFUP1|~3#d0et0Wpnjq4ywfIJz2Z$&UmhV?pG=@FKq5cKU}`Ez8&C7NVyJe>dwAy zOr1KROVV(q;v3jdSqRw^f+hq;JHK|d+~ahwD_Yn*>*d(n$S18PX?Pb9s|0HofI=cL z8jF~JR^k95=cirv^2J>uOz#u>XyDLM<9hpMSZb2Z*4o>C<)_J9R*Lmr(_+aSCF*ke zj%HG4LK3S){t3pAY)M?V(Dp7zHi+J|tmhh_Jp1xxv*bln2P)lqsK~laWV9G-H7mg- zoiwkqSe4zZ2wR1beg1K6NeNOlIZa8#)T80-l=$bOzD)=gj^1r-{MB*P&P@|_ z%)?&4OMIhUp}*U{FH|kSW1$ZI7&)CD<MXT))qf_A2}`q1MrERoi$HRDG4Y;6(hDYjck4Ltn5mBJyi^;ya_6WJ^A? zVCOlK$tpP7R`e&kut=C#ZVzzi%oj!y;+q#Ay~?R?VyUie!bAJncCZMe6+20M0fL!w zQj(9`c3Y3T*_r;h-RV;2GDB`dO`SdFj^IT2qLmvjibw)Jq~}j+B?D$jW7ySnj6HN0 zyiZ%UlVq%DUvfl)=|&!z zb2!zQE?#r$F;9!zrz8exEaPSn-rq8jw$EF~aS*xJqsS+rPM0Dd1b?{K>{qa9sF2+( zWKG2HZ47OXAa2reUvv@?Ojq>9<4{LDfI5?OxEs$Pszi~MS;8UYCiTFqJq%gFVP+utZod+-^LFcZz4 zI8}LT>O#?Vt7MvmYdVz$y-u${9-5t=7j%JztWr)ta&891xz8(x5 zm0(CS2R+U`QX3z@gd9k^jTi^Hgqn`1Tg|g|64Lo{bypVgJP#<`M~mU_FU7vT5=o82 z_g=W*v=)W>4-1$dgboKfi6k?d7>|k$hip7STF_`+B(~*d+5VxdYn57Qz6Xi&?C!8j z8Hk#v)Z{SG!y5Z}ZvH5weB^q&Q&CB=(CSVRjUruP(z&OsT1?eQe zw$xAEl-aR#dQhOOCo&9G$31KQv&1JWve9z#I7CQFQ<-KFKpAb8PiQs@7z)qIp_M-+ zD^!Eg5VY^AmuD{4{<&9`DacdlV!GjJQeZ1>QgEVuPm`cR3iVl)*=`R7ECIlqqmPkG z+2dZoaz3kfl3q+$EbUpSwNN=O(rjBOgeQ;fRJrxfs(f)8{~k(?LZnOuXzo?dq|4YC zjY;Oda7op>?a-|L?W|H-Wzchjp=zD;bN??fM&a{Bt=KJVVke_gBu;#O>8UIxB|6JXa=8m2hK-i@%4|klD8YdK^heYi-rx5lHj-m{ z_vXv-*h~Tb<^! zcE~f~6r+-Cj9`STQdV+FKl-t*T$r73YjFj_jZMkHVNq`RbHdoGenk>R*$Oxx{Tb)T z>n#Kc?N+_x9-?%;{ZIq*5c)+9kv+`jrL{lUXP^5CWsH843DQ~HWB`5rbNQv?v&!(Yg01pp25_wLIHXa~LX;q> zI%)sPv{h;$yv=k;$0A;Ilj3@es-0=JKq#~ZOF6}6YakEWN&W4*6&wj`It?k%P6}eH zkwl;#pB}q26oFXw&v6IsVjBvWY?t_nNXn|99p0}llew9G1Y5=pOV3lDvnvJ2} zNSCn^_f{te+bP7WH=q*}6=A(<>FA4W>{IK8gIDXu4pB4Kz{#QQ-OuxOAuhbxW*n=Q zLQ{~l%nXyur*E!08O;lo)rbT;tCP6vC%=LvW?ifXP;?s59UY?jRDX{%D7dgQZniG#95@ z_Lx@*-i0xfPlA*+pl(AMNH{3%vhB-pLpC%!6r!Q#Q4nEGOlcc8(v%4zw`?@%P?V8r zJaNyd?Pt?9L0=fxG%HMjs)ZcTuh4I`;Am7Gx3apWCkYV=`gQ1)8DAIlJGWFrv9doh zk}b!Pbm~VJLhzcMkIsIxll66bb0bEddIyS|8rd)VqJ#EhF~)D=1W&X2%T`~``U68H zWeO6-%q;P|moh+F3pIjFC2xGdUrw22{aq!{7+lLlL! zh7C7xS=f&1v$nfXawZHFEezdhMG{-ZJRUZRbfdb;W3_OxZMUiyZ92XPhgr@W5X_==^gle&RMoE}cw~-j(>8M4b%(?5>EDJ9DwFSgqXIOS z$8Ty}Z>ng=NWzrKrX$?Cv+=9DN?oaOMnENq$~SIzlR<4bgQvR@gqa?>1M_@edAuF4 zW7M<@W8SjN{9Mt5ne~j;d?}n;yYBsp=Z-Fq+Ygpj(w(Xsb^9WDIhb!xQxvnitwHJi zwmB)%la+q86Qyk7T(52l4f+0@amp7dN zC&n1E@!~aCng}Y=P z{8cCX;w_nZ)qLiLmt;ZinR&C2zUh|1sorDeS#ul{LFX&kc{ix( zvAXt#`zEWtcYW4+ybJ9xE$Z#F?^~`_7L#C>L)TNy7yQdj`(+F@nO_$4`{l$%IAj&Y zr}8Sc-V0laiKt3j*y|?ME=?ebl+O&b9;WeJ7|6FZ-<6QfY9q}Kc~k^4JWTgZzj~?g zE%3H$(B1RjGJvSYrSC-Hy~*YgFRJQR`QYUf;(oBjzSErRQ~0# zd}E`3!P?XME9I@RZJ&rr?9Ad=M%#fM2xq!k(+;LcY0Z9uS^Ubgx(n~%Wp3HUP_V64 z;;H_^YpUtXQL~HQ{8C)0dq+shM%$l}kgP8{6=$IdTt6Fi#PmA^^PNgHLBjc;IY-7J zU`N(#ykvzRxXWs2A6@t@sW+%!yVIt($~(j+b*>3nwjY-=j+uybPURS?T4g-&xFTWc z$xq3fDyMrXs>WH3l&0tAagr)oe_#SV-`s4+ZH-s@tG^_v%G=u3#*9j|)nxRStrTLS znbM*fsj<@K4_MN{ea7_S(4|#Dq+TqVcFhtG7F+7J+M7Uq`X-+rU1!YaN?$}_teEs9pqpYs|0|bEUg}diE z6%EjX+TC79MwYvKy06&a4Z1d$IpV@T4{EG+m(Jt}A+Qdy1f4QEcRN3g1ktZfd)Pnu z|Aecc-Jps252efgate&mPNAW?dG^ie;auO zJLT9dqWf*;1MqUL{m(rBj+MRH}^DLgagn!n5jKJ&jz%Hr}lk0H=)m zQWLETPj@>*2GFnr+6u8S1-oZri588ME@xBm?ZwCbSd%s-0<@eah-(=&r7+ zdlnuwjO8{lKM#+Z>rcTgtP4^Uv4|MY(`XLDC()@8{TDOYftqA}z}i}(#g^w*Lw9z( zcIstKPLv$s{_@(K!SbIe`>~nb?$YsIVD<}XnBaR_j&sIuRANS+J4Cyf`VsrB#o^jR zy!A3I0YFJ5pJXKWJu{E%ewA^)M>tv0ehRs1@WmvJYE8|vkE=JOGNYB%iqaJan$1WX-C?B7OZ z#uloDGpH43W;=?!oUWF-DP?-pj?&g;=QVwEW z5E6ECTP&%Um5N|aY0-#YV`@Xd zH(&zoF?Z|bYKLbnyT8m!g>GdO$-r=2q=nLUnUESi_vwAL!yVl=)LRvjnSAItJpq#7 zQ}kLt(;yBbZG0RFzp`qMf>cYtsr zier8XEI|PPN4$TL1Hd$K;?0VWgNDS`|CBUAi_zavd}k6cp|D+-WTbb?D4Nu$+^xEQv_Q!g|E+Y$kcou~kAp|Dte| zShZbaSKR-5)n9-rck#_t21{BR?tKeZr&l@AK#}A;VPY)j2#AcFwAC9os zN`FgPeaSYD7jFH5xbt27yqw^&A&!GH22H{pF(^rTG}PJ8fy` zRK6?v1dEK72%L@1kEGlh9xC?-uMU|t&udUA^<{g9k*R#b1IWwK9B4-D0(Izt>MR0h zPNc>_%`Do5+E&M(!w&Akmh`aDa#OR+ap8WdLl=lfGHtd60p@7rSj(?u=>(<=9_JOg zMdTVB4!4qeuR|A}#MseG_C1S)M2c@__Qs>Y^hp4w7mGUUl8~qs)O%=sf9HNLHnI-m zKx!8r0uL;fOM`ZGk+-!p^m)fkRTlLNzMMFoSm07_3V1k?stiezJY2nN-We3U?{BX^ z{}#ggcz0x11{by7G~DOr(jg*>Z#7t1oO5c0&KQ+@0_V`4GojGwVL8@qNQlgRTLO{$WxYfu!on6&-*%4tqae}>)2 zOvO4Qklt207Pp7v(2YxnzLUgoU6K#;X!@I}s5&MOPa8wkbr2r^44nA51W6STE{auY zirr%3u6w6Uo|&G; zur&xsMO%BWk=`gBIEol=UT;FJ+-)s!v9~@9eEw1i?0YiukU3ch&cbf*^`=|*qiCCc zQG`FLE|*d=Z~f2OV;@VWjZ6&rn{K@_`=-@Ttws5mYxlQlUL0?aM?F+2)S}l~8Lfvm zyq23ABL;uhc1v(}-d~@=>2=ap=NVDlEXIspQ96}|kX3Rb?@k5^D9)kJ>v4NcIApFU z1?^Wy3dFl7Q;Q#pn)9oV>X^PkI+!_)j$H`w;P^JgU+%$t{C@lplFP6lMiVjX4)nSY=(;4Er>ig%nH47h3)Ful)pTDN-z5HoV&;EHw z>jPJG&XP(aaae0OGnE*ZW9RK-&bF+J0K@vF0`b*}+gc58=oSCSEhI)$41IZ}H7qel=0D(8 zsQx{6lG!Kt=u{B#!_oA9@}?SoXEORlP4Ic4CoU}3m#rW^Dy_ehnYvY?+?kLjn7S^G zQ_Rp?D2rbUwl^{Q7bRnt?+lbu8EVg3H>J3()9k_R@1WHFnz)|)@q4PB*6I3G*vhs& z;xRJZSMT?alMoqUcuGHC;!siHcoU@Xd|`Sd9VF(`osiNZU(e@)9q+JA67L|M^*pA$ z)!N8mKvK^Af+LRV>CwTOeJ}ns|rFic~CckgzMeNr=_s@#z>n}pnzSw|S7^aiN_jla$V^UR};fc(SbpG?GT3%bccsg1@>Wqv3@ z*&a9l@4cuZBzAcCb1d*&+1=^1(GhXqF@A0p8b0v3A zSNY^U1eKLN34CRf=A(QwH3F2Kd&lY>-5sVlq0;di9nOeo!WO5RvYW1dG1hcihP!@@ z{k@F0Fx&y-e)y7tLZ2ZbY1_r>df$nRKEZe8bquh3e5~pXblCxhs2!(j6AQmB1tB*@ zWvmt??V)DO9#pvjlDJK&8K5>;(;gNZV9Io*>c9~`@+8+cH#Y*wGdEC0EmW{2j{8Hn zvL8`cd(@Bcz=U@cH{Z_TeR)s_x%`Bt-(F)EFgcBo+jad0`D~>5k;ovWieJyvrsz_n zur<{4QjRpcfoQ~&JZ@e{-#4cc*2%Tuhliyw>ZqZNO~f0-*y$1lBTx`gGckeYT!cil z;Gb9af<*`}7Ee<+99_n>{IoW(4&%_XtV{A<4goIY7G zqI&iLD8XbsM7PciBR^HnCd-*MtJ1cxvQoCP(Awp6_E6bzWae)5+0(2*W`cb?QzZni z$RIDy&o7PW#ck0nuda0LMDLSP2A@q0VKX?R6xt$&O|-;-H55-LYAWIzUh)SXMaJpu z$Z>D-q|uP&Qo)_dOZk(^8=c79>YW(_e|8L?gKP{>( zmeL?rcTrjTZ<<;qjjbu6szkLE5ReVQUMMO#y|pxrFKd}&qAoA+EN&kL*EWl*Gq9cl zwq~(vGTG0wfw<;#EkYBOX72Te-ZW?Quk9e-&{?P~hCpYXz;Ep}dy(nFDBo5|Y%`?E5Dk$>A1| zdPXvgF7@^Bx5DQxH`{X7+`E?hrjDtFD|D3ST1L-PV!2KYak&Kgrm(UVOSl#T2w$h8>de@r8hcAk#8=}Nn5t*Qfo z_?&OgY`vH3&VE@$MTE#;(+FhZud<=bR>o0FAt(ex5Bm3piq)uj7@#*_m~^i^EERGe z^fquF1trOQr65_e4C_^!QWf?%zN8Rp&6ayn%M>QaJ|3i=athRDp7&O#L&t0<6k5*w zE-;KHNn%WwdA>-xnM|#dgrr1pn)fka`?^bp-s+uY1mGuhT*A6N_pE>6jU*L>@% z33DTL+!hKhg-U=nLy$>RWp$`OMj(_L3Hnp;IGhWl&HKG3ot)*e41tWoyZybvd@G5j z<+i*9i%bLBoWqhQWKYKT#z(94LVlmDi7a0mhflNRH_c`{-dgy)a9B`mm^Y|pZKjDW zVM?pDn+2&CMTQCuEO8?yHV5M!{^YOpXqLKq>vuv2@eJW;wpx{pc!)y@!@~m13}bqd zJuhFTqB{JHq{f)!`4{g@r>mE2^fFN$?{GDOIYExl^q*p(XkYwI1YnJHc_TD4s?T@c z`!nY{SKM+>xYu4u|g$02qxBASprKFKqU&14z(L~gK>5UND= zB(6km==^3Nv_qZD&;u}-D1|I9OfSM-#~-suL`c_v*2t{H7XU_?OR0jV5l@1+IH0() zwJhAxvC&`*_kx+2u=ye!_V4}c85q;C6-8Uhb!Edu&zCzD#T2H!Yr>88y8h}2fw$Eq zqZtxOg~J+N5%Z38Bi*Vhm`O02uE@o$-S{QLyd{!!mB?BXoT~Xvywa-kQ`faLB`~*d ziXFl01};kCwA|n^yDL(AT6(+ZN*!J&BUCZDWfOU*0U*0#FVrNP+iCCDeM;lMft=*Z z6L2FC-_$9@5h(K6>8VYyqYzX8*f5jYeE@9q(<4p=F$p+Fdc5@cJDLy1>>7Qz;Jt`Y z*6pUEA43$1-Z`z9M($$4z2VG3?-)rKQt0<^Tz8~&0ELIEAwkgG-SFF|mVnXHTH115 z_y9}4|H4j*0AG6KX~om3;r7guRuxJ-<;!#zIl1@M5OwSeUv&aE2fYb()bomYO9@eD?%!QsZsP={F3ZD>yd&}|0m{zq>)e-sgnNdpB(m&u6k3M!`q+L zaN^WHOhyTR|2h?856H7i*dHEoxse6ZV;PG|hYv+EY}?Zhs9@SxfQIFj!Sx)gtr}oQ zIyYv1Im%qCjH^y=;U+m8-`uPQp@qn|xQcKx6LFcuYOnifv{Q?IxuvloKPv!J{BjCr8gB|p!To3q4v}QsGdO6uUSinOVcGi~+W{TP`Wz(nEU|9uTO{q{1 z4o?TSJMlSp+&gjSuhvvtb}Kt&g^_?mf{INywG>l!y_z@=C8Rr#1P;C5SDQ62Z|6** z&3MFutuluo&A6hlq-w|e)Fc(z10yPJT6e^3BhNE$(@aZ#Lo5m38dl1MFsiqql~g;b zWkii>CSR+~E2LOFmJdD@<^@g?V9o3@KA%D{>7GQ^zZXD1S`q5Q+uj-{sNr+&`0)o?K%KX*RTa zOy1NH6y!c`{Uhw`6qh0^k`Qj5Ml2o@l)CfH&&P~Dv=%ro43}J3r81hDS%@E!c~aM3 zmjc$325!$ByV(Fnrr!Xz@TQyLDBWR-u+&`&B|WwVOwTV*wQ6L&m!+%^l7I;81}FU$ zPa{8tl|{CLl-A7U8t*lHBT9s>7~c~eypukw*j1Laow&5G(*?P zc#&~tktatc&_H2wu}5KZN{>VpSnl`URwrNxW%e&VUhK0h7I)@vk2Xf}Lz6Qlm{aQy zx512`j8eOn*G^QD>4jy(L-v_^2XH1l;N$RMkP=6IP>21B&d1HdT#MOV+}_?l>Jg}w zOpd&>#1C%+MyofihIe}i{K!Dz^>DW75@`1FF^EB_Xk_8?<_!^(GTRzO3HBdLQ=keM zuVETm9t)SBJ$z!Fmd2)+Pi%s2 z7T^U>30DPd#(_B5N?kfgHNfKKLE~XW&L(RzL|j3<#7{Rf>B#jSSTRt*h@jSvyw!D9m~EYX*&~+3df_DOwQpNjMXM2s zkp4F-^lV+{$MHU`g6md7{(0X!1UUJ!QDkfB@a#`PBR51FZy6fP0LN#LcEIh1eUU8cNohd6~ae1EoJ{gDl>TW>UG+ z5lnEyhwv_v2NL-nP#dVOJ3K!gmNZ)z5j^aikPx|iC8@Z{GF)l`|-SuQaZrule0QPC~Q1AP#bn+p2sE39bm`I zGW+4IUw2xdI8x+Rev@J}^$onZ8G@_#`Kew5&7_T_m}eyp2Jj<^x;i3wilEGjKi+H6 z1-Rk79!FwpzSqg{c0rXI#JAYtP;G2Y@+*5B9iwT-k}}Py%ge3+seKJR22Y!piL?5u zk%jCy=sdF(4obNHdMBtF*22`KIE4TFp&y7G%Gti-G|)+rv)K}6q7pe8LYy_p(K~M) z3_}e@Hp|$HX141@V_4NR(g%TutmUTziC05}5AF%U)sD5;E7K&U1pbc;JU!TEvNGD^ z!G%l)JP%R~)V)RP=d?^Ioj3_b_DBW+huUupXHvXm?A!LiZ}mx1Sy=%DuRq2y6^K+? zLW|g5ntgf=Nfbpgg4C{LQct@SBdBa0YEL^f1}`hkHc-;^4GOE{OBA8Cf{{aN;~}%O z_wM+W*egwDi$qB3k#5nikJ+g`wS0E4Ze|>?q8;bd85D`uyuM_NG)kPA)Mmy+Im&}; z8Z3OkQV@d@eaW65 zhn?2lDH`feg8yXu{-8)aJX_nqc;&fjI~A5GsOYPUDD@o)`L#`Va1)=_8SKIZE~Q*b zK@9fbB^*MflLyRyVNCs`w5%bx%r?ROaHMLWxT>f7viAy&ZBUQT132E*y5dt~d=ou#Ct?eR1f+MT<}qd7{M(8?QFAx3`GqxsI(sbAv1L zr}{v3{n4|&VT_3~V(j9)^dsaIY@*FMlGc0Eyh7$pY^$*TxXBH9sOF=2ubr%pJn8Kc zxk8}I=pPQ9`!#^Hz|x$)MD321yJ|Jh{gD8n&rCBT4s2vfzhn9=9!2yLq0r<#7U%mj-E>^m3Q$3zI23%h*Op_fBdQ<_U*w zFTly0TWOY|!(8#7gUczPNVeF+g|Vu%pZVHn#K{qBbI8}umT>93rv~Tw$B|ggTaVjW z>qppE5l_rARzzkrzwMv%Eeo;KaE4Sd8nYS*tE_m51xT37tYtYOod2ii*=)8`^ zK>~ve+NM7R&fr~+voTL#`7e$@1D5D8J(-#wHPZmv9&0(9R6;+|t!E8Qnl{mI2g@F? z=)7#gwK?8$EzaL;a>eHtX7yUW4`5ke-(C$zgY@(MI-l;D@I{;)Xu5qJBZL=ycdX3O zl7K!MD~A^s9u<-&f=o+-ueu`UrJp>V&d;qKA`bnyEU?;LP)Iwxq@pU9l)|U!#mGXECM()>a7Y+z>p$`%! z|Kfmy>Lmgb$z&)7$(xmAhbI$AY>PBfP=^|=i8`1C?DmsFiZKcZd_0l_k%K4dL0cxg za$sKH`VjrFePCB|OZoF|m~k+pQ%%+k!DDzu`4?T&v|wb#aLRkf~h(S;*wL#vmm zo`3|*i(k%>`#HXMn8jeKXBZnyyx(q_kG0$KzN6qUcB=450aVFw$YVXcaUMlgPv|LL@-wq0 zJ!U>$(Q(5EI9MDHZnF=5Mn>>o<5T+wfGJMhV+>OV2KAUEaUAvekKA z%?3zbY|O_x=6b7c3jd&eqe)JYUio#Ex^K2}!O`Ee$%xyfSh;E%G_u{^kAW`3&mf8s zNG6+@ifNnT>y7+kKY*D4f#w9==dl3>GQxCBq-vIZ%A*3nCBg%ub^HEUFt04^ywVARc819Bc*Anuy0-iwmBp8ZTyKgIZ zuLwcEUYqSNp=ZNp>?8+!8?Yaw0dIg+LK_%dGM&N^uH7G8LV-3OU%Tb1If%i)ayCHB zF3GbWoo&jhuKrk^2&vr?qK-h))hY~dR!c)-gbKumQY)pn(5BCVF_CTYRHx9V7d|Dc z=?%X_b6(7ckeFt`$ixRG7DbA9KJHlJ(PBW0prz%Pb7vf0vf$AdVWKD?CW9x4Pex?m zLSX4;-b>KI3Y>tm6ogAo|31N|(s$nrA_(D3J`H2j+S3)y(boO=?1ZnIU3BdF5(nAtnP1unyXkD3d~i)vr;H5YN*S@nrqp2M3ajLOYj}C zcyoT8hIbxsV!~8Wkjf51(+uJ}TT~Kn%OH+bC$dVWN`8MNBlpmvfcB^2f(HA=9~vMW z5HB7`72Ka07&st{Fpj0E61{uLsR?pppPy!|A2Tuk25wp~IF*Hx{l}SPp=%Mt8m81> zw#dqqF+tOmdSO-uY-b4FMPgT9Tw+~LV4(|QWz>T$lYH{GhWNwW5O0_P;o2J#wmJ@6 zFJ>x4B&)h2QpH&W-zrMR*v7Y#VI9t9;27K~ipKrjpGz}amReqq ziJZ1r7ZyDQaW|cV#TGa1v(%EEX@?WJC2ICHsT`=6JegBL)E)a5A*)wZqc(w}p_>oV z{JY!bt{Yz3;*pN?F85utLxmb0cMlJ-zb{QYA|e8LRPvluK1GsH8(laM$;#{s~QA$A2pH`?dEZEz}X$LQhSksVBp=Iqrxs zG{65-I{s1FP-88bss|{kxc1*r3>RNOsxsx>pNg-3kr=Zz7XEO5h9`69f7Jl$E;a+l z_2lHNrJ(3l@1TFaGxgx0aHp=deD#3P0JH|R>XMCmT<(xcuPzT+2xP(Y-IAaSI@go{ zz<~@ADv2@_()12QfgiJSIN{PAfLQEfcVdj<_^8eW65LfYPFQM4P0N=O_Od1K+ipJMYLQ@}+gA3veEJUKv1&5>aC zk>>Z17k+C$k}dj0bn{x%I{seQghZ+|YuEeXcI+FatZfP# zdp6=2Z3;UC>U{Ofac`ou-oRG&?uCELoq9FWaa-+P_}lZ%P?$u?t7+`7hHkw#F>DDt zG8s|_+Qpf>0Cqf-5mJ~a#?jpP6(qdcsCv{a)sV87Sh@I@+wEm4;f@bV#nHperpJl9 z9yGrYhoRM$>6R5_G1!J;A<=?4@~o{m6@3(B^e)=p_-6?Tre}P0Z>(&Ivvv{;`!1vv zO9P!@MWmqJBr0ul)-VHYb8xez9QbwW16njyd`RM_Kn30_#StVbEa+fmaIA>Fg2;NS z#1%zse9f9fwitk6cILY4BB323p=V~Nw4_QDnnp9Cs;F)PS0WN!WaC%=s}nZYB&2|r zq~>fBveqs{5BG3f3gppz+;E5;yhY8GW{nf7Iz$Jyn%e3{!n&?c+`~0JC16oxOj7&g7c+>j51IvATQ|U) znZFG~2EABDS<&i*fGIxCYXfjsN0R$37t~*aGe0NS>#>vU-A?cw$^Qu3F~hYUGKzKm z>SUr=9vnD`Awv}>V(`bT)I^B1&3&9ziRK`C1RGnzg_(&%EsofFtj&f=VTbaUb`%^B zV7%9U05N3FNZwA)MCNB&UHmBczwv)LA3{$8b<;jU{|F41Ufl~awaRaGZrYbyP|)dJOI$L{eZ4L19n;aLC20#8cv#+%tdU zu!%d6Q#b{oDS@Q>vjCjo=i~O`>kcPzSagzX>i;1N@eS8DG z9DTp`my?Akstq9=oq+Nr$mTGbdIHYX++4XK7aLaadipxm(j8AGyDE=aEoo(4AIHgBF$I|A zkpuyWb2m<~ij=zY3#~~J>!c*hA7%hjkxV3>;U$g{Q6-jo6PZhUGA6%B7y1l|#Xp|I zxWksU+CM?ih^hScYEl+BhDY5b77%O>>)nRmOBo8R8QvKJpB)~^hnrmu4JRNrmkf?J zcYyGR`rax`%g-hTIfQ%*M_UU931Mv9yD9?CIO{J(Ke%h7{botP_Wof+10j?rMnJn+5l>D@5pOJyBTQ6;cZ1vX z!J%EzYfI@npHC<5$WH;$2%F1A9$CTJLtcV`u=WPDb+cay6rqPD;FXRSIJ0ER6yO-k zWXtfUtQ8=s9v;l08laoiRvR}Fd2Aj2YH#u633Ei>f+XbcRyH#c9#L$tDX1)I6TYYWpbew6ZXo){=7eseUE^BXF=}s-}=ai{-ri3EBa2yYFU3IJ5K&H;AHxdAun&}hfd23m zOaguRfn3_Msc5KsZGrK*#opiF9gRTGuerg=Ui>nJTATZ>9+i5ZP`;{W@DQ3Q)X@5z&=Sat6A_x<0crqJ`;U!FQ4GDe0FHfrr+Cl!x;qo;(~%nB+#7v1U}W;aV7LYvg%{%Y4- z{MKcUW#q_0r9VQ#wuwRx1EZEWUo0-~QBW&O1I)K+?z896x;4$IV~e3?W@#K>8bh}n zf&xuv|55Rug^t^$i%FTe+X#;b-a15CA$G8Ur%e=}RQBMNid3otiem+Asu+2j`AHH` zlW~vi$%q4)lyJ3=6lqhf^s_Kp|^_8P{IKp~2K+7T0*uuU@F{iN!|~BG;Z=@G zDkS)Kif0lBHl0O)^H!OCHu?|67!tX9jdhr6`^vsJ>v(%h%}!|Cn;ygu>luPd`vEUe4}6lh^h?hz@rFjsQom z2ar|{tpT`9aG7wn`S=3>2svF*lLaf3N8F&v@VR}lG3B~$%;wxg6-22OK0NNYq_JGNQ3xulmCbRH)3lb*;xrO8=l3_( z;fE+IeV@s==^SA>|Io4rM?>G-dG@8QvDV9LRwu3fs3pz1e)|{VXSwV7U}m>Zy{sqS zYR5uiXsb-mO^p;^S=mon$2A|y^Y9_T#02m&SOeq$+0Zz?YZwpy^?k|3s?acaMS1(H z=pc^VA;Talig9x^*>4onEN`trXtmY`Xl~I=$@rr_%%mp^TyE3Q@Zj^|_w5b-w7$OI zuDCQH)p%O7Mq5O??*`rU+v>q(5#?%!E0U7H!8}wHY{9GF1QlU zOpKQN@*cV5aE(B4AQ_&76|y)~d7Lcb70LE<&+4O*(q~@rGjZ^Pk-{7`;vik$7Q;)A z+RgUL!tKAhJfn6W`d;F9GJ%4L=~uL8W^Xmf9IV$Fnd`i$=k0`7znPD`^QWLhYTKJX zV}?oTi1*H65>xDk!Ac>g_x~&-nd@XC<>}(e1YnMp(T5|7^hR!PKc{MFx{66vZb*9ex_Vlk1!`+uR<9Dcs{WxK`q_Pu zj2w4st@ns6fKfp3A>WEh{ra+N>bdhfx!FSP_~;_l^oV?doFlIH!9E+|A4$tMc0_w; z(RvpWs_VZ$T1Cvmh&Y{*O=}NC@Yu4G)VYU&#YTPh_$PF`owxb??_bwH!10^d618v` zi@K?|JvT0j{eEY_8*0U?-{c!<$+J-318+wc(qZAO_o`blDdL*)=)ugzKb_rFng#f^ zfYa80xF8?5hKGMKH%1eee(m`Pf{kCTSZ}*v|9A~P{^F5+%q($I!Pl)3{?y^!?*xk7 z*_ewr*V(#X8&kfGjj$Q>*A#rFtOX2fDc4#}xy6d*GM;1$mlK`#tkd`97)jN>W>gcq zhm{!+)Bsw{PrhEBg7x5~?EWZEuG5;Ug|-ZyUyZ;y)6u4W15eKb5AUt}yNba&br#n_ z#gPZw80^-$Y8?UmN!>>3rq25ECKrcFPP($#I&RJRN=i(jt)wM(2fe-a8xOPwyCxBK zwVr$JhqHkyFKjt+dhgSKkqp^`(v!{#0G+y#->zDNq1QhaZwH^+pwqkTg17^Eau-CV zkRmhl7Ii>OGoaf_LYTJfPS=g-SQ(xV&LXqx7j850-&ECv+b~>j%P^_MN&XW@WB@@(rKBC!{AcC~@-JJ%NTM_TnmQ0f5+|VW+lg?f3f5Z=6w)7@h7~4AEECZ{0_gNmGUtPL>$Mp#UaR zj^Pq29)-lHLqi6n9;q0J7R4Ni6WF$e%~LeA0swhs=@9%pjMZZcbWZoDexj);xYDJB zNY0mtnD*gmHMeo(*Q?USwN3w4F>Z=3L~e;<;lfod*Mt~nt-^!%cDCo>*2Tur)6#=h zv;SkRNnL^z!UQnn$$&c8a{vG&TV!Pm8+yfhgv7!=v-ceUWx2L%OINv5w+*Rl^( z4tOp2ng)0=!;22HT=Lx&1u@GwRVgG6Wa-Fl=mD)X;!6@KfYxcA!|`lF&;pFfLCUdV z+Rr4m_P;*}AhHjzpi@`sC(G$$^+Q`Deh|oxxwL0&*zBr5oMsii8b0Bwm8^~f=hyDI zX>)O70NpT$Ic<=%0Mj_PT0=m2R-En`yJh4?O-mV3D4qkcg_U`G>I$eT*qprzM>GOp zm5N;S+uF>dwhE8TgfK=c1xO@T%g?(bJ3QYa%9X?Wv-m76jA!^RXyzn)!%U-0#*s^P zl~>y0D)sPS&by8+RVB5ihCp;7R%YE=sjAIR3)wpHf%}&(SC69SUc@}*ZZ6vb;_rnC z59_ku!QvPOwp3M<`f*~Av)3fA}bfjPbDT(*O-p#@@_F#YnO(ANDI z3$bXnZT3jyf~QyazWJs;yPPm{!QTjDqtkTQ%_=b^@l&}j-%$9O5=`96xcLjO6|<(M zK>GXT4LVC_amcB?)nilEf;P%s+9hITW#sOg*i1F;S%{)3KwUjOFXkbYJ|X`LmDeSz6(?sWASG*WLD&$C!}cQ@2KH$UM)hUD z)ZpkB_HsA((gw2Q>tvGg=#GP*n6MyKeB##HQAl|I-s!nD$z_c$zHaP&cA@_FH1ej~ zj+h{gXppXVv8hZEYZvQ;-m%-Nf3{|_wsJlF5#?QU;FI%ZmKCP4mYxlU!T3J z9xH|Oswqwj=(=^&wmVTVQb+PbNwZ6hYxZIXZG8A5-GK3`-2#!{MQq@=$4(QUj!NmQ z8t(UYkJG8~E^7|WdFN|O{WE@Btt``mc)?=?X*f@+mj|_>@~-7D(-F4CLPSDi%}&?i z&dGUf8gcWT>ab+B@PZ0)CWQXw#Iy8D6WXkcz*_gc?l1KL|6MGf7+oY>eAQiJ8P9HQ z^*3A!)=9+vWs0KVfVwdF3AOMBTLSgU<#iP+VjA&-Fi!G#ot7VKVVKF4Uq&Y@D@_7M zd12k*g%7$$7P&P>-}x^hLw;W9?`2*kp5t?mEgP_`mUZHr%20`u4 zpXpa+s(0tM6PD{NR56QwTxd{KdK&)T!|bvitkWzf5?$5uXB9AHu;nc7(pDL%?RQM! z{*HcvIG&lKmsDFr_+)d&d%CY`@u-_a?dhhzhgE~_vjUi6;5}b$EgFab&}lF=b;wkB z9)xc6bt0{;`|>J~tnY1i;W*G0&PSGI8Wrs=brqjH>w4)850Nsa0dHGDRVbA%xl!7U zecI|_z4H6t=FLDyWqEdljjd;wwKLQAzwNqvi%s(o2e$6utJIakhuWy2& z`ASU5kJIJ^-R}-Gv_jEUL~^mqe8a4&Zm(@$QlCyIP{aBM=AasQv-+sAlVxZuG$0Np zPjf4fF-;7>z?V)=Z2LMEM#9SS!B0FE?eAZjLVi_aC?4SAk!gLS(!daz=%bRrObaiR zp-MxOfQvwxf;vmwgSgB*q)K^gx$6FcPQS+e4amT`vTAO56je!ykt_rIP?r{r&$kgEEEUrV!uH^1?mXva!L%=O zl6W{p!23{P6gnTT0DnA!LtGR>T-g9nSn zZt?x*25L)E0ptk(>yL^ZbS!N;ZYxS`2+6|l!GwwBL(su$bq8&G(ML6ix0wA{^MEXAYcg?Jn#BxuN&E^J_2`l$Iu&SisVO*3MjV|L)Y5^L z6}g+I$Bl=wobI=lFwD~WJH`BRD$^m0NvuSCs^Ub-Ur(eSq6*h!0^#I<9RZ<*0`V&NE`?#%S_Q4CT=hL*NVugykOH3C^Q z1w%ouZdnO$i2&xM@230p8|E#^`_tWg<&!tMMBw227#;w zRjUtX-f88ocJ)BsHW;@M#@|V8?$>ir@nV6coHCGd3`qOwsC>Kvb<1Y zdl;C$crdpR-kPK=zvX=n7>|+c)r;zIzdPH8?4|{x658zFEXCUX^ca{;%z>yvlmFmt zuTV}Q{|1fr`=A>$23?HC3&WeY;A%QQ_P&Ill^RV&yRuu}4o+b9gy-OtJ9SF{OMgy> zCU6W(C1T~SaN_jJdTghVBrGW?E&3O>1XhBEhGi1g^5BiFEW|1=QTjLCm3ZxHU}g9k z*CL)nK~RuLXp1%?Oucd!_gRg$GXbYLHd$6`c*_QL3Lcw=lLX~V3a!X^_@E>bWU1RW zq_ZAb4pke~)eNusk@66G{5wl<#wTeR&pJvR^t1*bnIH(*IHzBu0Uf^2fA3TUlIM}o zgddV;9jHkMG7R3I*`sk+^L7?_@C<19FdZON8|t26q&TjX1IL?+k^s-(x36jgdk5Qb zLIk7&(yn77XL_mZm|HH`QD$}w-09h(2G+DQw`&mD&nxPIi59e7{=oHw>XcnA&HzeX zIS&nXdflZ*PX&H<3>9r+a)jY|O$mQZS&D(%-9QsIh3k}*UZ zYIVvvN1pM{D93M^8;)+@0V)zQMOtAbJl~lA42g!2tSawKOrF&wA>CV!Wh>^@!hNUUeb3_a z9+%)0RJ8Tz5gz+Zu;NH)YP76`;K|YIkmGZ?5Qh zK;_*8Td-jM1B*T};U!cgQL3bsh8Xi$6%i03Lk^>^G1M@VGcyb*Fy{`*CUC>1VO;#H zTAf>)ZB5Y+a{Rpft-RyTa@*g`K8f{fQndkV=tI!;4>X~13Vo^G+H1Dj;?n~w6;wgW z&ga+D?e+EZ^|R)Oi@S@o)ZMNHOw%cJ!6eLwufeUEb6yIMK-gVV&ZML@C5TXsWso#-J z&RcD^+ikXpQ44C+`W!cYmfjpW&RcI?+gok6+iFmw3rkb&-@l|b+8;UeN9t0owG?fy zr>CX1+ikYnQ3@d2N+ZW!GCQ88W+qU)V5Cw>6p};`5FjO1r>5H5jdt5@w%bH8hN`P> zw`z0hyz8#E+U>5}ZLZlRt6-`KQxqteOKrB>Z9yqW)uFZ9UG}B%^tRh=zTU6RD=Y0m zL$H(I>T|X+gq2X5gldV9Ht2$T6p%>*R~Q|SjyyXY+Y_Ht)IB{PQrl^7enCUC{K-Ci z`J-$-1IM3Ar5e>%T2+(bJwvmw)R^-Xz;!Se0J0Yg1}8=wxX{$OdT%Ymo94cEk1Q`` zt#A&Jk#P6DW9q+A03#EU2RZVGy~qIAjtAEKNL#fBQRwRT;`_Clx@y@b!Q0^F59WUS zk7S--PM-IT{wi{_jyT_p-5hx%2Ep0fuKsSP=H>^3fqyuj#y$M6sO6a=?G0SStI4eqQtJPME3hGiG8ob=u z3BDM>>Fu7qvt7L~?y9RTwN$T{pL>~=aVIkFI!kuG8(zfIXt;~e8qvRZ5J$8?4r4mg zXFxvwV|!ptdXP?M`w-Un9<=Z|76#huw?ov4l`|=mH=VMV>E(R&jd+6;AP6KP5_t*a zWVtv2xLo5ri%)NK{13lyeEuqkAd0X`R|vNYRMoBz&+Zn@?Rgf=ARw%zTN7r zJ~p2F*XVc{&UYeb*TbZi!biXi+y)FjfG}Y|L$-699PaKp9LAf;gc3V*fO7`dd5F7> z+|eYGNIKjeCtV0j$o3@8!G>>7fJ8(*0wWRU7Rkm%I587y-W*4f5JZ`R<62rH5hfU8 zBViFlKJ{>hXoYadY_}UN&=ocs78)aB5)vS^E)|ftirfc@k0Se7EqLuYix`UqV31c6 zAPtoxzX(YMr{D+3w0aseNj~2nPhyB_XC9=SH-G>D1Jd?V`hQ=i&*#T{KCOJd)TswD zBY`qU8q5;NG^_M|8QV>FQyHhtIqY3AxK*as4x!!8xDEjWz>q7zM-mB@KYv}WS4i2oc8AuJpIBxI5hDrl4P#Fee&5~D#kEegrt-SHB z>e`2`9tbv8MG2p(2ln2D`01;#Rg-Z$-Vku(yf1#On$km>tbrRk+B94GA#Gyg)03IW znuHKhCn&?MwBg(|0}83>F};vO!NGfX(odF%k=Bg4#4lZ6g{?cS`>hG!iL-u>@7up# zooQ;oZk>|Z+i?co zm)btVxa10l<1iR7LnLki8_6V+N~)@=s?h_lE}p0C(rFTrj^N^TrH=_uw|5 z05j@8LCEL+ra17YAnS%ZbK0T4*;c$)rStw@pa1{??DhO#FXek)pAXL~an;Ixjxip~ z2N|WiJEt!;X7hdH!lI63^P={TRE4vn){&$7_-DOl6MR=%+oavE5gHd9G})^<>m|(_ zx<w>w~_el|};U(m!Ng&Os4ALh(_Ml%xDeB*5^}yF;m)oD$^>NkJ zCw}}=cz`a^InK$47S?iD&EqZ-VB;1ITzXqW&2E^Q>#iCT#)>PUVh z{@!;|oqO-*om&nd3D3+W3cZ?RVQvU_OmrK8nN=Fso@UQJHHnaZeaRqzKp>J>5El&q ze2uZad(?b>)WYgU!1x3bP(U;Q5Cf-{{6mZH-=6YC%l9fqL`s8&R*HfFt0dL;S8aqajJ1A_qF^k z7(V-cQ-8sq;%i(5OFz8wNMIl)3=arAO?T9ty>-`Iov@T>nGv~*MY zA1RNWE&k)y5z_7*Ms54NbbWm+hclJijYm|SYTI2|m5T!M`{IXR2mqaspTYO+wbHU> zOm}I^mfNkmx>H(OsohO!)m%V|oDkc+22u@!a*#DgGt0xZL1;`0L}5|%5D?BZP2v}d zP>X(Md|Uh~6jwEcK|OI{1EcR}(3A4(L&Q&q@qF0?z^m`_K7Wsy?-BJFD`;D7Tb&5x zJF~eB)2>~vmrgM|i7Txt>8VnTse_n-fMLMhO7SSGk`Q1C$ZbVM8q`~AD$%X0=QglY1VWMt zI8`eG>%UnpLP;X?CIB6O4xoaSXbgMZ*GUN>39#+-%N*4%yRGHoWyh*_5>!0QmxLgc zq=bmehSdlMX7Nqp5y)>O{((M6C6^m4qzH1p~@HKrWf1gd`D7(7xsnC|i^p7wEyCzawGa%tYJvjmKG@c72wjSe>+n&mM2hRKfT*-CWyP-<=Wg zNoJ=!DX%HS^Y}1!wPcd&&gC8KHvL zrB!-X`2$Er&=a`Y+(;vE?j8-Lh-d^x#S$BB&|6}_ra6bB18cF0BuP}^&K9WU>^Z4Q zIn14I0*^HWQ({0n*0^prslTju#y~ovRF;U+N`HZ7LnPjW;g{P75kvauxG=S#N?Ne_ewo+SFR#|_8i1d{nOVZ^s7 zxSfG4E#llhY=bz;P~!zw{p1RK&T>f+k>_($5QLz<#6)&B4l|PxEQR)Oyh#Lsy+g5i zKyofLDw0hjE_L*1%q|V~wGLogu0&itr^vEf7stnYB0OX`z~jG9U3JyyTdxqd+s77A z(KJ;UnGcEAIiOGO>{16QNCAPE))BzD(suAg!!t~Y&o5jHT;{_PuJ!_ks@Wn_ZDEz9 zO)A6UN0ozz-286#SG{|EvX{MHb7Mh73nMZ}0}w?&1|KFK#OfZxcXeCqlPi%Q$|jcw zE*$Vh)zx%{9<1HN7=aNF1>!^wj&2iNR`qV+?rw9O=Q+!c2RDKc%}WuvIF=1z$7t;& zBjOvlu6HIPQ1Q_@rli!-X-(^E+)o#pixYL53!%ch6B66;*lJRjwhv-~U<1A?<8%)4 zU?UpK!`stkQv0Gc13V}th>5eKsXQChq~iiG0r4_M3^W8zKnzW~aV)1&D%duerVR*S z#b)zi11QNPk}zO|h^#gM6;J^@qt^Fp*?rs8cs_^OF?{jUfj7$tAEAQEmi%72XH~2^ zc&quSJ^Z)oJoo2^Z1Sw)D4-%peKpC?KhfK8a=(W ztkhObxylB1ySuulSasQY-Ewt~i*|i&M3gzq9fB5cvTbh-p5ER)^}#haGLHwb&ItYE z@G1HH9*U|(86=S-3yHsmdS#K3)+_=B7G2gBi}TV=w2kXXQLEi&;UIVYPgG3>Pah>f z59EFE(1$$_s(U}J&A+}}Yqq60^Xnn2XL{a`T+fGAvj!Fhta_+}Kc2d2K-|ts6#&P_~zvo_;2ODZ#FGd2^3v zxjkkf!}d|miS>Cod7Y6=aoiQz*U)dInKiuVt%z#@a7bt;XhGTHx5EizehBv0JWg`9 z!Oj$s5Dm#?(4ytMQD*Ix~M9|S&YoBm#_ z#PNN1!T9`x!!(Ubq@t*xsFD=Y3#N|ak*$v69OPwqoHFquw;b|-ujj?W*DkB>dnlxOwXt@S8%9D3oV4|*S?sh@k zQgzjJM@`I*_}({$w&t`IaxHFJNhFAoHYm3%iOWP=l{>mvQQf&+&JA?rcWiFe%7UOd zMNt;z7LyQbcXp?B(cN7m8#|_Q?U<{j9NU_?u2?9xIdmo6(&M|51d%LKMZ6D@HbgN!s7|>cRCJZ0Xz=Bob$Za0tmlw0|?lR=H7($jdjO2$5)%4AZ)1AZu0RC zurg76#P*G^jXmVxqNvAI`r==o#ssioBrtBoWPl*H!E6q09Oe{*g8XjxSpv&20wOVQ zCl^(mOr&Vqg$I~ zxNR7gmZIUJ?(Ih_7AUtiw-^2GSLh1xkfV#bW~5 zO4d~Fx@#7>a)XyMG@D5n(YafucW!OX%4SN9qprC`s0+KMRnC~C?(NGKxJGH+(Mhp$ zTboXGYnC~&6^%yJTTbh3%=i0in75AT+TqS?b0r+2<#TeVF>Gsda;{qD*C?@3Rn1k+ zYnCHixpP-ER8(r_w>1?-qcqV5sN0pza;|M$)y*MJySue=jXSg&)2^~>oKu=L&C{HY zWH&12xm>}yuPeF&9JyjMw??;VUCyfHRHq|sZPF+cZf-4>AS@IL zl>($8_0#~&7DC*jEo+r>+gB>ZQCGb1v|blyHMc0O&8p^%Zc~8K3T{gL{Q*y+tu29RJwJt%jXC1k^F1e9h*Ez9V<=xPn>D{&0 zN;NUpKwOiQyQr6TAUf@PlMrE(1)Aj zLVS)w=1vq4-ay-(jCslA!=I^K^^!tG+2R#aib_!TVJg|bE+(5dq?mrq=bQId+iERZ zEViqYkZNOy_TPoOB>hj_{r`T%yPj0*>-L!=OCdxRFHSxDz2_D}oHXx#^V{gCs-nS! zj1Ip5`@^%Z-FzEZtWpvdqSHWw?K7_&xDg(gT1`+mZ)3M{=Y;nAbU^wuLy+IF^gTNL zI-;#*w4)OBt;NhyD=#i{QA*Jo%TlEfX-2J;QAMzl*7I2gU{j5-t9>?c*V1doqG(a z8($sPbanf1j^weu9USeqkviD5YS~I?#=)D@oyi7NL?}Ycr7JL6bn&WNbgo_jBrKA` zsG?*F5RR6RI{kjXVN+Y*^?LR0`;>e8P4QTuiYhFGQYj>X0x|*E6m4H9%uLYT!+|7M zNr3Pkp9G}(-&39b1M_OO+hIhP$7uzUozvL%KNVF~L{+9eJ*#L>F$dFerEDP~VlpRr zmdR{{<|l8ah<5pL)a{vTQ9{JiXkN!jdT)^NZMb>E>jJe0Ev>^Gy@}speg53}p==b? zLkXTj^4rcrITx!)Nt~*=(T(sgYP4YpLP>6%<&QF&VZAj`aAbgFIA%AJEP6XGW?)gd zG=NrUwIL*;bkUY|tXU1+K73A5x7UH9#7$JBOBG2XKF}f*0g#=wPR}2aH$I=2-{NGp z)}ZAvM7vGz7x+~^ly4-6^)U}*g*$VBG<;#D9H!z3o7bnBtfSIlN3wMHJKZ=Pxz7S` z?3_*W0K!D($*?XG(cl_$;tk#~h)%h#opx6qdN9|T&vvLP5<)OwVV=UW;1BFi#XJgj zhkuW*DEfCcd}*#2lFyI`f0IM9LuLA^$MK5v;H&9r-g(ch&3#xpd3wan;}?uZF^pk> z5JUk34TwcRH%}1}5JH0Rk_6^_(C<~}!{R|C>;Ps;7X5x1*Hx3Kh6g}x=ZUS_?L>`% z1HOzDdUHEm*&^uhfEY)iWC3LcJS!-2oZjY?22jUN9nwR(NZx&~N;y|_lpKgjPn^gS zqjn^H`gNoVk<3g_I3+Kc#e%FK-;g*3kVt)~EbS8ewTRS7V8!CLvhJ>y_ZwS#$`cS_ z3HMJ4P_9p3+NAdRSGsto9x$#{saXn5Fv+Pcp3J(!iiFFa;N)z+5nX5s;0FP3;3Se} z=ZZ9L%)4+~mN}v%){e4P6Q3K%lN{~O;FMr1(xi;m@eAqzlw{~+jf5rmg5X6Vgek&r)Lh73cCP@g$k|dxn*E6f|>c0(gu%wu#kawIc zrJV_`$yGcP0T*$-2+prEFo?s~p5y8TIQo_X2m}ylmw!T8eWBi9 z%#vg7nHeQaLWB?Pmlao5T&zRAkzvtzFYySzDxwkX`YSzCP=$#dWUx>~#TOvKf?$`6 zU%qRO!B}g=sMHQ4?nUKVeyAZfnN|~&31Ft`4F*MGO7IzauYb=R7TDsU(ZTB@<-is? zhy6=It~P!&obFOCoJibp0bWp(&D8hfFLgprNREGHuwjUih$L9%M-mrDk01`aYpCOa zfyE{iKnQ?a$0YEQK%kJI^2m;8mSBcXjA&wv>vRV=ToGA>IXjW5-mLB@ZF9K;d)?bu zg4>?q zf&+*NhS0(Q-0!sGQ?bStF^pnxafBZZ?9DhRJz*UBc4IqUzD|vG#@daAtmbjDa&63h zZ+Qt@$cXPX4LQ7sWCte`R$FW>@~Xdp+TNb{^WcJMG-kDw42dHG4Japv2>~96f|Nj3 z@0l7Y+O2DQWR~`N#@5|6w(nqGaCS#0X2fbC)dGYudghQc5&-CWpF@%=h^nH73-*1| zPPi1mV;tc_iQtfCe(ZF1&fkK_QA{sc? z9=c9Es4B3n=r=cOe(7_disGkPL6jCMi7IJ%B*PvcbQ%~(0oZAw(+45((>!jCbm7e~ z=ff0Rcd^8I2Im3*VuG+swk$~w)4=tjxX$(sR#jO3A1$^UQmm3eR!Iu5ke10LRS{C6 zA$kRbp}N~jVne2JlKbqXsKmny+3QHW{X49k2kiir;0-Uhw?%vpR8xE`N z?fHzYGwaiNp7Nw4Zjx=~a|1<4gbpD#Dcht!0L+u#q}Keq{KNHeJI|$Jp7iZu=#MQ| zA)55n)*VJ8CVH)IdB<_aQrH-wu(OLx10^l#lEyi!V#cD;FG}lfR^BO6>V-@Ih6K4R zHZKl-IPI8ZNl?#@>Z)WnGi7N$-`+R z-B(uFI#~+mzItp%S@l~4mUnwJhUtKq2Xwgzs}r!S)LTZN2nm`GLd*#P97k_+Z`8vV zd71d??knEn>lPFzxvn3VK@Zi(z2}|e$nQFBw%ZdU3^|>Uh5#v7iNmKS0Neq-+}TRk z4!GPeg<%UG6F+)P%WSL&Rs@wR;DnNR4_Togg94^ z2(4FCFB>ESb&Xp}wN7d1vAJv^RFU~O-K1Vmr*gJg3ldMvA7*_~MF3JM0{{|u zHrZgpo8(r*A(#ewYS-BP2PGf3Kf8|8p5I+t*tHd^BGhfLYBsjstH+Ak`=4>2sHDN6gqjO&e~WmEr^4b)h9iKIfOQi3ho4316z8X2R^2 z04ECtV~Cr8Lw}t>;gN;~F+j?JdA8N$fh2-4fNg@r%Nq3CTE@1TrEQ>*Vkm5gE~SMH zgJE5>#6JJoeGj4ee+T}453{fV5E10Po*lYgw-dxuDD_hM9=Y7_WqVdR2BhvX+nW|Z zgSm7fAree<@fvWb%ZV9>v|hKEnwZ{hIB=pLH4ZCBGlPt$!c1oY;2pn;s`$J-yci-( z1pASzmXW8kub9;e&Kw*92JREB2bt?)WR~rbhVgdTtuqp(-L4!X@>k14LlHc6>-oa8(Ekm)~(iH?hQGdeqj*#y8-o)0{qc z)oRYNangY387n20*%0|K{N6Ve7cW9t@xK{IO{O-h%L^-)J;bkdFqBL z!oFr9%%0k@c{2sNE=h!vOBO3>jbp%kBX@_u-+`%=WRQ@J(b7u#dN4k0l_HXOLZB>G zBjNRQN8sAtTRLmn6$p)`4l=Ov&n%oQGFUf6Q5Y~;%MwUz78$++%c^kl?)|!WnW>8# z_l>A%(OFJ})@v?p;ZDj7_MBYjBQHuUjA>`|&U9+8dFPlG1r`Cw1cE^X5U4%2BNh|X zehFs$z9g8;t|D~^qS@NW;VAC+ zzid8nNd=ccxf;cZ5}w!=U_h2JtBK`toLK#Zy8sC}H;k<3G%SK;*3%UhF%wohO-S}c+O|;_G1eAxsjhuB7p=L<$*`%^Sf*g& z&PKM->@3(%BN6oy1y5NU<@7LmFyklA}6`gk15}%$d@x2B7u{JHDHEj5WVlMZbufFM1~tHDYH4% zO&Y4IlAOItI+=j2=X5iWuF|K<%t_8u&8(LW1b|#A7#IzaLdDh&3j{c5_-=B1l->i=+wColn z{JCW)k*J3Jle@0o?>>F_^XI?GjK&#^%w%RWF^t1v+EQ9l(`m77EG?C^=sy3U-SOAC zdbZxNfQSG90d6Gud34bRbK%*h$b7Zhv_i}=)|r{8RqU^UyMWjO0hlrk;rp2`cZEFA zamLvJfNpsMf!{``Fks+Yah>olJjT^C0%&gYIPbTRPTADa2bUqux0T|$^X8C^bCKUb zs8AQj31GJT09Ih*!wk&9GdNi};Nvu-C6duXJlk5Gks|ylct0#oA#}%D$bcImX%}COW9FYDZ1S99gsdO?R;7xB}I{vl`(i) zKVL4LSUO0O%e58IZo$X7-K3ye9efxpNj^-*gyMK40MdZLCRa4pBhuD0T-=6QezpcWB7AVSvC_ za89)tc?EUY=U%2W*?pJH?45)<9<>A%h!b~7yTS-=4Lct-`a(C6q9txmbw#UI#gLXP zNCemhRuC$J@z)WHjs-5wt|V10Ww#52`o^TannuM4nycI5QF0CXGjzn3fpoy2E56S zSZ={W!L=Q7Zd@9jFP<(I*@83%Dmj=1>>hyUUfaHiMlh}YmzNAEu^}WPUgbg2scwDS z{QdwL0AfB0pl!`MIh-YhEMQw;KA{30NfQngUAnYwYi$b>QiqRYO$KR&zLdZ_R}Z>Xy^`rx#FP1lzuD5ZWwlk0igDav@|sm`uN<5IOEA!7yV zbPS3YU<7Ufv@REUpBr1=FotyPx48zS^Nr#`w=0X7!W~L}$ewz4@6+^c7+!f5NLV?J z>6(^+cLQ`Qj`k~J$8k*!ve4+YG7FYs7;giPV~zA82EgAc$X#R31}F)&ZxJbLEfCJi zYuweTm8w-*5D#)U#EDzY5!4koJ^asj5ioE06SPR%U!&4Pl6-Y{@xF#9w$273ALG}E z@vU19NS(Ot7>@G~7aq|*NbPVIyx>sRbT;7@H*Y$?%w2+kxlsny`Io8D>biagZ_9S$ zXc5)wXMs#mFR0gl7tRLV*Ll`r2o7T7XHh0*XE5WUD4mkrUOB8&aabkxlMylk54*TH zDphUXXzMj_OYZJ8S#Q5D-T8Gk?B{|bC5D4BMM*)_4K1b%mK-4vj_FIKh1Av|$XzVN zV2+GREJ0`moVYU!FXT89LyhaLlmR&Eo~WteMfY&ARSkPzh$nDCBsp0Gv0*F5ci&Tc z5uze^eO_le)@u#wo5$1JhmTJkoxM5fFb_Jof{@n(F$6LQF@=PHa7Jf1iQ5hyh6)IS zvkVDgi2#}ffz+pIBGkD z%A?>QbC=FY45Dx?Y-|g-Tj5hPV%=SU?hYj});4?RJwlbM%q;FhuI(wvJ2s;fZ-ia9 zpa*Iw#5u^j6ME5s(|XqmDTT{XkhV$$GTTpS1c+M#nA(=w40=tDQRNZrAQvIewcO#k zheyg@$ywjwyM*%OkP9`?wk!ZcW!?8>RaYvj7_wCnwfX(6+Dn0e06oxgAdTS>$A4XK z;j&Ef>e)wT85tC@=AKF6+~U+melmPub&X)AW|3$hX#~Wu&&!?lMGqndXO33(^U(3x z)4|VkgByK3dW6gc4lt0cDbibZ@|99~pL6R1*nI%*;N@+!fZ#O=px`^=A%sLc5Jh1m zi7QlwS*Ef9swPs1nLW zoi$=Hi7f|TejSn_kdf?z56V}n+3LF~@?}>sqT6_u%C-T+pEJH}oL229j=B;cZFL(H z-1yVzZ^P{ubtq#X}0RqgpyS8tl<&7LPU^NBmwfBH-hK}8>DeXu@6aP zk&>5*7lw$%)Lo!%3PRfoL!{t?%R8%Rj`& z?XIsT+8P=C-->S@pvicgznf2D8R9(2F9iY#B$C2XfHaa`=G8Bc*F3hqgT|K}vnUX3 zoAe^I@6`&OLtQ(pwg_fX&RUjrB+8OWoL+?ZWmGmHQWT#bkHdSR@8_ZlaX&Nnc<9Rf z+N*7-QdFSCwRf~6I;Et7;zMc8S2G}oX+tkKdQt=HKPCU)((wPW|3S%?K!I}GxHJ>? z^hGQ)wtj3inX!R=y>+>4F3vI47m}jSe16Ahc*?9)3gf_MMlA{KS<~O4&Z!c0m*Ad- z$T+>LZBIo45NTYDiYLD8FcwRBt;#^6222Safqs=3!{4qJvgsnAmxSwUF85srb8@I} zj=J3Fy>5-iq<96+P6UTXTzO!UfzWJ+TrRxZY$;1aFhnaNjM@>4w%2Uyx%b*U-Ll16 zjqK+}tES5ta6EYM5ee8ec}-aK4jZi#H4Nl8>ec}vkQxH1s=_3TvR5T4`Q@nU8_ZM= z3p>3e7tHZ6ToalLn1KKr=pAJP=951IBHCb&cwW~y80cSXAz8GLipzUxuxo2rG zJF8^)_l>wkjR)C?ovacs!_1cH-ClA}o*bgl(8PC;`P8-}j-#f|E#oQYX=LU28RUD_ zG7=69C~;RqAugQSLklFLMi3}OmJ_7IsExqP%b?k{>&m2nrrgscqMGlkqADRpLx)y!!I%v7+9aEm^>fwJD zHq1TZ9naLki9ynHrh0h9yTGS{Pp^AD_fFpVdYxI&n5;10*G|2z>ys7TSg`F=t3#?# zN0hC$q=`=+oO!$N%hm>UV}-_I8|{dCQ%#hHJ6OmT8h< zndJ0@958ViH&p9#%*DtF2q{uTV+zoCpqXJqQAF7hMP;croV=U6&7di24Tah*a#rYJ zSa1w6D#^bLENYl@+rQ+A1xP4Fc;T>+!RFZuZMTninzo+7FQY`{$5%sM)xiCJ`toh8 zHt( zkb;Q`Bv$lL>a`2JBIvGsUK$sNM>EKn(R(;#f)}wARbLeWz|gE*5V`_w++&gf-;an` z9(?@-^8JRUw?0IH-rHG4%q&R*fhA@Pde{lb$(eCRNvb8HA1Bawhsjx{d~G42jE| zeX1B$4FD0z2IB`PMqr{&{s+xDmM7uHv%zNOv*@sFo_J$7t!tDd?xArvwZ zK&S{Zq>!gY3NVv01=`{%(j5sn@U#me1cgb5DDATCuqAdX)NKle2XtM~OK7Mg-HIb3 z1cE};B%LZj7Q~W9?xY;*T8wDbjT=;!q_S3$uT)9=6Bv_DA*Pa7RfZ(GVoJL~K`lZp zDF;Zd5TT8aPu?5j28uj9px*4o2PGj4RM}?7r)ysz$`d%p6ET8okii2Zf(_fKOo*ey zg~Trr<Ert;df3@LoFm}$JvTcVd8bK~{Lkg% ziR)^!o=$H~&C`7^BjFVU;6q4S`oE79y?F0?c(P#*`D@cW^+_^J%!7i6X(CgH1=h|M zbecY^k}T8Vq6hKFKQ9a%1T8G;*qO2S&4wXEE!@!60c{VY@-*U zsnM+zwu-CGq{_s2HCLR{Wnw%PDAsPGO0=M?R-o}7 z>6vl4-SiY}l4coIFx>*?$yu&(@b|c0!8)UBH>jE#Bz#%-APS&E(oUqz(Z6wyG8r2} z0ip77NLd7E0)fO>*sQWZur35`b#GSN-15WL>rt^tpr+ia_!`NPXqvEES}drTKaY(P z?Ft3rbX^tU6;MM`oB*;4Y&hJ`#uFp}@*z)+t1c{hD8)?{Nfekf5*Q#rN|WKvnA)*b z&(()#C~niR_9-E5KpJg<5pchULULS3IEx?zR%jRc}$SEGO8(f0JOcb$G-1Vd;Ai~NoNVx2A6}k`u1wAt{+pji7W+ft1 z05l*7oIidx3w zd%#W{U`Y0OwAuqACe#LP6^FK@JnTzGsU%4mAtJ%c;kUqY(BNGvNHPH;$c&04G8NT0 z?c#aTImq?4t&n9C+P)%WjV8RDL~YN>y=6?M2`VXu8EmU8{$?!Y^8Gzl`fGhR%=7Pj z%h&JiJP7@WAwC~?e33&*a#D!W>|sqM zV%i-SJGU#A?Mq#z?Q4E<+O%v~7FhyLiRF$tOgnUaD%G;vYD#RuO3Ci)Y39`GyH>Ss zHGMD?!^DIUV_xgCh;uT!;pg08@JToTIKdzRL*tR^?MQo`VHWl&Bzu%tkv%)fY+)$f z9U$LBM99wrNsJ4}No65bExAV;zIAmkc?mNTwDQAdyC*MEPGKFahT4HD8&TB27(fij zsnbo=EC6wPQ?GS(=_{#Ao|9^{T9CUBtmvI=B=adf8`SZu@kj3RU3tHr@Tu2YOjlb{ z#6*C^8q(5GZLLBTDis_Aq>4l^T1>V?Lq;pE0#PCo0Cx^NCQYumE^u}MV6EMhS`g>9 zoULlyS>q0VHY|yt<)EdIv7pgK;cBR_JvA2&>CxNisfQP&k3Jh|w;aghM##N(ko4ri z-Ojx)!d94A(kmx4;e}~2#+;HU8X8go*%cVFO}D~2{A9azb;xvCK6RGVw5nx`c5YFt zhyfE!ZA)6I88x8M1~sKiODzOOB3P1RG}frC5Qq?q2&~(aq%Tf`skhl240-gNV=)=@bUn9; zAEHb5wB$_gcG}3WdCj8N%gap*VlQixE>TcKg;0-d%?6WzE{T(pn+=<`;F+M`wawkS z2;<3Zl`;ZSKnMg~O|9+JrohhI?i)y^VRaW#%xfexBxvqsFL@`{{&Cx7$@u$Ha8u2a zuvsFJU<)m@7#}Qob)64bd0EPq#j@BHY9nJ&LRs@D)q9%r=U2kFF%9tT8(vJlMNtgH zH8!b^(0UBANRW$cU>OMA+#3loiBd*EPI)40HOEQAB%4E~vJocbRYhH5LlG%0n~vES z^&2?7gtkU~dL=U?)I|NBs66V2sGB@~^FHVKzc-$|yIkc} zyF!myjUC$^>v?uTs4zTOzBDXYXjv?NE6%@1+wY+?adJeRNV7-*6pchc#MXR=d;gEL z{V)6e=g;PF@OD>MXJzoc7v|jzfU#ry)=?XOAKt!oo%MS=cBp=D7DL0ud@GZjq*Uj3 zr;EasM$)4B6At)>^jh0GD|$b7jW>&VZPc%C2bSB!(`~IOd*=zj<7W;I z1B$0!dTc*l|Uy0zV1Aby#W=gb(O-xpnYwN3OoLLWjhAeF36Vj%`UEW_! z5b&t_9p4%Le_!waLH=C!B6%?Yd;-V%x}Ss;Ac$VD7SkeshbjbQ zyRWQJaTrSN-bT@JbRpG^C@@&5F!*^A@e)J0aPlJE;$@NtzB(*P_@2XyhCn>Tjzd^^RknhPDQ=V>uU&>Ak?Op z?DXhpi$KI4;sb%c+RQYh z&_WQIFrteLvD_NIt8Pl*$cqCaB*^D+yFuABio?DEE1jY9ndW!veld`9Vo-v3J&cAV5s4@_52{tX56k-Q8RuXi@|g z1P}=X+CUO(uK9Z@)E^fcj)Aej$kes9z4dn*yxe)tZaavHPBbHiAu=TjHIz_O6(+{TGE-0_(FEf{rNfgG zRuV}-sZz6xd3nxrbDTFka?xXFx3=Mf%WjqmD5D!!!q(R{SA~09Yr}0ltcYz= zZDBLd0&9E*)iTJmV%p}>R{T3#!Xs<<)orT3U9AaqK~L|f0Nd^xil2Ld2*ODT2uun9 z$%vT21PDl!nrMWI7;j937Kp@IVuWC*3?v9igb}%cFjZ8*0Rrl%V9VhxcxcdpQ4=;w zcxi$m||%XSElq)V;C8%Kp4a@X(6YFMI@0X3}hq{+`43-&g+Sj z0nsSCySnbWcR1%=(Q$((b=P&cuR%QA!$ld)wWi|-O}!aJ9ik} z&f_xa-PEYow$`<+EoE$|h}9C&M%$d%bFhwfGp>t}PG@fCDIiEWaG~AE=q~PE(sjt= zxaOU8%ZGK_iL$oaYO>bUZEb5yYAZ@s%VI8bhV9Pcfg&^t<#%^Bi*GGx*tG;mM3Nv= zFi6B20K9eD6CpV6SnljscS*|TIjEGhm9Z_QDPd`CqAhJKwUN=?*Ku()OgXV}cXT%= zTwJ?3B`miOS>7@Zgh^3bmsxfyAI~+ z=QSCv1WL5F(yCc#mZ@zmOHoS5h*H^51&dl$p;2lbb!FqnTU^>Lj1f%HC1XjDimJv6 zu@xr8FFJK?8B1lcHZ3yj+nl+Y(d!)N8eMaQvncIKv_z^`Cb+JptueKw#@Q*2itN(k zI=OAx8dPf1D@x|uHLaJ9b9ma0rK+t=&}(uRExRsVoW?y&WnqAcsOx z*S8CY9#eb{tqh|y%n)lM+d|@X?(SdH({EBMjnH=X)m07Ig^cm!c|Y0m1l{dm1_4;f zBxDn8s{`exA%?slEhbotkzo&WimA2R1R!Ju56JKj1F{kbbmVQpmNH=$MHWI$l0=7G z^G)u2_FBOVBzZeYY*48sBvOs__rx5074t=I{vqAprzf)8ZI(W-w@4j1KMCfQ-s3A0 zRGX#aE=)VfSZQT#nzzl{ZrjUer=^o*LLlA*-x=^Ia1VbXq*#oAs|F-LZ|-1`;n%eM zihdB(7Jc74&y9Z6!W&%m&iT#vWX1Az<717y?)2WN-sS?0N_BW^pJm>J5oQ$?=%9iF zVN>}U+!v~q=9VHA*u98H^x(Q>Pg z8{vg>s;a6nF(e`H?WTvacXcO>01{1=T6F2xWhHXkw6dE;id-IsQk}i0ajn7I&>mPK@s|hczh1i13Gq>)_^NP<_re$ zIH-&_3I_uMuT^^eTCaNF6Gqt`+^}i@Ewd=LjAQW#R_hj5h*<79eiDcc%SrG)PM#N*w#O6z$fBm&@oP$tz9NNGY6 z7KF78l3581g@PKg5XzE4wIGsOg7#Vz5ekSFNS27IHi8PsDJ8Np!V=0rYRM+3C6*?$ z2_hJhRD#4TLKP(5Zw2UzSTYZ!df@$##+-TMM_Ao z!cb&8qlbXw?C);$W8uYo&NU)-ol^tiy#22O8Xa{u6*6bS&a3$1V z6?edyhnqm-GkFj}1*L=I6}&`5XyI0bi?_fdS;iC>S;j_VAdAjKbK%X*Y|=5uJZ!FQ zZ4_pl^}C+iae+1vO>Br67#7qiutop^V+kZw3W$gi!tmivGH_8iA~(|ydmKE(IqmK3 z&FR!L$a}d}1QIH+RQi7JPpl7e9YEYXyev@KqjRHpyn~wZa<#>qf->cUbqGUj9w@H7 zbATDJz-2Mj;T4RZ9+?hu8HfaPt8bvv7-2BViX)sP9LC(kbG?EI;`Mjf*&O%1I!=Nd z9?y0;+OnxjZ6&|}DM6-bWTq)JY9ksc?dzK4aN>P}JppRd6r6SD8vn?=-Qk(0uVA~a>su+1Rg5ugpq0Ne&7U+iQ*BNX}NL?_Z+z)%w zwN|Yvi;)=L3Ddg{j+%UHXw|Cga@%sHdp#VTj>qNKuPxmnJgo0s4 zRiIMQrHYDHrKN>RT2k6HEp064T30KPlA)&A@6Ur<-1vJ^T_BLFNCNn(-lHNc%&O3cKJ43Oj+EvRP833y@DKi z7N@Mp&>4gVh+t$LkTXRgqF@kXbkb-pwI-_tu>7P8C*}^jyNGkiB%_BTyDvD&2I@-I%62sAGE2jno_A7%Vy1wE!S?>vo913yqQ%72{)XS9@V4 zr^nv+c%G*UWCS-84Y+UwEfb>}noym$4c%Af$Bpbz1mg4?F zU&8#Xs)-a4QYi?rFn7&#22Dj8{$dt?T7c#&MHI!T`n zc+^#?B9K^@t3xjl5;yplmZrfUJG(LThVJmJ+kOY1B| zD{NZKs?n`fXtlDMjUr0}6{M0{lcgS24N@LS2nrYLlmRc*({yALy|rx?X510~55z6th`r|uM8xKGm@NBB7 z&Y(p?&sA6;SctHu!Jap5Es9dqc(u2c% zPrmsNz5M?l!ucOl?-D`R=MRQ`kEL3-crB9I3bNZNcWaxQmMeMg#;3{AtBPQCs)DE= zR!`8I%?gKvtI86wXf5*|za1ssJ9s?K!9irBDLWYp(l%77E0R?B6 zi5e)cEz2_q?-a~CdP6cQcW9#H@Y(feNtQXI==LK1)5doAcXz+q{XRL}y)`okELHBd z<+qi&RBZ&CRFu4nN$Q8gs!o~XUifVjPLW5Jb(Hw|CBU zdtsjYp<0qmhX-|J;ldVV4w=x5SEhF6bA_5coNLa>r$=33VDgb_5gIZSwn%0R)6@}| z`4HCPf_GtMR1&EmA2vs*(5-8ETW!2H*0-{{rbPl&fvFPLR$VP6w552jUs!CDWm1xr zn|EB18xqMDt(66hh>0Swg;`5fB1t^ew|XeHb8XV3&1X$w8x-uRG_pM$`9DN& zBx8=s1d<>{kc(vSc$pp73CeAhY{17W>6KF|npYs_Ijg~hgpvUR#Jjuhvqz@u`9c#on@#~{CEmrecB+y%7*4nBv2X?Ndw5cgl+1FYw z9o^m8D{|yfX>69tCL<@M*C~0n>hp6-HkCMHBN#T!3mUqYDmHSk;j|-h!|Y7uc8H82 zB-wLr3A<7sYpz&vB9;h(C9|P}vQrC^v-a9*7(E%C_AizvoHkVszYg*8v zP!M855zDPA_bS%1+XYTiB<(0jL2QFmidhQ384MZMNS$fc$@@^e1!BlRQ-?Np4lipG zr0v5bvYmw#!2*gw`=}|p?I82QM8+*d4r$xHC8NYL5L)CX5S&+xl2xf~Wv}J|33Bu| zQP*l_vN{fgF_nvyhS*#vcAm`Q9SB)H;OvX>JOCv{8`b7ZU~9Dk=*c3nFlzErF_ALb z7~d_Z*qJ0!Qw|}Kf*@uW5``>^a&Rtag#lN=2FkP!FuD;e0-4_{t(Xmc zV@vO@O+rZGQho1A)gj3(k4h=Zezrq~w1Pl+xG1qykrGQJ*QZ{)LyBXqnerc zKVGUUIuu&#&z}Sp<1RrQbFN+8!;&HlRT*L|NyDr7+8@3LVst+XGVLUA3_-z`*ti>$ zHn!Tfu^}w7E4xf=t4TClEU94pE578&ackU(8Z2@RZzHD~+HrK`yJ)q6&b`RBfZr@Ahf-{C{wV?R@_K`hOq(FRl3$010^Teu|**$hAJGr6|1p36eOBpm0O= z9$sCY#tCo~mHH*J8y(CYY#GA}AsMQtfXJ|SbRGUesu6Fi8Al0lXO5jZ9~`2-q3Gss0r2&-PtRaP-eJ;?}i0U78U z$U+g$1m06dW-8Z=29Z@fKJITE14*}%rhU{uKW!u>>GqAjKHBR2DGvU(tv=YcjLf?#nolsT-Az})b zp*pskB;C5(0WCnUl1Bqgys#8Ub{NXmxOUMN;obtU=w(STzzT7|Y(Fg3#b6>52%~%` z1t3^*(4;MKX@G;I6n!)iM7F&lQ6wkfNyAX9=}N%k@nKmGbwaM&??dds_Cuv)I`a9v z;)bOK6-?1QGE|U}LP!e*D*)Z;aYGs$OOk+!)jQ*{Dg5hbxARjxn@=_S$*>*3PC*>{A}5(0?L%{tEYaN zVNrX95kQ@sLfDWn%_Ma0QO~+5u*EipDxK5J3prp@C1IM9jDnIZ1(GRCWJVyUQhfbd ziK-1R&7%cWw#l{NnVbk}Bd`$Qp~ygrKG##VnIlox)2>zdBi6&dVC|r|%$GT3dyQUi za@*Hm4&KHz0fApE^!I&#AG*`aKIVr0Pvk?cxfI>#V!GOrB`6d_2pi0|iUorsJFSCq zjklw1n)E3xZOA5ep-G`sW~CvoMsaH6&%S5p%Uu<%0 z_&<~9$!Fst=lJpOfGm? zPDQagZBZi*CKtw?%%eEV-fS1zENto?fRID{9S3YaTzpkN-s!j9Mdrjtpl^vzB;q+c z!nxqm-xaN|5J&Jx#1c0f?TpVQa>J$3$s~Y79g*SdnG|D%z`}jg>>i1)z zJORk-OfjrqC$2?RO&aZQzB3taX%2aN8}vJ7InDC&46WyvzDsa#nU?+aywKl`Zo_`J zlG5T(QoIsXH7*%jgWMhh6T8!VrC|@+Sl&V^T4`H>fqjz`#KwLLlxfa1Ku< z*qaCuq{YjV)1ViP_}<#p;Z5I1ef0KTyS<(FrUc<>j{9-S@TNI=-s=~J1aAVx zw%MxPdzl3oxfGXjFb)xKsRo@mk_iYXqyt0%VY(Ub;DeGZ;ojj?kgE@BDTo_r4YfAFF}UwmpQ?g2OIK@(pC{Lv?OWGsbl&e);Z(wg!ysNbmPVxBUXEqR=Qdo!O@Ksd$`0Ae%JH%` z1>=tW04jpIw(pyEWr$CBOnC$#Mjw!Tl{_^oSlV~+ycDHZ6^k}GLl``h;_8nTBx;Ity$A33% zNQ2|d=!zm7{dC}tb0SYNbjLyXhLfUAvZQ+NkjgbiMWWZSwz(C(=GwKn8j6jgN~t1M zTOhVrzHf$&EaDxp&+V=Jt7wT8#{hL|#j{6w^J6!ggEP$gb#m@=`fHbF1zy*CwToue zGNq{!06>xiW)+U*3ueT*U?G|S3_e2N1gi8&j7WL2Oe!Y8#s!HC zZ&skvrym_NyY1_EY-v(^&uR3Xu@}Bh=~8jaJuRteL)@t`&-pwfzSyg6a=>beSc%cE zUiNunC#CxQoII?bfjXhaP@yH5g5Rdc(D+rh)>bsq9a|BAA-Ci5y}V~^fU*Fx1XxB2 z$b^8gM37xjw#+h@Oazri+A_VuyK6}~!K@L2-U@<}NerglfnB{Z0z~W9GDyl*+m$Bh zrgLV831aaMF!W)7k1}t=1AH%RFKvodtyNH_iit(Aw%jeh4g84-BvFJBqlO(tj^|1{ zR711sWrdDN`qqg!RDDzY29@#(zj9w?(BDUC8ux#$ta*gdWqcYfsDyiR1>OaJVfslmkU(7i)5Hv zCPU}OKldhQu=&YJ-Uf?$JOJA4()-xImeCGu3;#dd(19EL(k0zyC}5EEcU zd4lg)0n8PgS&s$E{nUID4as0cCh)yM7YU|a}$MwHci`@#Xe|E zLd)X@smZ06m@cAIk&0RK%C_>peHQ0srgmQY>$>;N&Q#^v^~f~jr!M5DF68{M9%AuU%F%si0oXpkXL&=B<>J}bq zN;K}|ySO+}c#3jm=Y<;hNV zdb^J^-+gyocD-Az?>+RlJ1x%7vAqMEiWEhVUoby|@4u`5eZ8-c_a8Ce-P)^Fyl)8e zHmZ<|Ry`k_hO8m7e!7N8DW~d2rhgEiNq}J=-M8i6?K6n0BR&6U^pMjyuZma=qxb$- z)BP`l+Pzny&%_Q?a2e*`630nY&`CRlZ4Fn5%x%f?b>ePq;q_}8JbYb&NyGO$=H%+# zIYTB&f|IAFOo~A*{289Ph0t!E5FWa~W5qjrLyUOXdol(=9FpCk znGu7i3EjSLgIR4Sn_%^0UVB>qL%q${ zw~L;Qg`UpPjHh)UNP`FvOIwM#MpOrI*dr!qFGO})q1z=zQYan}77wdurzk3barq$9 zS{tw7+29DLjinnSC7Bi+9f%O>2B^xdfO^g2aMw_GYIB zB{Y$O0>U!x(00g#5OwK6IcTYT_ z(+uOOK*w1oE!?tt3skTzK+mmB$jj}QYXu`NyAOxbs!)>;R$jd6NwnbQ5Z<;^gK7W&- zUzA{D9Wy3_lsnSii4K_>l~DotV6srzcYRaQi0TroKmjnecFzUdtKNSWrk$f=3MCQnxcFH7>m??;G z(NcqsV7pQa(n=!-fC}RSt0pj#M))2jQtr#s^Pqvd8Gf>e3dks0s;ZT!49gNkqR2|2 zWY>1tcOXvk5zOf%!3nfAUsIu{lAIFiN!Ny@8jOHz=KH^nmB z7drbT9D90!sldauTVk6>Z^Hx3Z(Nqzu9}>pl18trkAA$|^O>47S-+!3)qtLjcSgRWSzWM3=(Xivsr~W+efE5D>gtaPA=5 z0+r6po9#z!N}5m|G?rE-kXyBYCh-@7PTV0GCex2&^#tdR9%!d(qbwSRX&_AsLZA++ z4q{zlY}g4p&SVVYfg+6Oz!Nkr9cqz%^GNSO_` zax~YQB*cqLWQQn;UWpuJ7NTCsCwkRBr;Zb+k))p2CwpI@4Bx~wCG*)lz&M#s+jb;xk-6~yM0 zsH2{q)Jge2J~t(wI;&-&j7L;y_w4jX1bOxLJ`dsXX)_GVsU|`!B#}S}Fbp#hBG{1n zyU0|@vUBQ_igMSe20MwTrN*(gC9_b@Zq!?It!<-|AnP`k2~r2%2~A&-AP|3Nm%``s zJ)hA38}a_m*<}bu(Z~TnGV<^iJyZupEs3HJCa}Z_c!$({dzL66_K6lWrN*UXezS8o z`CHI}WIaN5pA{0-bDEA-SqD59=ZX{vmD-dO#NHoM5sVIq#b+^xxL0cs_xQ-%UY@wZ z#|oYCj*jsftNH5@o?j<3(N4LC8xbG0j^~{jdmOmLZ@&^G9Z+$;>PAUhp2R@D@l&My zt>Yhm5)44bf^(@{Ze*y}Hp0vyRlhu9!!$2e>Ir6B1Tdo~ZY9X*#kmMp56Xbye02^+Q=WBmg-Qe_M z7GcEfr;VkQti56IT#1M4+u&O!M~soz;eKbX^Rb{*U}55{gCrBmH#VZO^jLy`igcpz7d|rG~vM5xnS?tN-10?mf8X$WlnG^u5l{mop zCqCTkpB9XQpFZewOm(GkiL;Fgm_^&qD@d{qP|hzj$`ttGBL$1wlI5V;U^M6e1`7u9 zJzrV8ktP3NYEI#?_@xYUO7M z6;jHAQBf+Ss*+U)3}GsXXJy?hNh27B-C37&6n8YJ)^HWHbQWZq1Bv&OE zI5B{gNid>PlXj-c8cQ0Lh*?CIDk_pvky4PG3Q4+YfW-D-(xj;2yGD?9m!QC4=P5vj zEFiQCMI<-4o8(UM^qQMt1cY$>HM+a#Dw$ZeR0V9U(dG#?u(4P75)@w4RxerqU5{kco^X)PYXu zxlAxxa3M-E3r4hR#;Y{i^LJZ0NvqYYOElC`gmj^&7`1^hiKzf=AWyR~!g^B>SP>7831%RSlhi{`GnQa0AzEa@#IV%X zTt+n@v|(qm$*DhqW6>SuHrBHeYDj?rBSaQQel|k~@)4@BPqw%tq>E!Zu4MAuukRB8ykgS-riS$Zs zL47q00B*4mfdr9O5FwtGg|*X{2;iqSHcP1}=OmGaD6v3Lj7SBHvy=#=DPcaEF5?|X78Nxz&$#wxg6FbU#N)AO6wm}K#P6?kv z>(e75!<P#t$~YQVGcYHhz%i~h64aGg$UUPndva#hEXlA4E4B7E30N! zY!EoX0SwP|x2S{*pNF=D5?YCfB|?dpm{T(7P@rs6qpBK2Q#)(0ifuy?D)@#>AsHki zh2T3-?U6TFki%IOe251qcTFz+L5PBg2(%DM zbZ7%?sH(V#&34-&tA>i}ZM3q2!dqr7r#!+Rm$CU>8!TIhAVE%CD05UfPHxp3(IBkB zlPdfxd&^sx|zU{k}zQzBqTXlbK)NZ@$vc#TG#QX9(3|fiOkB%l>u$% z#k*~`^z|8nK$59csksKs0&u)fF$bfhXkG=WQ*2WJ2ofg8f-DVom!hm$&L?7<>dFv_ z2ep{fOR^CQ0N4)KShxeGKw;ZR?c2$XXL&}#l##8J`;rzga;qeSDG_i0$fJ%rbKI@Q z^3BjP&5V$~`&r&m>!|#A(h?~>KGp}8ekgigH(J_NVhL14wYF5LTE+5S;UIAdK)$XT z^ZfMX>Dp%eG(78Oy|3a_NxMjnPP;(vn=QT-lBV;cRXULi?m8|nNpAI}Z%2`l)Ph%E z31?Wnojo@@386(ul|-mlJeId)xg;s_aj-JMIcQ$16b58`EExw^jE>mrK~|JWBoav& zkdY(+9=jBx<|Aa3qjMlDizYLIq8=43?k`BG9*v|6U}E9CCi7JmpLeM~SH4?q zVNeUoMeT)zbShUnI6i-uPhU;T`X|1N&5n`phP9C*^$VuVDF}q$R_z)Mu$LKC1`!G$ z2wX#s@iix>vygo!>F2h2ypOzRFzDkuHTK3up43T-O0M5+V)2GkR}~S6NeSsZ2btj| zI%zkX^{W!vT3Rff2*A9DNqqxv91};IG{8e6z;A7IY@iK@s3LGUSk_!ZV5!M?PHx=^ z(PY3gVX6RTa^~k30USFjkuu@2+1YVwYyg9RQ1%Vl4y5Z-cAa5^(<~Az7Q*F49XP;N z%8Hvs&#~;j;d#N*-A8VO*lvNwj8KXnl2j2t4PeMMG9NyY{++o_yU;f|ZKZ<(+hQum zcP}X?w}_}@4b!NV?_ndQZClB485yXy4MtIYcp6wh73d1^@)YYC39V^G7{1u4QPyQC zZ7(0Nx|`dbBf*J-B8=)}|?()jdF>^rnJhP@n+zbD7mM2nk)Hie6&%en_1KqQXM)0}!CR zux`T?v%_fS6~5YR?%3HLhe}lZGa0dv+s;I8lA;+M)=KKc=E!@DS0TtSoA%>!+%Uls z*5JA2+0DgEow5K#u??^%A5LP-%*@ZF5JLb63`KDRE_QVOH-nn%XkaCq>M$e@;i0`Z z_2BM&%(%sGhmLG%jRo|gx#su+XrN!7E}YMNvYNz@=7@85yH@2fhi*9#*L@&VLuBHa zoAAEYLz0|?h#xwyVcUyt{?^)BoZCHD=t;X37dzTl8YfkSO0|RmTBo&sn#g*T+ljPT z^MG;f*$YLNDKNTs1ny)O?PI%qgaf~^?mqvb{v3tpHk#dT$w3M>e(EPtQf0l=P^Yf^A`y4}-lO;uyGFBBK zS$iqET-zAt#;$J`Ypl3VDv(ux-U(-)y25*V(e|+*!SYF2D_{#@vXN{Z*TnREf6MGX zH^lQ_C8kJ19HRKJJ_qdj5>L@DqDTQBZ+wrojB<8wg*)BFRMWoLxxE=Wi_OTqs!kr5 zyy?`;9Za~DcORq7489{-%ch%Hh>M$ZD_mX9nd=-wy3J6CwRE`Oh^N&o)8|M*+}-Hl zOz5k`>n*j$#@!o>8Yejh_!EZB)7CMVlIQlBb%eJt@k!!l^7f+meKs=k3K+!E#$v8t z6NF++3DUO~F_Sjtq-4#jv2YYW8V>lVc;3_kVQI!l;w2?fDh|R)L1{m|ZJ0KR&K6;4 z?>qiTMJB_m;oit>%*YOv?YdFgrcR|o2Q!z^R)Qi6UWynZwJ%2{gP0_u(^V1&6f}Pw z``$)6d*f}?AsyvyFOAnO;~O1MSc}9&XskGU6kN;PRYZq#ze(`+^9r(BNZSvi2&6=U zY6R_8_m1=4F;@3nVC^zqDkgMiWLznIHlmBsM@Tyt#JEW*Kn~yl12b~qHN(r|3#q^_ zGWNB+>mulq*>FDgBNM*}?y;LYKv9uA=BkFZZba)a_Y#z9CP}n3Mrtz!^23N2+G8Z} zK~{s8Aa9x=fI$cjJTOcIamwRG6r0K?h+G|1XIr`koyBd#?QaF-T{)**T`gw- zVn#*-jOC*&ga)*;fC7lY7Nk=O6God1v@V>mO}d&in>ln`Hl*5XijJ*rjg}iRn`Fl$ zE&#|G2#5)+N>NlOQ-K+=cf2w;HZ6c;Qn0|v#R`|p5|ceLtR%gR?UAXaR^l+?1#wln zSdw+*sYP*tWT#ZUO!|5wsSH$rfuS%L5#R8oGx@W4D#I3OlychH%qZ`=>t-{7jMye(Q^FZ;)wqcxxVMGiTB@mX`_WNu|eKp8k zOn^6NW2$s)gi!C20b)+ zwS|hdQN^d>_WXC+_3h7Yc3W*i($z%y9V68i+iODD3{)11z5E|wKJPtyg!c5GSXmMS zkP1Z?pa`Lb#p;NiP|#8i?qY}fL>>FJwtGszokAKRpjdKP9Qz`2dvb_jEvq5N8->{u z>vN-b(_0a%n|FMoWYT%u79Qzan$9}1qxW|0dW7gKhe1Bj4G+Tea_rT0wxTpZ3FnYK zf`%l1Gw@}%>Qh`(Q4p~nxsGE#-K$mP<0GSlEK4sFhYq zTDDS}WH^4-k4d!aMp{@bO8gPkj>kiYw2M+wf~7=Ms&v4|(DFVFy2MO!cgt=V2OAFK zt;9HV`b8rhkmgZ}sD_3&an{dQED*DCm?*@<#EGODNG`IOf(mS90lTrwucKzo{U=z; zs66}N%8u@s^>2CEMONa)w(75pa@JVhn;;b&d))XJ!k`j^HesnSmpkFiy0EH=5HEAd z#r_|cYkN-=wZztRc0F6pOO9JE#u>xvv)oxBXF=*$2zoW*m| z7aUFP2U$tEhID#Grnu0)5D3LZ<+lQPJ(vkkI zg{dRW$!xbTwd9X%te`j+wW#LdQv2$190Lce*oJ{ugku&|DALl{r9~ZbX<7??sq|nf zGlKB4AQpsx#FHeJ2_{fNme~eaGa}6~HKv_;Au#E=v~McLKoR7F1IgdW_Ut-~w>1>d)YWQIZhSj zuPD{AN@2g4u_TVDq%V`1p*!Q)I}tkhku7e5$0|{@MuOQrG8=~zY14&_425l&>06f)4enzIUPX^3h77EfRA#U4#&@18B(t+>$%Uot-+O zd280KT*E9UnFP;qnW%2e>d18y5O9Wi7DnB@i*P%)#dC&rKy_}TgCryg798SZb1MaL z&v#FQA7tCDzh9>dI`lijjrQML?Tzc5SP*$K<$M^h8XI^ny~?V>+zf~}(2)qKyP3`5$0*W(XZ zbV)qCwN~1;)mv?$DG(?!s;opUp66bG6R%4l>`ul-K6Of{18B%{cjPH2+MC%&E9F#2 zkaSUCyG1S#TZqt;F<@DGR#;MeT${d2UiL;(sU>s(HJvx-P7)Z-t)SRjX8{(lHKt@~kF62L#M^pcxE^07O|R4{yXDKjZu--TGg2{QY;H zXQH0EqV=-(=)z2Ky;P875N7;)a!FFzE^d=zp;SinNsn)|{1s)%{Msgm;sJn>0?w4_ zv};AQwv>`c)CyrxpUVR0R;?cdnAIwDy(2ciWnWdBKH`}Yn8-; zNxW<$EA?3!31IPuYMl)zd$c5@b~af7dWceoF~?flH_0;pyuqdke}BWv7!^+J>Bw%@P@n3aW?^5fMg>REjksja1Pf5Xb}=KqpkB=K!SWI^b%-=rk~Osckv9bB8(3 zDZ=8H6yb-y8=d#gTexG)30Whg{iUiZ*=b80qDh7+gfB=0+Mr@$f#pp02L(z{@xf0K zx#Qpl%IaXnOQ~pbf&_x}AOv@d)^b$ljnFbT7>4>(E|rojo4RV2m3Y8O0aIjd1_cl^ zleIy}imQk%s)W*$Esl`3SMOU(=u;)w6y+qOgL0!q9l_gVa_r?@s}`zSsRRAm^o6IT({N;Yo|HdU2_)M3I9-4gaAo=hOOgPuJVVF9*kOK@MHR zxF|b-^&z_ue4djq{)YKxMJ0N-*r#@u(&;A7w6-($*-yy6jKKLEEp# zaOyEEhjREOK$<9v=fEuZ=>Z+0@gW+DER<|TiKU{nuY+b2qu+QR zx8(-lk)%{d$C@6!|7>o^`}+Z;-*{+u@Qh*jH48$L&}@=X22ks;#DT}DedrJ`kmCB< z#frs4xHA6j-1h2lo7&Z62(V0h1c_n{gwU`CT;R^jlsWIO7AhcV!(wcUX*o$tB~mu* z;GD;s3iaf&6=k-yHSYMfrEhW(fT<`&H8QeEY>AUuj2bYuTWKwq9?0Gp{uU=5zbw+# zj>D*ePmkyJKBL`bT6}9YQE6&`pvz%f0>Bt4#15PjJ>m<4G!2*_Y`qR9Z3>N?hg)|o zNHB&R+@-D91)vZ@0|w>SSR3&*Fc3NPGjT@tcQBG86B%hXOJ>x>(yj1%_PxIZ>-ev8 z)u|hgH+0L_hn!YJWU^|^ff)$`Kr^ARgj~^04i9cWAO64K`VVpc^Wr_tjprvB| z%a>5LPpSMn{9cNJ{z>DAu5SIPq z*`$)bP75^=r42z;lE}7z2Fj&bRJFZ*RlG}Ah>G_wj-Am>s!$!^LoVR6G{D52$~$SE zkLRxc3+%|1=_8d!&2aWp9+aqt!p$WXgW zSW_b9aJ`Gf0Oix}S6fz_J~Xj}hP~2Y8&r-gGNgbj5K%aPYyl7vr2$6Ap_GUk!(o(U zkR%xvNeEJ^`4~BZ8gL9ZKo>tA*6c{;aqTKuMchDBrxKJpL!!h+LWJ4(^u0Q9?ygX& zCz%L{cH)jHa*TIdQ7?qMyuh`K!h~R}#gaMEH1X1ql%3 z3st;gBoZ`4N(9G`S>V29x7za?OdyuCQc~EY02i?@tN5RUy>(pEU-U3O6cI^5L_}aP zx+J7)q<}~ZQo;x6Mv#ULk(BOcG|~;yC?E~eF~%fDcaPYf`ThOA|2+RZ_qE-`Ge;m4;Kgi~YC{PhBGL)@ z(|(gEXDv=pW}N#}=2K2pNO-z-?56YLj|#q^u--Lbs&ouDCJjCEe36ZvR#_~BvXm<^ zE)1#eZ-z?%fCXH0=UiT2o6GE35dZm2t1@J0xG~**abjMeR9MNTB&?LtfN~RSQ+@w& zmel%rkcmUdD~gsFFB^bQ#&m|fM#K8C)gkp~SO*OTy3Im{a3j{jQqE{SJ&9y@Co_B^ z^QzhF&?e;T%&i{$mwq8%3r}lWOYB!gdDQPkC2$cSUz|hC{z64JUC>T+M-MNhp_?~Q zpZz=NE`5qgOMnduTY27PaeQ#w#d|z+W`?=5*Q|C z^cuXY^3L&^6s7m>ft{WXR~dGKQ6=MSnly;$F*NrJ(bqTMmEM8jThg%6fI45G^fj#L zGrp)I{&T=8!!E`0Fi=PSCbaYcL)4+O%j8D#F1YJL87p0TUXF=Y(*FrxO4_?Jb*N>L0hh+8ZF5; z{m9Gis*4W;Qfo#h@h`!>H_-O;ocb%Mq%?uN=I5)fygOv*rP5)2#!(xX7vgj)tvZ*pUdd)g|)7CD!N04C!}x4fa32cmJ@1ezT|_fT!a@THHQO+kh|4bbQ!sSvRI~l? z2557o8H_!=a3_=P+PMaHV_lAAV3%g};~<{!Iy7|T+76?MgkaZi`U9CRVFS0Qrx&I- z=g{z@2Z93tVqp330u*?RiQMXPSrt+M+g-zgulVZEk$HzkcEFSH??Ue{J48>hmnTs6 z8zAaxpJ68`=+%|X%~>aI<7n*a?W{vC2cQjGz6JGL3TTE0G-EPi-2`9Ypz11=I$1Y!C9h`nUeuwQp zfH9xtSeWH1OhZJr-K&3kM9M30)9b&=)?yoDRFAh}5&2ZeLP1FQ>-C#>nh0ybC&a{h z4|Fj~!6EWB8BDq)fw^y5Hz@UWl=M>A<^`!M6yY=>)t9XnZpnWQ+}Iy$u_Y)a5C#q0 zCmf^*45nLl!zERsvZClURQkWX{mt#Ypf%dh#Ka#-^_3*viZDFzRBCV(5FTOthDUE{ zX_KJ!-lu3a9tFW_mZU%RPb9tx_zTz-GiGfvZpd$Kc=y_fBZjla(V!QVS1G73 z!1_h8nvc=<&Uba8}#krMqByS#&c?zNiUD z%AAFT{fltjlm`@16cj!L55Cf?;b$x{HY|oxza*hDd~DIXQs5mkWS$=w=Tow%{5BGyFD$hPW z){V6N)@~14Z{g19d&ulK+fr0lO7+-kY(PH~G0imyvdwHYL5pg!dO#2)qWL&^N~7a zJ`(#v`bbAnDS_vMhx2ZGicf3$BloZF4Q#VR>jRO(CV!PmYmF+|?7vI)$5MG8Boc79 zeO8I~emS|O^O4tFn^O6af&P4wy84q3?|yu*YgW+iC78i)qM{*_OJ(y8S>yW2twB=6 zTM5z$7p5-CJ=Jhq)_Y#=_#jA_)p^7<{3l&1Me(A1L6K}zgqmDR=rpUa?t-zG+tofftixI9z^-oB|1SJffMus+(HqG#seSH~BMkr?)`0WosK1|H5@>#O|2lXey zN<{m?rCCWzeH9)rt&5vq;w#%ZrwP@(zOP6ZG80b+HwF-aN zkIlv$YNwS2_4w?#!GiZvtY-O;ZH>VBFV z>A#R4Efp-}wp6z*P4_-pddp1DEtJ*!OGAT-@>hCUZL7Q@6M-mO<7}_J<%|KR_6zko z&N#g!XGd-LGd*pQ;vTss`DCtH{UPJBR9dEDHQJO%*?HN<6yZVciDW@_`mY;#yoVpq z5PdMO3K96O!Z`bBx&a(JoDb@?9w~M6zh#z(1e3pl3HVGgWo1c6X@eIv>oj@+)X|aS5SWw z6ij_io$^Jy;5iM)*Ly`C`OM!q@>c{y3-eXKQ(5T1iwXX`YfftaBJcW&jh08TM`wo0 z$c~AhVq-NZrdq%`vJ$^8zv4~O`mc+5l;96s zqd8{ZeAkK8u}u}RrlFiDqBZ!SMsMwo&&Eq?#H{spei>8Of3%H(A2q4MIoB7eYHw0AtECx${>-HxHkW3I{#P3 zDSm!iW1nBg_fsFz0P5nNxav`8|5s+OrSpeVm4P1~_xO(^K#nu%qp+Z)xHX5~ptd6G zACd#x;`WfVwWqw=$|{P*N~7Ej)RF&DYm0Jf81eur$zD1&OHz{<1}9CF`p9`R^%k;| zK6~1fE*STbEag|L{2s>w$3_aC&blgzqOCd$$13Sh#ieelK{7Q%!@|itV!1D}Wle^F z{E$bgtm=-a^$dLO;cPj8h)8hY;*f#->>wlPaWbC>6H|h`c;T1l-yR#tmNIh4Bfcfj z6|rdHQ9ODOZ}@=lBc1u#^4&x~dC8RvdecWbw&)J)izbgoA4W|vrIuf|-DN=8W8-Zs z!9Hlsp@8v8Xce~Cbr6->cmdPhJwpST*TpPPfOZ!^^9zR4eaPho)clAk9@4dV1Kz`o zn%Ueuv`XE=ez*l90;-q%JyEGESlw%^%bl6cxtSz(?Su|;IJJ%0gkuyI9qNE#7iQac zw4KO(3~cz+^*^3)^4Y1sSmL$29xTR{oRKhW2U~qRj+?+fhY1`$r=Yku)uq`1F23ap z-mAMZo540A+0S6_&#D0LFT38~#^h{HVEIP>l22mQ#?P@6Xl&jICb9dDeG?jTitWAV z^gA0H=4->apu6TT!IdXBVwTd~#As~#jq>rfIBBtxwOyLsw2lH?RBZs!SJM71R z4=^PsX4O~VH@9^cSo0f~;-f1W=%$$VRKA?xNE!B=VxQbV?l3U>b0b^W`JLwIEimt# zp?UR{Ip}P~YWp$thaX1M z3pHm?&7N&^fi)*DBy3gBkpc%fJx5YN@OIY=Sxf?ei8TArXCLE2{{1@E9zCi_eh`oe z$5?E(V)KSIC-2D1v18bYJ7Bn0qF*UjOsDSRM1FSfP`~Z@AgG{}nBQTA{@i~s^zmB+0iE4QaR5~*o z!e9};%MHu7ftb1SW3%RK@&$hw99nY*zP>$7yK@meGSl9B#h`Uv-E)S(dfy_)FmL}% zt+Zi=kv+GPor!m%lbm9A&J6CG-S6&SWBWERf;&XtXsh!v;o=Idp6!l@?e0y}>- ze&c*|*m=`&|1bcIzFD4I@qy}Q4#}8RT*2%w#*@(|iQ>%1(7pqP)V)r%J;*C*7G^eu z_3^__(OszCg;~Dq@EVl-5G!y4`*KbS2pHe!TDifF2Q)(C_QGcUVagNSD^Oi*_3C%4 zTeCup=#g)1(?u2xq|$YArG_zExIxxiZoHK=%RF5|3f};k(ik$=pe5&>??;c^?yvdJ z{c*pw8M}Rtu*+kllZFmKV|T$`{l}+5%u;60CNRzO*X>YJ3Sd`IB&amc*5~ll=fk1u zR>!l_@mwXj*g;71LBPXq|JWhOI4^MST?E3Ox;IXXSGEx}9oH23F4N%Kl#^0rv{L0$ zr3!nFrf+X?nTpXAh4D|cY&lpGR49@HQoHxAk%2c*EQB4mEsxOo4i`8SR9IR5W=5q> zOZ!nhl_3-Lb0DxQo2Y?Dd1i053hzD~kSOr`^RHFLRO{fLX3HmMzps|j-^8X*?vrgrT z|DQ@{)zZH!E?K~19bbaLJXtE8d`0O%X@JLLyf1h&pYvF$W;5(@q_b-)0?7jTKFy5T zgtun8J2$YkZiZE{+GTb$J3FE<5XVwk5I zkVXV90YrK_`X&AldqkGDHkYXpcp0|3aSNcEU!Xb1PY0krpG zp56-$e3<10u-$YHytm0v4WQq<4D1Hrm&)RQQ5gC_(0USd>?TVDu>G#Zg0ID@!U(|A zd4LDN3#`O3avAkmY+ zX2^A!AK$6mX`O==C&;y=9|LIdasdF+|7iyApA@)qPj~Q*>J#})w$i$SNxO{L>7LiR z+Qrf2T7U3zAATo@3MIrL;l%*}wt)Mxfcs2wxVCU;wxj?`-y)U_swuXD8EN)1OCd{A zTHC4Dp&0-G?LWHzYJxz(KcxRH_gEAO@8MJ6D@^|^ji!kxE@rWgSSNkXlsgf3!ibl* zBlPDFPGB4oj!V4Uss9g=DEUnSi7Y{=It^3tS9PI*QHIh+@Vj28$94^XDRokzX}j%L zoYCxw_MsURP^Yykj+Vv&+4?fGJ+=!ic!b67l$Cin62P51 z?>jZm{wD(WsvuZ!4ZlYedmayX@P3a9pPKQ$>+j!V00MaRCGyolr$fytY;I=$g;Ry$ zOTM39j$vk76(8B9J@@+A&q>1;(Z(tOfGXR)^`75X)c{$RXSsNci6%~2UY|?>xOA4J zpnrAp%+T(-ab3+d*ku8#QsSqo2!bpU+%@?AR%VIf8lf7Ef>LHAC!)y$N@(c25_KtU ze@(ytlEtEV8%$2vL!h6a8vP~pIPI@{#CO?ZSig{k$eXmNZ7wR7U8y1wx2QqOfjarH z^O<*K)^EG!hOAiBom9sP3$Z;0PmeK5JBn}l?3of6gbLNJ2`wI|{Cr4ZLn_5d%(Exo z7~G3QZ)TGgkm+F}(KqgEK|@m;p%bLndvj}#*wR1E3Rd7T+1#gdr72J1jp$87(I}~V zdQh4gik|U>u(y8ZnB0<6R3c4SbC9c7ljshHab~`$evu);`%P%}PxmNpQc_%_mlZ^( z!?DT1!S(vNKsco#=pY6(9k8uBM9~TQF;sMqO^_JxyOc>p0hH!<@o6-;ru18#l@W#T ze(*}C_j$*n?;0%~1s{nvk=|nr1UL_5%gS%J8O^cuyPU`c-WP+)SCyy%uIYwIIa9Vr zk_1LS#&su@bQezZ9|^TQ)WL)y=;z?$qrG)1kM4~LeQ+he2 zs*Aq6MqA74iZlMMHD0QDQ%MG$&m)$pO|lVYmy~)6PG|RFqYyjt9hYY3qH_D?B>1HJ zPw4vRB+)moubk~H{z4~)T!6GE(FL^c)@CTd{lOX?ncfO~z*nPCzCvj7F8O4zX)7wI zDZ9XBec?w{n|g$g!>!&F+HD)`ReX9hu77TmA}!E`mi{q^)*B{AU1T1WwAK2lc?$4I zc?{7|_BVE=!-1$>*jry?$_}$8@T9`W2v>|fRM=J!8J1uAR>Ea2GO_8I2JJamOTbvF;lwO35x!tU7ZSM{R99JG=kmN5e_RpBEd^z)M zIG?>P41-{0{D-|P#i8P9DYq7Z!>OPA9mtlrvIcB(l*pMcnMnI!gLPcW>X((=+*vApXc?#a6=2BawSiQ zx^Gl}0jz&(@xs#wstG)hvECkjLp}z}x?#mfB1k*PW;cf{riME?buW4*g1mfgTrTvV z{JfR%Q8Q^~`BA(%asI2lI>~Ayx0*U72E#UwfWc-Q1rKpX#=*?DS<&^A!m zS`U3&?NYscVJwnq9=gD+oGP3WXZy?0HzPyst2s1Q=2qGZelO?a2se!VWOz$K=V=kD zP`u~Q%^Oe_Thi#6$=P_J2MRoO0Edd7Y@ejslr8%4 zb~i)PN&P%JpQD@Ix*{*S{n~GpSBRbGq+NFVxhzm6n=khsLZ(lF1wCQ(&!zOmolsrz z2d&Rh+epgoFlRu?loU$dQ8(d*JT6gmd+29sMMSMJylsY)T*Nc>gXDLZ2F-LQ%>nPT zsba|9e{HRP0gdAf$rK-IWU!LewVGo;yh;LYB~07#OYqfh3ZgDukh6c}GyGm<*SLi_ zKkOl$=i|uqJ1+LT>2Wh(DfE|Kg}YiXCd`>!W%KWU#c2OsBA=+tQ5X;YR0U3s@)y6* zJw`)}Pi`P>5Qe2&;+c!HYuyNRcnNGg^&Z3TJ

9O|1ju@f&k4^zL@>in)5;I=b~> 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

8wNgYRkz|rWk#m%!*OU?C0#PCo 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 literal 0 HcmV?d00001 From 5b873131e5297cf5042ff1f4e387f266153b5ac8 Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 21:37:35 +0100 Subject: [PATCH 19/23] Introduce process_till_header --- rc-zip/src/fsm/entry/mod.rs | 119 ++++++++++++++++++++---------------- 1 file changed, 65 insertions(+), 54 deletions(-) diff --git a/rc-zip/src/fsm/entry/mod.rs b/rc-zip/src/fsm/entry/mod.rs index 51d0c4d..b9e6f35 100644 --- a/rc-zip/src/fsm/entry/mod.rs +++ b/rc-zip/src/fsm/entry/mod.rs @@ -42,9 +42,6 @@ enum State { ReadLocalHeader, ReadData { - /// The entry metadata - entry: Entry, - /// Whether the entry has a data descriptor has_data_descriptor: bool, @@ -65,9 +62,6 @@ enum State { }, ReadDataDescriptor { - /// The entry metadata - entry: Entry, - /// Whether the entry is zip64 (because its compressed size or uncompressed size is u32::MAX) is_zip64: bool, @@ -76,9 +70,6 @@ enum State { }, Validate { - /// The entry metadata - entry: Entry, - /// Size we've decompressed + crc32 hash we've computed metrics: EntryReadMetrics, @@ -125,9 +116,58 @@ impl EntryFsm { } } - /// Like `process`, but only processes the header: - pub fn process_header_only(&mut self) -> Option<&LocalFileHeader> { - todo!() + /// Like `process`, but only processes the header. If this returns + /// `Ok(None)`, the caller should read more data and call this function + /// again. + pub fn process_till_header(&mut self) -> Result, Error> { + match &self.state { + State::ReadLocalHeader => { + self.internal_process_local_header()?; + } + _ => { + // already good + } + } + + // this will be non-nil if we've parsed the local header, otherwise, + Ok(self.entry.as_ref()) + } + + fn internal_process_local_header(&mut self) -> Result { + assert!( + matches!(self.state, State::ReadLocalHeader), + "internal_process_local_header called in wrong state", + ); + + let mut input = Partial::new(self.buffer.data()); + match LocalFileHeader::parser.parse_next(&mut input) { + Ok(header) => { + let consumed = input.as_bytes().offset_from(&self.buffer.data()); + tracing::trace!(local_file_header = ?header, consumed, "parsed local file header"); + let decompressor = AnyDecompressor::new( + header.method, + self.entry.as_ref().map(|entry| entry.uncompressed_size), + )?; + + if self.entry.is_none() { + self.entry = Some(header.as_entry()?); + } + + self.state = State::ReadData { + is_zip64: header.compressed_size == u32::MAX + || header.uncompressed_size == u32::MAX, + has_data_descriptor: header.has_data_descriptor(), + compressed_bytes: 0, + uncompressed_bytes: 0, + hasher: crc32fast::Hasher::new(), + decompressor, + }; + self.buffer.consume(consumed); + Ok(true) + } + Err(ErrMode::Incomplete(_)) => Ok(false), + Err(_e) => Err(Error::Format(FormatError::InvalidLocalHeader)), + } } /// Process the input and write the output to the given buffer @@ -158,40 +198,10 @@ impl EntryFsm { use State as S; match &mut self.state { S::ReadLocalHeader => { - let mut input = Partial::new(self.buffer.data()); - match LocalFileHeader::parser.parse_next(&mut input) { - Ok(header) => { - let consumed = input.as_bytes().offset_from(&self.buffer.data()); - tracing::trace!(local_file_header = ?header, consumed, "parsed local file header"); - let decompressor = AnyDecompressor::new( - header.method, - self.entry.as_ref().map(|entry| entry.uncompressed_size), - )?; - - self.state = S::ReadData { - entry: match &self.entry { - Some(entry) => entry.clone(), - None => header.as_entry()?, - }, - is_zip64: header.compressed_size == u32::MAX - || header.uncompressed_size == u32::MAX, - has_data_descriptor: header.has_data_descriptor(), - compressed_bytes: 0, - uncompressed_bytes: 0, - hasher: crc32fast::Hasher::new(), - decompressor, - }; - self.buffer.consume(consumed); - self.process(out) - } - Err(ErrMode::Incomplete(_)) => { - Ok(FsmResult::Continue((self, Default::default()))) - } - Err(_e) => Err(Error::Format(FormatError::InvalidLocalHeader)), - } + self.internal_process_local_header()?; + self.process(out) } S::ReadData { - entry, compressed_bytes, uncompressed_bytes, hasher, @@ -202,6 +212,7 @@ impl EntryFsm { // don't feed the decompressor bytes beyond the entry's compressed size + let entry = self.entry.as_ref().unwrap(); let in_buf_max_len = cmp::min( in_buf.len(), entry.compressed_size as usize - *compressed_bytes as usize, @@ -237,16 +248,16 @@ impl EntryFsm { if outcome.bytes_written == 0 && self.eof { // we're done, let's read the data descriptor (if there's one) - transition!(self.state => (S::ReadData { entry, has_data_descriptor, is_zip64, uncompressed_bytes, hasher, .. }) { + transition!(self.state => (S::ReadData { has_data_descriptor, is_zip64, uncompressed_bytes, hasher, .. }) { let metrics = EntryReadMetrics { uncompressed_size: uncompressed_bytes, crc32: hasher.finalize(), }; if has_data_descriptor { - S::ReadDataDescriptor { entry, metrics, is_zip64 } + S::ReadDataDescriptor { metrics, is_zip64 } } else { - S::Validate { entry, metrics, descriptor: None } + S::Validate { metrics, descriptor: None } } }); return self.process(out); @@ -273,8 +284,8 @@ impl EntryFsm { self.buffer .consume(input.as_bytes().offset_from(&self.buffer.data())); trace!("data descriptor = {:#?}", descriptor); - transition!(self.state => (S::ReadDataDescriptor { metrics, entry, .. }) { - S::Validate { entry, metrics, descriptor: Some(descriptor) } + transition!(self.state => (S::ReadDataDescriptor { metrics, .. }) { + S::Validate { metrics, descriptor: Some(descriptor) } }); self.process(out) } @@ -285,17 +296,17 @@ impl EntryFsm { } } S::Validate { - entry, metrics, descriptor, } => { - let entry_crc32 = self.entry.as_ref().map(|e| e.crc32).unwrap_or_default(); - let expected_crc32 = if entry_crc32 != 0 { - entry_crc32 + let entry = self.entry.as_ref().unwrap(); + + let expected_crc32 = if entry.crc32 != 0 { + entry.crc32 } else if let Some(descriptor) = descriptor.as_ref() { descriptor.crc32 } else { - entry.crc32 + 0 }; if entry.uncompressed_size != metrics.uncompressed_size { From d0a9b8c6a2470ec4d7d4c0b5699df3161bae56da Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 21:59:11 +0100 Subject: [PATCH 20/23] jean unzip-streaming works properly on sample file --- rc-zip-sync/examples/jean.rs | 3 +- rc-zip-sync/src/entry_reader.rs | 2 +- rc-zip-sync/src/read_zip.rs | 34 +++---- rc-zip-sync/src/streaming_entry_reader.rs | 106 +++++++++++++--------- rc-zip-tokio/src/entry_reader.rs | 2 +- rc-zip/src/fsm/entry/mod.rs | 26 +++--- 6 files changed, 96 insertions(+), 77 deletions(-) diff --git a/rc-zip-sync/examples/jean.rs b/rc-zip-sync/examples/jean.rs index 5df6ad6..a6f3a86 100644 --- a/rc-zip-sync/examples/jean.rs +++ b/rc-zip-sync/examples/jean.rs @@ -380,9 +380,8 @@ fn do_main(cli: Cli) -> Result<(), Box> { } } - match entry_reader.finish() { + match entry_reader.finish()? { Some(next_entry) => { - println!("Found next entry!"); entry_reader = next_entry; } None => { diff --git a/rc-zip-sync/src/entry_reader.rs b/rc-zip-sync/src/entry_reader.rs index 5a276f6..89b25c5 100644 --- a/rc-zip-sync/src/entry_reader.rs +++ b/rc-zip-sync/src/entry_reader.rs @@ -20,7 +20,7 @@ where pub(crate) fn new(entry: &Entry, rd: R) -> Self { Self { rd, - fsm: Some(EntryFsm::new(Some(entry.clone()))), + fsm: Some(EntryFsm::new(Some(entry.clone()), None)), } } } diff --git a/rc-zip-sync/src/read_zip.rs b/rc-zip-sync/src/read_zip.rs index 06d5217..43ab8a3 100644 --- a/rc-zip-sync/src/read_zip.rs +++ b/rc-zip-sync/src/read_zip.rs @@ -1,11 +1,10 @@ -use rc_zip::parse::Entry; use rc_zip::{ error::Error, fsm::{ArchiveFsm, FsmResult}, - parse::{Archive, LocalFileHeader}, + parse::Archive, }; +use rc_zip::{fsm::EntryFsm, parse::Entry}; use tracing::trace; -use winnow::{error::ErrMode, Parser, Partial}; use crate::entry_reader::EntryReader; use crate::streaming_entry_reader::StreamingEntryReader; @@ -244,24 +243,19 @@ where R: Read, { fn read_first_zip_entry_streaming(mut self) -> Result, Error> { - let mut buf = oval::Buffer::with_capacity(16 * 1024); - let entry = loop { - let n = self.read(buf.space())?; - trace!("read {} bytes into buf for first zip entry", n); - buf.fill(n); - - let mut input = Partial::new(buf.data()); - match LocalFileHeader::parser.parse_next(&mut input) { - Ok(header) => { - break header.as_entry()?; - } - Err(ErrMode::Incomplete(_)) => continue, - Err(e) => { - panic!("{e}") - } + let mut fsm = EntryFsm::new(None, None); + + loop { + if fsm.wants_read() { + let n = self.read(fsm.space())?; + trace!("read {} bytes into buf for first zip entry", n); + fsm.fill(n); } - }; - Ok(StreamingEntryReader::new(buf, entry, self)) + if let Some(entry) = fsm.process_till_header()? { + let entry = entry.clone(); + return Ok(StreamingEntryReader::new(fsm, entry, self)); + } + } } } diff --git a/rc-zip-sync/src/streaming_entry_reader.rs b/rc-zip-sync/src/streaming_entry_reader.rs index d7e1c53..d4eaf9a 100644 --- a/rc-zip-sync/src/streaming_entry_reader.rs +++ b/rc-zip-sync/src/streaming_entry_reader.rs @@ -1,9 +1,10 @@ use oval::Buffer; use rc_zip::{ + error::{Error, FormatError}, fsm::{EntryFsm, FsmResult}, parse::Entry, }; -use std::io::{self, Write}; +use std::io::{self, Read}; use tracing::trace; /// Reads a zip entry based on a local header. Some information is missing, @@ -21,7 +22,6 @@ pub struct StreamingEntryReader { #[allow(clippy::large_enum_variant)] enum State { Reading { - remain: Buffer, fsm: EntryFsm, }, Finished { @@ -36,14 +36,11 @@ impl StreamingEntryReader where R: io::Read, { - pub(crate) fn new(remain: Buffer, entry: Entry, rd: R) -> Self { + pub(crate) fn new(fsm: EntryFsm, entry: Entry, rd: R) -> Self { Self { - rd, entry, - state: State::Reading { - remain, - fsm: EntryFsm::new(None), - }, + rd, + state: State::Reading { fsm }, } } } @@ -53,55 +50,39 @@ where R: io::Read, { fn read(&mut self, buf: &mut [u8]) -> io::Result { + trace!("reading from streaming entry reader"); + match std::mem::take(&mut self.state) { - State::Reading { - mut remain, - mut fsm, - } => { + State::Reading { mut fsm } => { if fsm.wants_read() { trace!("fsm wants read"); - if remain.available_data() > 0 { - trace!( - "remain has {} data bytes available", - remain.available_data(), - ); - let n = remain.read(fsm.space())?; - trace!("giving fsm {} bytes from remain", n); - fsm.fill(n); - } else { - let n = self.rd.read(fsm.space())?; - trace!("giving fsm {} bytes from rd", n); - fsm.fill(n); - } + let n = self.rd.read(fsm.space())?; + trace!("giving fsm {} bytes from rd", n); + fsm.fill(n); } else { trace!("fsm does not want read"); } match fsm.process(buf)? { FsmResult::Continue((fsm, outcome)) => { - self.state = State::Reading { remain, fsm }; + trace!("fsm wants to continue"); + self.state = State::Reading { fsm }; if outcome.bytes_written > 0 { + trace!("bytes have been written"); Ok(outcome.bytes_written) } else if outcome.bytes_read == 0 { + trace!("no bytes have been written or read"); // that's EOF, baby! Ok(0) } else { + trace!("read some bytes, hopefully will write more later"); // loop, it happens self.read(buf) } } - FsmResult::Done(mut fsm_remain) => { - // if our remain still has remaining data, it goes after - // what the fsm just gave back - if remain.available_data() > 0 { - fsm_remain.grow(fsm_remain.capacity() + remain.available_data()); - fsm_remain.write_all(remain.data())?; - drop(remain) - } - - // FIXME: read the next local file header here - self.state = State::Finished { remain: fsm_remain }; + FsmResult::Done(remain) => { + self.state = State::Finished { remain }; // neat! Ok(0) @@ -118,7 +99,10 @@ where } } -impl StreamingEntryReader { +impl StreamingEntryReader +where + R: io::Read, +{ /// Return entry information for this reader #[inline(always)] pub fn entry(&self) -> &Entry { @@ -127,13 +111,51 @@ impl StreamingEntryReader { /// Finish reading this entry, returning the next streaming entry reader, if /// any. This panics if the entry is not fully read. - pub fn finish(self) -> Option> { + /// + /// If this returns None, there's no entries left. + pub fn finish(mut self) -> Result>, Error> { + trace!("finishing streaming entry reader"); + match self.state { State::Reading { .. } => { - panic!("finish called before entry is fully read") + // if there's no data left, we're okay + trace!("trying to finish entry reader"); + if self.read(&mut [0u8; 1])? == 0 && matches!(&self.state, State::Finished { .. }) { + // we're done + self.finish() + } else { + panic!("entry not fully read"); + } } - State::Finished { .. } => { - todo!("read local file header for next entry") + State::Finished { remain } => { + // parse the next entry, if any + let mut fsm = EntryFsm::new(None, Some(remain)); + + loop { + if fsm.wants_read() { + let n = self.rd.read(fsm.space())?; + trace!("read {} bytes into buf for first zip entry", n); + fsm.fill(n); + } + + match fsm.process_till_header() { + Ok(Some(entry)) => { + let entry = entry.clone(); + return Ok(Some(StreamingEntryReader::new(fsm, entry, self.rd))); + } + Ok(None) => { + // needs more turns + } + Err(e) => match e { + Error::Format(FormatError::InvalidLocalHeader) => { + // we probably reached the end of central directory! + // TODO: we should probably check for the end of central directory + return Ok(None); + } + _ => return Err(e), + }, + } + } } State::Transition => unreachable!(), } diff --git a/rc-zip-tokio/src/entry_reader.rs b/rc-zip-tokio/src/entry_reader.rs index ce18641..2b24e9c 100644 --- a/rc-zip-tokio/src/entry_reader.rs +++ b/rc-zip-tokio/src/entry_reader.rs @@ -28,7 +28,7 @@ where { Self { rd: get_reader(entry.header_offset), - fsm: Some(EntryFsm::new(Some(entry.clone()))), + fsm: Some(EntryFsm::new(Some(entry.clone()), None)), } } } diff --git a/rc-zip/src/fsm/entry/mod.rs b/rc-zip/src/fsm/entry/mod.rs index b9e6f35..b46f42f 100644 --- a/rc-zip/src/fsm/entry/mod.rs +++ b/rc-zip/src/fsm/entry/mod.rs @@ -86,17 +86,23 @@ pub struct EntryFsm { state: State, entry: Option, buffer: Buffer, - eof: bool, } impl EntryFsm { /// Create a new state machine for decompressing a zip entry - pub fn new(entry: Option) -> Self { + pub fn new(entry: Option, buffer: Option) -> Self { + const BUF_CAPACITY: usize = 256 * 1024; + Self { state: State::ReadLocalHeader, entry, - buffer: Buffer::with_capacity(256 * 1024), - eof: false, + buffer: match buffer { + Some(buffer) => { + assert!(buffer.capacity() >= BUF_CAPACITY, "buffer too small"); + buffer + } + None => Buffer::with_capacity(BUF_CAPACITY), + }, } } @@ -240,13 +246,14 @@ impl EntryFsm { ?outcome, compressed_bytes = *compressed_bytes, uncompressed_bytes = *uncompressed_bytes, - eof = self.eof, "decompressed" ); self.buffer.consume(outcome.bytes_read); *compressed_bytes += outcome.bytes_read as u64; - if outcome.bytes_written == 0 && self.eof { + if outcome.bytes_written == 0 && *compressed_bytes == entry.compressed_size { + trace!("eof and no bytes written, we're done"); + // we're done, let's read the data descriptor (if there's one) transition!(self.state => (S::ReadData { has_data_descriptor, is_zip64, uncompressed_bytes, hasher, .. }) { let metrics = EntryReadMetrics { @@ -255,8 +262,10 @@ impl EntryFsm { }; if has_data_descriptor { + trace!("transitioning to ReadDataDescriptor"); S::ReadDataDescriptor { metrics, is_zip64 } } else { + trace!("transitioning to Validate"); S::Validate { metrics, descriptor: None } } }); @@ -344,13 +353,8 @@ impl EntryFsm { /// After having written data to [Self::space], call this to indicate how /// many bytes were written. - /// - /// If this is called with zero, it indicates eof #[inline] pub fn fill(&mut self, count: usize) -> usize { - if count == 0 { - self.eof = true; - } self.buffer.fill(count) } } From 99d286265dcc3875ac654a22e4d7c62eddc6e58f Mon Sep 17 00:00:00 2001 From: Amos Wenger Date: Mon, 5 Feb 2024 22:17:22 +0100 Subject: [PATCH 21/23] Add streaming zip reader to rc-zip-tokio as well --- rc-zip-sync/src/lib.rs | 2 +- rc-zip-sync/src/read_zip.rs | 34 ++-- rc-zip-sync/src/streaming_entry_reader.rs | 16 +- rc-zip-sync/tests/integration_tests.rs | 4 +- rc-zip-tokio/src/entry_reader.rs | 2 + rc-zip-tokio/src/lib.rs | 11 +- .../src/{async_read_zip.rs => read_zip.rs} | 124 ++++++++---- rc-zip-tokio/src/streaming_entry_reader.rs | 179 ++++++++++++++++++ rc-zip-tokio/tests/integration_tests.rs | 10 +- 9 files changed, 302 insertions(+), 80 deletions(-) rename rc-zip-tokio/src/{async_read_zip.rs => read_zip.rs} (67%) create mode 100644 rc-zip-tokio/src/streaming_entry_reader.rs diff --git a/rc-zip-sync/src/lib.rs b/rc-zip-sync/src/lib.rs index 5c295fb..98e0ddf 100644 --- a/rc-zip-sync/src/lib.rs +++ b/rc-zip-sync/src/lib.rs @@ -16,5 +16,5 @@ pub use streaming_entry_reader::StreamingEntryReader; // re-exports pub use rc_zip; pub use read_zip::{ - HasCursor, ReadZip, ReadZipEntriesStreaming, ReadZipWithSize, SyncArchive, SyncEntry, + ArchiveHandle, EntryHandle, HasCursor, ReadZip, ReadZipEntriesStreaming, ReadZipWithSize, }; diff --git a/rc-zip-sync/src/read_zip.rs b/rc-zip-sync/src/read_zip.rs index 43ab8a3..771b69d 100644 --- a/rc-zip-sync/src/read_zip.rs +++ b/rc-zip-sync/src/read_zip.rs @@ -18,7 +18,7 @@ pub trait ReadZipWithSize { type File: HasCursor; /// Reads self as a zip archive. - fn read_zip_with_size(&self, size: u64) -> Result, Error>; + fn read_zip_with_size(&self, size: u64) -> Result, Error>; } /// A trait for reading something as a zip archive when we can tell size from @@ -30,7 +30,7 @@ pub trait ReadZip { type File: HasCursor; /// Reads self as a zip archive. - fn read_zip(&self) -> Result, Error>; + fn read_zip(&self) -> Result, Error>; } impl ReadZipWithSize for F @@ -39,7 +39,7 @@ where { type File = F; - fn read_zip_with_size(&self, size: u64) -> Result, Error> { + fn read_zip_with_size(&self, size: u64) -> Result, Error> { trace!(%size, "read_zip_with_size"); let mut fsm = ArchiveFsm::new(size); loop { @@ -60,7 +60,7 @@ where fsm = match fsm.process()? { FsmResult::Done(archive) => { trace!("read_zip_with_size: done"); - return Ok(SyncArchive { + return Ok(ArchiveHandle { file: self, archive, }); @@ -74,7 +74,7 @@ where impl ReadZip for &[u8] { type File = Self; - fn read_zip(&self) -> Result, Error> { + fn read_zip(&self) -> Result, Error> { self.read_zip_with_size(self.len() as u64) } } @@ -82,7 +82,7 @@ impl ReadZip for &[u8] { impl ReadZip for Vec { type File = Self; - fn read_zip(&self) -> Result, Error> { + fn read_zip(&self) -> Result, Error> { self.read_zip_with_size(self.len() as u64) } } @@ -92,7 +92,7 @@ impl ReadZip for Vec { /// This only contains metadata for the archive and its entries. Separate /// readers can be created for arbitraries entries on-demand using /// [SyncEntry::reader]. -pub struct SyncArchive<'a, F> +pub struct ArchiveHandle<'a, F> where F: HasCursor, { @@ -100,7 +100,7 @@ where archive: Archive, } -impl Deref for SyncArchive<'_, F> +impl Deref for ArchiveHandle<'_, F> where F: HasCursor, { @@ -111,13 +111,13 @@ where } } -impl SyncArchive<'_, F> +impl ArchiveHandle<'_, F> where F: HasCursor, { /// Iterate over all files in this zip, read from the central directory. - pub fn entries(&self) -> impl Iterator> { - self.archive.entries().map(move |entry| SyncEntry { + pub fn entries(&self) -> impl Iterator> { + self.archive.entries().map(move |entry| EntryHandle { file: self.file, entry, }) @@ -125,11 +125,11 @@ where /// Attempts to look up an entry by name. This is usually a bad idea, /// as names aren't necessarily normalized in zip archives. - pub fn by_name>(&self, name: N) -> Option> { + pub fn by_name>(&self, name: N) -> Option> { self.archive .entries() .find(|&x| x.name == name.as_ref()) - .map(|entry| SyncEntry { + .map(|entry| EntryHandle { file: self.file, entry, }) @@ -137,12 +137,12 @@ where } /// A zip entry, read synchronously from a file or other I/O resource. -pub struct SyncEntry<'a, F> { +pub struct EntryHandle<'a, F> { file: &'a F, entry: &'a Entry, } -impl Deref for SyncEntry<'_, F> { +impl Deref for EntryHandle<'_, F> { type Target = Entry; fn deref(&self) -> &Self::Target { @@ -150,7 +150,7 @@ impl Deref for SyncEntry<'_, F> { } } -impl<'a, F> SyncEntry<'a, F> +impl<'a, F> EntryHandle<'a, F> where F: HasCursor, { @@ -213,7 +213,7 @@ impl HasCursor for std::fs::File { impl ReadZip for std::fs::File { type File = Self; - fn read_zip(&self) -> Result, Error> { + fn read_zip(&self) -> Result, Error> { let size = self.metadata()?.len(); self.read_zip_with_size(size) } diff --git a/rc-zip-sync/src/streaming_entry_reader.rs b/rc-zip-sync/src/streaming_entry_reader.rs index d4eaf9a..325731a 100644 --- a/rc-zip-sync/src/streaming_entry_reader.rs +++ b/rc-zip-sync/src/streaming_entry_reader.rs @@ -90,7 +90,7 @@ where } } State::Finished { remain } => { - // wait for them to call finished + // wait for them to call finish self.state = State::Finished { remain }; Ok(0) } @@ -116,16 +116,14 @@ where pub fn finish(mut self) -> Result>, Error> { trace!("finishing streaming entry reader"); + if matches!(self.state, State::Reading { .. }) { + // this should transition to finished if there's no data + _ = self.read(&mut [0u8; 1])?; + } + match self.state { State::Reading { .. } => { - // if there's no data left, we're okay - trace!("trying to finish entry reader"); - if self.read(&mut [0u8; 1])? == 0 && matches!(&self.state, State::Finished { .. }) { - // we're done - self.finish() - } else { - panic!("entry not fully read"); - } + panic!("entry not fully read"); } State::Finished { remain } => { // parse the next entry, if any diff --git a/rc-zip-sync/tests/integration_tests.rs b/rc-zip-sync/tests/integration_tests.rs index 7b22513..2efdc1c 100644 --- a/rc-zip-sync/tests/integration_tests.rs +++ b/rc-zip-sync/tests/integration_tests.rs @@ -3,11 +3,11 @@ use rc_zip::{ error::Error, parse::Archive, }; -use rc_zip_sync::{HasCursor, ReadZip, SyncArchive}; +use rc_zip_sync::{ArchiveHandle, HasCursor, ReadZip}; use std::fs::File; -fn check_case(test: &Case, archive: Result, Error>) { +fn check_case(test: &Case, archive: Result, Error>) { corpus::check_case(test, archive.as_ref().map(|ar| -> &Archive { ar })); let archive = match archive { Ok(archive) => archive, diff --git a/rc-zip-tokio/src/entry_reader.rs b/rc-zip-tokio/src/entry_reader.rs index 2b24e9c..094da2a 100644 --- a/rc-zip-tokio/src/entry_reader.rs +++ b/rc-zip-tokio/src/entry_reader.rs @@ -73,6 +73,8 @@ where if outcome.bytes_written > 0 { tracing::trace!("wrote {} bytes", outcome.bytes_written); buf.advance(outcome.bytes_written); + } else if outcome.bytes_read == 0 { + // that's EOF, baby! } else { // loop, it happens return self.poll_read(cx, buf); diff --git a/rc-zip-tokio/src/lib.rs b/rc-zip-tokio/src/lib.rs index 61d4d5b..75c506d 100644 --- a/rc-zip-tokio/src/lib.rs +++ b/rc-zip-tokio/src/lib.rs @@ -7,11 +7,14 @@ #![warn(missing_docs)] -mod async_read_zip; mod entry_reader; +mod read_zip; + +mod streaming_entry_reader; +pub use streaming_entry_reader::StreamingEntryReader; // re-exports -pub use async_read_zip::{ - AsyncArchive, AsyncEntry, HasAsyncCursor, ReadZipAsync, ReadZipWithSizeAsync, -}; pub use rc_zip; +pub use read_zip::{ + ArchiveHandle, EntryHandle, HasCursor, ReadZip, ReadZipEntriesStreaming, ReadZipWithSize, +}; diff --git a/rc-zip-tokio/src/async_read_zip.rs b/rc-zip-tokio/src/read_zip.rs similarity index 67% rename from rc-zip-tokio/src/async_read_zip.rs rename to rc-zip-tokio/src/read_zip.rs index 427ab78..439d1c6 100644 --- a/rc-zip-tokio/src/async_read_zip.rs +++ b/rc-zip-tokio/src/read_zip.rs @@ -6,25 +6,23 @@ use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf}; use rc_zip::{ error::Error, - fsm::{ArchiveFsm, FsmResult}, + fsm::{ArchiveFsm, EntryFsm, FsmResult}, parse::{Archive, Entry}, }; +use tracing::trace; -use crate::entry_reader::EntryReader; +use crate::{entry_reader::EntryReader, StreamingEntryReader}; /// A trait for reading something as a zip archive. /// /// See also [ReadZipAsync]. -pub trait ReadZipWithSizeAsync { +pub trait ReadZipWithSize { /// The type of the file to read from. - type File: HasAsyncCursor; + type File: HasCursor; /// Reads self as a zip archive. #[allow(async_fn_in_trait)] - async fn read_zip_with_size_async( - &self, - size: u64, - ) -> Result, Error>; + async fn read_zip_with_size(&self, size: u64) -> Result, Error>; } /// A zip archive, read asynchronously from a file or other I/O resource. @@ -32,22 +30,22 @@ pub trait ReadZipWithSizeAsync { /// This only contains metadata for the archive and its entries. Separate /// readers can be created for arbitraries entries on-demand using /// [AsyncEntry::reader]. -pub trait ReadZipAsync { +pub trait ReadZip { /// The type of the file to read from. - type File: HasAsyncCursor; + type File: HasCursor; /// Reads self as a zip archive. #[allow(async_fn_in_trait)] - async fn read_zip_async(&self) -> Result, Error>; + async fn read_zip(&self) -> Result, Error>; } -impl ReadZipWithSizeAsync for F +impl ReadZipWithSize for F where - F: HasAsyncCursor, + F: HasCursor, { type File = F; - async fn read_zip_with_size_async(&self, size: u64) -> Result, Error> { + async fn read_zip_with_size(&self, size: u64) -> Result, Error> { let mut fsm = ArchiveFsm::new(size); loop { if let Some(offset) = fsm.wants_read() { @@ -64,7 +62,7 @@ where fsm = match fsm.process()? { FsmResult::Done(archive) => { - return Ok(AsyncArchive { + return Ok(ArchiveHandle { file: self, archive, }) @@ -75,43 +73,43 @@ where } } -impl ReadZipAsync for &[u8] { +impl ReadZip for &[u8] { type File = Self; - async fn read_zip_async(&self) -> Result, Error> { - self.read_zip_with_size_async(self.len() as u64).await + async fn read_zip(&self) -> Result, Error> { + self.read_zip_with_size(self.len() as u64).await } } -impl ReadZipAsync for Vec { +impl ReadZip for Vec { type File = Self; - async fn read_zip_async(&self) -> Result, Error> { - self.read_zip_with_size_async(self.len() as u64).await + async fn read_zip(&self) -> Result, Error> { + self.read_zip_with_size(self.len() as u64).await } } -impl ReadZipAsync for Arc { +impl ReadZip for Arc { type File = Self; - async fn read_zip_async(&self) -> Result, Error> { + async fn read_zip(&self) -> Result, Error> { let size = self.size()?.unwrap_or_default(); - self.read_zip_with_size_async(size).await + self.read_zip_with_size(size).await } } /// A zip archive, read asynchronously from a file or other I/O resource. -pub struct AsyncArchive<'a, F> +pub struct ArchiveHandle<'a, F> where - F: HasAsyncCursor, + F: HasCursor, { file: &'a F, archive: Archive, } -impl Deref for AsyncArchive<'_, F> +impl Deref for ArchiveHandle<'_, F> where - F: HasAsyncCursor, + F: HasCursor, { type Target = Archive; @@ -120,13 +118,13 @@ where } } -impl AsyncArchive<'_, F> +impl ArchiveHandle<'_, F> where - F: HasAsyncCursor, + F: HasCursor, { /// Iterate over all files in this zip, read from the central directory. - pub fn entries(&self) -> impl Iterator> { - self.archive.entries().map(move |entry| AsyncEntry { + pub fn entries(&self) -> impl Iterator> { + self.archive.entries().map(move |entry| EntryHandle { file: self.file, entry, }) @@ -134,11 +132,11 @@ where /// Attempts to look up an entry by name. This is usually a bad idea, /// as names aren't necessarily normalized in zip archives. - pub fn by_name>(&self, name: N) -> Option> { + pub fn by_name>(&self, name: N) -> Option> { self.archive .entries() .find(|&x| x.name == name.as_ref()) - .map(|entry| AsyncEntry { + .map(|entry| EntryHandle { file: self.file, entry, }) @@ -146,12 +144,12 @@ where } /// A single entry in a zip archive, read asynchronously from a file or other I/O resource. -pub struct AsyncEntry<'a, F> { +pub struct EntryHandle<'a, F> { file: &'a F, entry: &'a Entry, } -impl Deref for AsyncEntry<'_, F> { +impl Deref for EntryHandle<'_, F> { type Target = Entry; fn deref(&self) -> &Self::Target { @@ -159,9 +157,9 @@ impl Deref for AsyncEntry<'_, F> { } } -impl<'a, F> AsyncEntry<'a, F> +impl<'a, F> EntryHandle<'a, F> where - F: HasAsyncCursor, + F: HasCursor, { /// Returns a reader for the entry. pub fn reader(&self) -> impl AsyncRead + Unpin + '_ { @@ -177,7 +175,7 @@ where } /// A sliceable I/O resource: we can ask for an [AsyncRead] at a given offset. -pub trait HasAsyncCursor { +pub trait HasCursor { /// The type returned by [HasAsyncCursor::cursor_at]. type Cursor<'a>: AsyncRead + Unpin + 'a where @@ -187,7 +185,7 @@ pub trait HasAsyncCursor { fn cursor_at(&self, offset: u64) -> Self::Cursor<'_>; } -impl HasAsyncCursor for &[u8] { +impl HasCursor for &[u8] { type Cursor<'a> = &'a [u8] where Self: 'a; @@ -197,7 +195,7 @@ impl HasAsyncCursor for &[u8] { } } -impl HasAsyncCursor for Vec { +impl HasCursor for Vec { type Cursor<'a> = &'a [u8] where Self: 'a; @@ -207,7 +205,7 @@ impl HasAsyncCursor for Vec { } } -impl HasAsyncCursor for Arc { +impl HasCursor for Arc { type Cursor<'a> = AsyncRandomAccessFileCursor where Self: 'a; @@ -293,3 +291,45 @@ impl AsyncRead for AsyncRandomAccessFileCursor { } } } + +/// Allows reading zip entries in a streaming fashion, without seeking, +/// based only on local headers. THIS IS NOT RECOMMENDED, as correctly +/// reading zip files requires reading the central directory (located at +/// the end of the file). +/// +/// Using local headers only involves a lot of guesswork and is only really +/// useful if you have some level of control over your input. +pub trait ReadZipEntriesStreaming +where + R: AsyncRead, +{ + /// Get the first zip entry from the stream as a [StreamingEntryReader]. + /// + /// See [ReadZipEntriesStreaming]'s documentation for why using this is + /// generally a bad idea: you might want to use [ReadZip] or + /// [ReadZipWithSize] instead. + #[allow(async_fn_in_trait)] + async fn read_first_zip_entry_streaming(self) -> Result, Error>; +} + +impl ReadZipEntriesStreaming for R +where + R: AsyncRead + Unpin, +{ + async fn read_first_zip_entry_streaming(mut self) -> Result, Error> { + let mut fsm = EntryFsm::new(None, None); + + loop { + if fsm.wants_read() { + let n = self.read(fsm.space()).await?; + trace!("read {} bytes into buf for first zip entry", n); + fsm.fill(n); + } + + if let Some(entry) = fsm.process_till_header()? { + let entry = entry.clone(); + return Ok(StreamingEntryReader::new(fsm, entry, self)); + } + } + } +} diff --git a/rc-zip-tokio/src/streaming_entry_reader.rs b/rc-zip-tokio/src/streaming_entry_reader.rs new file mode 100644 index 0000000..57ef240 --- /dev/null +++ b/rc-zip-tokio/src/streaming_entry_reader.rs @@ -0,0 +1,179 @@ +use oval::Buffer; +use pin_project_lite::pin_project; +use rc_zip::{ + error::{Error, FormatError}, + fsm::{EntryFsm, FsmResult}, + parse::Entry, +}; +use std::{io, pin::Pin, task}; +use tokio::io::{AsyncRead, AsyncReadExt, ReadBuf}; +use tracing::trace; + +pin_project! { + /// Reads a zip entry based on a local header. Some information is missing, + /// not all name encodings may work, and only by reading it in its entirety + /// can you move on to the next entry. + /// + /// However, it only requires an [AsyncRead], and does not need to seek. + pub struct StreamingEntryReader { + entry: Entry, + #[pin] + rd: R, + state: State, + } +} + +#[derive(Default)] +#[allow(clippy::large_enum_variant)] +enum State { + Reading { + fsm: EntryFsm, + }, + Finished { + /// remaining buffer for next entry + remain: Buffer, + }, + #[default] + Transition, +} + +impl StreamingEntryReader +where + R: AsyncRead, +{ + pub(crate) fn new(fsm: EntryFsm, entry: Entry, rd: R) -> Self { + Self { + entry, + rd, + state: State::Reading { fsm }, + } + } +} + +impl AsyncRead for StreamingEntryReader +where + R: AsyncRead, +{ + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> task::Poll> { + let this = self.as_mut().project(); + + trace!("reading from streaming entry reader"); + + match std::mem::take(this.state) { + State::Reading { mut fsm } => { + if fsm.wants_read() { + trace!("fsm wants read"); + let mut buf = ReadBuf::new(fsm.space()); + match this.rd.poll_read(cx, &mut buf) { + task::Poll::Ready(res) => res?, + task::Poll::Pending => { + *this.state = State::Reading { fsm }; + return task::Poll::Pending; + } + } + let n = buf.filled().len(); + + trace!("giving fsm {} bytes from rd", n); + fsm.fill(n); + } else { + trace!("fsm does not want read"); + } + + match fsm.process(buf.initialize_unfilled())? { + FsmResult::Continue((fsm, outcome)) => { + trace!("fsm wants to continue"); + *this.state = State::Reading { fsm }; + + if outcome.bytes_written > 0 { + trace!("bytes have been written"); + buf.advance(outcome.bytes_written); + } else if outcome.bytes_read == 0 { + trace!("no bytes have been written or read"); + // that's EOF, baby! + } else { + trace!("read some bytes, hopefully will write more later"); + // loop, it happens + return self.poll_read(cx, buf); + } + } + FsmResult::Done(remain) => { + *this.state = State::Finished { remain }; + + // neat! + } + } + } + State::Finished { remain } => { + // wait for them to call finish + *this.state = State::Finished { remain }; + } + State::Transition => unreachable!(), + } + Ok(()).into() + } +} + +impl StreamingEntryReader +where + R: AsyncRead + Unpin, +{ + /// Return entry information for this reader + #[inline(always)] + pub fn entry(&self) -> &Entry { + &self.entry + } + + /// Finish reading this entry, returning the next streaming entry reader, if + /// any. This panics if the entry is not fully read. + /// + /// If this returns None, there's no entries left. + pub async fn finish(mut self) -> Result>, Error> { + trace!("finishing streaming entry reader"); + + if matches!(self.state, State::Reading { .. }) { + // this should transition to finished if there's no data + _ = self.read(&mut [0u8; 1]).await?; + } + + match self.state { + State::Reading { .. } => { + panic!("entry not fully read"); + } + State::Finished { remain } => { + // parse the next entry, if any + let mut fsm = EntryFsm::new(None, Some(remain)); + + loop { + if fsm.wants_read() { + let n = self.rd.read(fsm.space()).await?; + trace!("read {} bytes into buf for first zip entry", n); + fsm.fill(n); + } + + match fsm.process_till_header() { + Ok(Some(entry)) => { + let entry = entry.clone(); + return Ok(Some(StreamingEntryReader::new(fsm, entry, self.rd))); + } + Ok(None) => { + // needs more turns + } + Err(e) => match e { + Error::Format(FormatError::InvalidLocalHeader) => { + // we probably reached the end of central directory! + // TODO: we should probably check for the end of central directory + return Ok(None); + } + _ => return Err(e), + }, + } + } + } + State::Transition => unreachable!(), + } + } +} diff --git a/rc-zip-tokio/tests/integration_tests.rs b/rc-zip-tokio/tests/integration_tests.rs index 0993e13..799088b 100644 --- a/rc-zip-tokio/tests/integration_tests.rs +++ b/rc-zip-tokio/tests/integration_tests.rs @@ -4,11 +4,11 @@ use rc_zip::{ error::Error, parse::Archive, }; -use rc_zip_tokio::{AsyncArchive, HasAsyncCursor, ReadZipAsync}; +use rc_zip_tokio::{ArchiveHandle, HasCursor, ReadZip}; use std::sync::Arc; -async fn check_case(test: &Case, archive: Result, Error>) { +async fn check_case(test: &Case, archive: Result, Error>) { corpus::check_case(test, archive.as_ref().map(|ar| -> &Archive { ar })); let archive = match archive { Ok(archive) => archive, @@ -30,14 +30,14 @@ async fn check_case(test: &Case, archive: Result Date: Mon, 5 Feb 2024 22:25:34 +0100 Subject: [PATCH 22/23] Add tests for streaming interface --- rc-zip-sync/examples/jean.rs | 4 ++-- rc-zip-sync/src/lib.rs | 2 +- rc-zip-sync/src/read_zip.rs | 17 +++++++------- rc-zip-sync/tests/integration_tests.rs | 28 ++++++++++++++++++++++-- rc-zip-tokio/src/lib.rs | 2 +- rc-zip-tokio/src/read_zip.rs | 17 +++++++------- rc-zip-tokio/tests/integration_tests.rs | 28 +++++++++++++++++++++++- rc-zip/src/corpus/mod.rs | 8 +++++++ testdata/meta.zip | Bin 0 -> 45922 bytes 9 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 testdata/meta.zip diff --git a/rc-zip-sync/examples/jean.rs b/rc-zip-sync/examples/jean.rs index a6f3a86..7d92451 100644 --- a/rc-zip-sync/examples/jean.rs +++ b/rc-zip-sync/examples/jean.rs @@ -2,7 +2,7 @@ use cfg_if::cfg_if; use clap::{Parser, Subcommand}; use humansize::{format_size, BINARY}; use rc_zip::parse::{Archive, EntryKind, Method, Version}; -use rc_zip_sync::{ReadZip, ReadZipEntriesStreaming}; +use rc_zip_sync::{ReadZip, ReadZipStreaming}; use std::{ borrow::Cow, @@ -305,7 +305,7 @@ fn do_main(cli: Cli) -> Result<(), Box> { let start_time = std::time::SystemTime::now(); - let mut entry_reader = zipfile.read_first_zip_entry_streaming()?; + let mut entry_reader = zipfile.stream_zip_entries_throwing_caution_to_the_wind()?; loop { let entry_name = match entry_reader.entry().sanitized_name() { Some(name) => name, diff --git a/rc-zip-sync/src/lib.rs b/rc-zip-sync/src/lib.rs index 98e0ddf..d1c890a 100644 --- a/rc-zip-sync/src/lib.rs +++ b/rc-zip-sync/src/lib.rs @@ -16,5 +16,5 @@ pub use streaming_entry_reader::StreamingEntryReader; // re-exports pub use rc_zip; pub use read_zip::{ - ArchiveHandle, EntryHandle, HasCursor, ReadZip, ReadZipEntriesStreaming, ReadZipWithSize, + ArchiveHandle, EntryHandle, HasCursor, ReadZip, ReadZipStreaming, ReadZipWithSize, }; diff --git a/rc-zip-sync/src/read_zip.rs b/rc-zip-sync/src/read_zip.rs index 771b69d..73bacb3 100644 --- a/rc-zip-sync/src/read_zip.rs +++ b/rc-zip-sync/src/read_zip.rs @@ -223,26 +223,27 @@ impl ReadZip for std::fs::File { /// based only on local headers. THIS IS NOT RECOMMENDED, as correctly /// reading zip files requires reading the central directory (located at /// the end of the file). -/// -/// Using local headers only involves a lot of guesswork and is only really -/// useful if you have some level of control over your input. -pub trait ReadZipEntriesStreaming +pub trait ReadZipStreaming where R: Read, { /// Get the first zip entry from the stream as a [StreamingEntryReader]. /// - /// See [ReadZipEntriesStreaming]'s documentation for why using this is + /// See the trait's documentation for why using this is /// generally a bad idea: you might want to use [ReadZip] or /// [ReadZipWithSize] instead. - fn read_first_zip_entry_streaming(self) -> Result, Error>; + fn stream_zip_entries_throwing_caution_to_the_wind( + self, + ) -> Result, Error>; } -impl ReadZipEntriesStreaming for R +impl ReadZipStreaming for R where R: Read, { - fn read_first_zip_entry_streaming(mut self) -> Result, Error> { + fn stream_zip_entries_throwing_caution_to_the_wind( + mut self, + ) -> Result, Error> { let mut fsm = EntryFsm::new(None, None); loop { diff --git a/rc-zip-sync/tests/integration_tests.rs b/rc-zip-sync/tests/integration_tests.rs index 2efdc1c..3df5500 100644 --- a/rc-zip-sync/tests/integration_tests.rs +++ b/rc-zip-sync/tests/integration_tests.rs @@ -3,9 +3,9 @@ use rc_zip::{ error::Error, parse::Archive, }; -use rc_zip_sync::{ArchiveHandle, HasCursor, ReadZip}; +use rc_zip_sync::{ArchiveHandle, HasCursor, ReadZip, ReadZipStreaming}; -use std::fs::File; +use std::{fs::File, io::Read}; fn check_case(test: &Case, archive: Result, Error>) { corpus::check_case(test, archive.as_ref().map(|ar| -> &Archive { ar })); @@ -54,3 +54,27 @@ fn real_world_files() { drop(guarded_path) } } + +#[test_log::test] +fn streaming() { + for case in corpus::streaming_test_cases() { + let guarded_path = case.absolute_path(); + let file = File::open(&guarded_path.path).unwrap(); + + let mut entry = file + .stream_zip_entries_throwing_caution_to_the_wind() + .unwrap(); + loop { + let mut v = vec![]; + let n = entry.read_to_end(&mut v).unwrap(); + tracing::trace!("entry {} read {} bytes", entry.entry().name, n); + + match entry.finish().unwrap() { + Some(next) => entry = next, + None => break, + } + } + + drop(guarded_path) + } +} diff --git a/rc-zip-tokio/src/lib.rs b/rc-zip-tokio/src/lib.rs index 75c506d..aed77c9 100644 --- a/rc-zip-tokio/src/lib.rs +++ b/rc-zip-tokio/src/lib.rs @@ -16,5 +16,5 @@ pub use streaming_entry_reader::StreamingEntryReader; // re-exports pub use rc_zip; pub use read_zip::{ - ArchiveHandle, EntryHandle, HasCursor, ReadZip, ReadZipEntriesStreaming, ReadZipWithSize, + ArchiveHandle, EntryHandle, HasCursor, ReadZip, ReadZipStreaming, ReadZipWithSize, }; diff --git a/rc-zip-tokio/src/read_zip.rs b/rc-zip-tokio/src/read_zip.rs index 439d1c6..751e540 100644 --- a/rc-zip-tokio/src/read_zip.rs +++ b/rc-zip-tokio/src/read_zip.rs @@ -296,27 +296,28 @@ impl AsyncRead for AsyncRandomAccessFileCursor { /// based only on local headers. THIS IS NOT RECOMMENDED, as correctly /// reading zip files requires reading the central directory (located at /// the end of the file). -/// -/// Using local headers only involves a lot of guesswork and is only really -/// useful if you have some level of control over your input. -pub trait ReadZipEntriesStreaming +pub trait ReadZipStreaming where R: AsyncRead, { /// Get the first zip entry from the stream as a [StreamingEntryReader]. /// - /// See [ReadZipEntriesStreaming]'s documentation for why using this is + /// See the trait's documentation for why using this is /// generally a bad idea: you might want to use [ReadZip] or /// [ReadZipWithSize] instead. #[allow(async_fn_in_trait)] - async fn read_first_zip_entry_streaming(self) -> Result, Error>; + async fn stream_zip_entries_throwing_caution_to_the_wind( + self, + ) -> Result, Error>; } -impl ReadZipEntriesStreaming for R +impl ReadZipStreaming for R where R: AsyncRead + Unpin, { - async fn read_first_zip_entry_streaming(mut self) -> Result, Error> { + async fn stream_zip_entries_throwing_caution_to_the_wind( + mut self, + ) -> Result, Error> { let mut fsm = EntryFsm::new(None, None); loop { diff --git a/rc-zip-tokio/tests/integration_tests.rs b/rc-zip-tokio/tests/integration_tests.rs index 799088b..35e8c90 100644 --- a/rc-zip-tokio/tests/integration_tests.rs +++ b/rc-zip-tokio/tests/integration_tests.rs @@ -4,7 +4,8 @@ use rc_zip::{ error::Error, parse::Archive, }; -use rc_zip_tokio::{ArchiveHandle, HasCursor, ReadZip}; +use rc_zip_tokio::{ArchiveHandle, HasCursor, ReadZip, ReadZipStreaming}; +use tokio::io::AsyncReadExt; use std::sync::Arc; @@ -53,3 +54,28 @@ async fn real_world_files() { drop(guarded_path) } } + +#[test_log::test(tokio::test)] +async fn streaming() { + for case in corpus::streaming_test_cases() { + let guarded_path = case.absolute_path(); + let file = tokio::fs::File::open(&guarded_path.path).await.unwrap(); + + let mut entry = file + .stream_zip_entries_throwing_caution_to_the_wind() + .await + .unwrap(); + loop { + let mut v = vec![]; + let n = entry.read_to_end(&mut v).await.unwrap(); + tracing::trace!("entry {} read {} bytes", entry.entry().name, n); + + match entry.finish().await.unwrap() { + Some(next) => entry = next, + None => break, + } + } + + drop(guarded_path) + } +} diff --git a/rc-zip/src/corpus/mod.rs b/rc-zip/src/corpus/mod.rs index 0161e63..8ae5229 100644 --- a/rc-zip/src/corpus/mod.rs +++ b/rc-zip/src/corpus/mod.rs @@ -259,6 +259,14 @@ pub fn test_cases() -> Vec { ] } +pub fn streaming_test_cases() -> Vec { + vec![Case { + name: "meta.zip", + files: Files::NumFiles(0), + ..Default::default() + }] +} + pub fn check_case(case: &Case, archive: Result<&Archive, &Error>) { let case_bytes = case.bytes(); diff --git a/testdata/meta.zip b/testdata/meta.zip new file mode 100644 index 0000000000000000000000000000000000000000..d5762f30b28400577c81869d92291116b9a3cb9a GIT binary patch literal 45922 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 w Date: Mon, 5 Feb 2024 22:27:06 +0100 Subject: [PATCH 23/23] Fix docs --- rc-zip-sync/src/read_zip.rs | 2 +- rc-zip-tokio/src/read_zip.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rc-zip-sync/src/read_zip.rs b/rc-zip-sync/src/read_zip.rs index 73bacb3..a9679d5 100644 --- a/rc-zip-sync/src/read_zip.rs +++ b/rc-zip-sync/src/read_zip.rs @@ -91,7 +91,7 @@ impl ReadZip for Vec { /// /// This only contains metadata for the archive and its entries. Separate /// readers can be created for arbitraries entries on-demand using -/// [SyncEntry::reader]. +/// [EntryHandle::reader]. pub struct ArchiveHandle<'a, F> where F: HasCursor, diff --git a/rc-zip-tokio/src/read_zip.rs b/rc-zip-tokio/src/read_zip.rs index 751e540..f64dae6 100644 --- a/rc-zip-tokio/src/read_zip.rs +++ b/rc-zip-tokio/src/read_zip.rs @@ -15,7 +15,7 @@ use crate::{entry_reader::EntryReader, StreamingEntryReader}; /// A trait for reading something as a zip archive. /// -/// See also [ReadZipAsync]. +/// See also [ReadZip]. pub trait ReadZipWithSize { /// The type of the file to read from. type File: HasCursor; @@ -29,7 +29,7 @@ pub trait ReadZipWithSize { /// /// This only contains metadata for the archive and its entries. Separate /// readers can be created for arbitraries entries on-demand using -/// [AsyncEntry::reader]. +/// [EntryHandle::reader]. pub trait ReadZip { /// The type of the file to read from. type File: HasCursor; @@ -176,7 +176,7 @@ where /// A sliceable I/O resource: we can ask for an [AsyncRead] at a given offset. pub trait HasCursor { - /// The type returned by [HasAsyncCursor::cursor_at]. + /// The type returned by [HasCursor::cursor_at]. type Cursor<'a>: AsyncRead + Unpin + 'a where Self: 'a;