diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..4b014006 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --manifest-path xtask/Cargo.toml --" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..75e40804 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,139 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +env: + CARGO_TERM_COLOR: always + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test: + name: Test (${{ matrix.build }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + build: [stable, beta, nightly, macos, windows] + include: + - build: stable + os: ubuntu-latest + rust: stable + - build: beta + os: ubuntu-latest + rust: beta + - build: nightly + os: ubuntu-latest + rust: nightly + - build: macos + os: macos-latest + rust: stable + - build: windows + os: windows-latest + rust: stable + steps: + - uses: actions/checkout@v6 + - name: Install Rust + run: rustup update ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }} + shell: bash + - run: cargo test + - run: cargo test --no-default-features + - name: Run cargo test with root + run: sudo -E $(which cargo) test + if: ${{ matrix.os == 'ubuntu-latest' }} + + wasm: + name: Wasm + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-emscripten + - run: cargo build --target=wasm32-unknown-emscripten + + rustfmt: + name: Rustfmt + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt -- --check + + semver-checks: + name: Semver Checks + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + - uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + # Pinned until cargo-semver-checks supports rustdoc format v57 (Rust 1.93+) + rust-toolchain: "1.92.0" + + # FIXME: failed on https://github.com/alexcrichton/tar-rs/pull/443, needs + # investigation. + # revdep: + # name: Reverse deps + # runs-on: ubuntu-24.04 + # timeout-minutes: 30 + # steps: + # - uses: actions/checkout@v6 + # - uses: dtolnay/rust-toolchain@stable + # - uses: Swatinem/rust-cache@v2 + # with: + # workspaces: xtask + # - name: Test reverse dependencies + # run: cargo xtask revdep-test + # - name: Verify tests catch regressions + # run: cargo xtask revdep-test --self-test + + publish_docs: + name: Publish Documentation + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + - name: Build documentation + run: cargo doc --no-deps --all-features + - name: Publish documentation + run: | + cd target/doc + git init + git add . + git -c user.name='ci' -c user.email='ci' commit -m init + git push -f -q https://git:${{ secrets.github_token }}@github.com/${{ github.repository }} HEAD:gh-pages + if: github.event_name == 'push' && github.event.ref == 'refs/heads/master' + + # Sentinel job for required checks - configure this job name in + # repository settings as the single required status check. + required-checks: + if: always() + needs: [test, wasm, rustfmt, semver-checks, publish_docs] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - run: exit 1 + if: >- + needs.test.result != 'success' || + needs.wasm.result != 'success' || + needs.rustfmt.result != 'success' || + needs.semver-checks.result != 'success' || + needs.publish_docs.result != 'success' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 074306c2..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: CI -on: [push, pull_request] - -jobs: - test: - name: Test - runs-on: ${{ matrix.os }} - strategy: - matrix: - build: [stable, beta, nightly, macos, windows] - include: - - build: stable - os: ubuntu-latest - rust: stable - - build: beta - os: ubuntu-latest - rust: beta - - build: nightly - os: ubuntu-latest - rust: nightly - - build: macos - os: macos-latest - rust: stable - - build: windows - os: windows-latest - rust: stable - steps: - - uses: actions/checkout@master - - name: Install Rust - run: rustup update ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }} - shell: bash - - run: cargo test - - run: cargo test --no-default-features - - name: Run cargo test with root - run: sudo -E $(which cargo) test - if: ${{ matrix.os == 'ubuntu-latest' }} - - wasm: - name: Wasm - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - run: rustup target add wasm32-unknown-emscripten - - run: cargo build --target=wasm32-unknown-emscripten - - rustfmt: - name: Rustfmt - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Install Rust - run: rustup update stable && rustup default stable && rustup component add rustfmt - - run: cargo fmt -- --check - - publish_docs: - name: Publish Documentation - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Install Rust - run: rustup update stable && rustup default stable - - name: Build documentation - run: cargo doc --no-deps --all-features - - name: Publish documentation - run: | - cd target/doc - git init - git add . - git -c user.name='ci' -c user.email='ci' commit -m init - git push -f -q https://git:${{ secrets.github_token }}@github.com/${{ github.repository }} HEAD:gh-pages - if: github.event_name == 'push' && github.event.ref == 'refs/heads/master' diff --git a/.gitignore b/.gitignore index 4fffb2f8..fe1c82e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target /Cargo.lock +/xtask/target diff --git a/Cargo.toml b/Cargo.toml index 99220d70..3afaa733 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tar" -version = "0.4.44" +version = "0.4.45" authors = ["Alex Crichton "] homepage = "https://github.com/alexcrichton/tar-rs" repository = "https://github.com/alexcrichton/tar-rs" @@ -9,6 +9,7 @@ license = "MIT OR Apache-2.0" keywords = ["tar", "tarfile", "encoding"] readme = "README.md" edition = "2021" +rust-version = "1.63" exclude = ["tests/archives/*"] description = """ @@ -22,7 +23,11 @@ contents are never required to be entirely resident in memory all at once. filetime = "0.2.8" [dev-dependencies] +astral-tokio-tar = "0.5" +rand = { version = "0.8", features = ["small_rng"] } tempfile = "3" +tokio = { version = "1", features = ["macros", "rt"] } +tokio-stream = "0.1" [target."cfg(unix)".dependencies] xattr = { version = "1.1.3", optional = true } @@ -30,3 +35,7 @@ libc = "0.2" [features] default = ["xattr"] + +[lints.rust] +# Feel free to comment this one out locally during development of a patch. +dead_code = "deny" diff --git a/LICENSE-MIT b/LICENSE-MIT index 39e0ed66..5ff752fc 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright (c) 2014 Alex Crichton +Copyright (c) The tar-rs Project Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated diff --git a/examples/extract_file.rs b/examples/extract_file.rs index 425e9a35..6a415a6d 100644 --- a/examples/extract_file.rs +++ b/examples/extract_file.rs @@ -13,7 +13,7 @@ use std::path::Path; use tar::Archive; fn main() { - let first_arg = args_os().skip(1).next().unwrap(); + let first_arg = args_os().nth(1).unwrap(); let filename = Path::new(&first_arg); let mut ar = Archive::new(stdin()); for file in ar.entries().unwrap() { diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 7eaff139..1dc1dfff 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -8,9 +8,8 @@ edition = "2018" cargo-fuzz = true [dependencies] -arbitrary = "1.3.2" +arbitrary = { version = "1.3.2", features = ["derive"] } cap-std = "3.4.0" -derive_arbitrary = "1.3.2" libfuzzer-sys = "0.4" tempfile = "3.3" diff --git a/fuzz/fuzz_targets/archive.rs b/fuzz/fuzz_targets/archive.rs index 79c26aee..dff73a52 100644 --- a/fuzz/fuzz_targets/archive.rs +++ b/fuzz/fuzz_targets/archive.rs @@ -17,7 +17,6 @@ use arbitrary::{Arbitrary, Unstructured}; use cap_std::fs::Dir; use cap_std::ambient_authority; -use derive_arbitrary::Arbitrary; use libfuzzer_sys::fuzz_target; use std::io::{Cursor, Write}; use tar::{Archive, Builder, EntryType, Header}; diff --git a/src/archive.rs b/src/archive.rs index 4d569c63..a3ae6f01 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -79,10 +79,10 @@ impl Archive { /// sequence. If entries are processed out of sequence (from what the /// iterator returns), then the contents read for each entry may be /// corrupted. - pub fn entries(&mut self) -> io::Result> { + pub fn entries(&mut self) -> io::Result> { let me: &mut Archive = self; me._entries(None).map(|fields| Entries { - fields: fields, + fields, _ignored: marker::PhantomData, }) } @@ -93,9 +93,21 @@ impl Archive { /// extracting each file in turn to the location specified by the entry's /// path name. /// - /// This operation is relatively sensitive in that it will not write files - /// outside of the path specified by `dst`. Files in the archive which have - /// a '..' in their path are skipped during the unpacking process. + /// # Security + /// + /// A best-effort is made to prevent writing files outside `dst` (paths + /// containing `..` are skipped, symlinks are validated). However, there + /// have been historical bugs in this area, and more may exist. For this + /// reason, when processing untrusted archives, stronger sandboxing is + /// encouraged: e.g. the [`cap-std`] crate and/or OS-level + /// containerization/virtualization. + /// + /// If `dst` does not exist, it is created. Unpacking into an existing + /// directory merges content. This function assumes `dst` is not + /// concurrently modified by untrusted processes. Protecting against + /// TOCTOU races is out of scope for this crate. + /// + /// [`cap-std`]: https://docs.rs/cap-std/ /// /// # Examples /// @@ -184,11 +196,11 @@ impl Archive { /// sequence. If entries are processed out of sequence (from what the /// iterator returns), then the contents read for each entry may be /// corrupted. - pub fn entries_with_seek(&mut self) -> io::Result> { + pub fn entries_with_seek(&mut self) -> io::Result> { let me: &Archive = self; let me_seekable: &Archive = self; me._entries(Some(me_seekable)).map(|fields| Entries { - fields: fields, + fields, _ignored: marker::PhantomData, }) } @@ -216,7 +228,7 @@ impl Archive { fn _unpack(&mut self, dst: &Path) -> io::Result<()> { if dst.symlink_metadata().is_err() { - fs::create_dir_all(&dst) + fs::create_dir_all(dst) .map_err(|e| TarError::new(format!("failed to create `{}`", dst.display()), e))?; } @@ -228,7 +240,7 @@ impl Archive { let dst = &dst.canonicalize().unwrap_or(dst.to_path_buf()); // Delay any directory entries until the end (they will be created if needed by - // descendants), to ensure that directory permissions do not interfer with descendant + // descendants), to ensure that directory permissions do not interfere with descendant // extraction. let mut directories = Vec::new(); for entry in self._entries(None)? { @@ -264,10 +276,7 @@ impl<'a, R: Read> Entries<'a, R> { /// or long link archive members. Raw iteration is disabled by default. pub fn raw(self, raw: bool) -> Entries<'a, R> { Entries { - fields: EntriesFields { - raw: raw, - ..self.fields - }, + fields: EntriesFields { raw, ..self.fields }, _ignored: marker::PhantomData, } } @@ -340,17 +349,18 @@ impl<'a> EntriesFields<'a> { let file_pos = self.next; let mut size = header.entry_size()?; - if size == 0 { - if let Some(pax_size) = pax_size { - size = pax_size; - } + // If this exists, it must override the header size. Disagreement among + // parsers allows construction of malicious archives that appear different + // when parsed. + if let Some(pax_size) = pax_size { + size = pax_size; } let ret = EntryFields { - size: size, - header_pos: header_pos, - file_pos: file_pos, + size, + header_pos, + file_pos, data: vec![EntryIo::Data((&self.archive.inner).take(size))], - header: header, + header, long_pathname: None, long_linkname: None, pax_extensions: None, @@ -587,7 +597,7 @@ impl<'a> Iterator for EntriesFields<'a> { } } -impl<'a, R: ?Sized + Read> Read for &'a ArchiveInner { +impl Read for &ArchiveInner { fn read(&mut self, into: &mut [u8]) -> io::Result { let i = self.obj.borrow_mut().read(into)?; self.pos.set(self.pos.get() + i as u64); @@ -595,7 +605,7 @@ impl<'a, R: ?Sized + Read> Read for &'a ArchiveInner { } } -impl<'a, R: ?Sized + Seek> Seek for &'a ArchiveInner { +impl Seek for &ArchiveInner { fn seek(&mut self, pos: SeekFrom) -> io::Result { let pos = self.obj.borrow_mut().seek(pos)?; self.pos.set(pos); diff --git a/src/builder.rs b/src/builder.rs index 54413605..88164c88 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -181,7 +181,7 @@ impl Builder { ) -> io::Result<()> { prepare_header_path(self.get_mut(), header, path.as_ref())?; header.set_cksum(); - self.append(&header, data) + self.append(header, data) } /// Adds a new entry to this archive and returns an [`EntryWriter`] for @@ -270,7 +270,7 @@ impl Builder { prepare_header_path(self.get_mut(), header, path)?; prepare_header_link(self.get_mut(), header, target)?; header.set_cksum(); - self.append(&header, std::io::empty()) + self.append(header, std::io::empty()) } /// Adds a file on the local filesystem to this archive. @@ -381,7 +381,7 @@ impl Builder { /// operation then this may corrupt the archive. /// /// Note this will not add the contents of the directory to the archive. - /// See `append_dir_all` for recusively adding the contents of the directory. + /// See `append_dir_all` for recursively adding the contents of the directory. /// /// Also note that after all files have been written to an archive the /// `finish` function needs to be called to finish writing the archive. @@ -749,28 +749,32 @@ fn prepare_header_path(dst: &mut dyn Write, header: &mut Header, path: &Path) -> // long name extension by emitting an entry which indicates that it's the // filename. if let Err(e) = header.set_path(path) { - let data = path2bytes(&path)?; + let data = path2bytes(path)?; let max = header.as_old().name.len(); // Since `e` isn't specific enough to let us know the path is indeed too // long, verify it first before using the extension. if data.len() < max { return Err(e); } - let header2 = prepare_header(data.len() as u64, b'L'); - // null-terminated string - let mut data2 = data.chain(io::repeat(0).take(1)); - append(dst, &header2, &mut data2)?; - // Truncate the path to store in the header we're about to emit to // ensure we've got something at least mentioned. Note that we use // `str`-encoding to be compatible with Windows, but in general the // entry in the header itself shouldn't matter too much since extraction // doesn't look at it. + // + // Validate the truncated path BEFORE writing the long-name extension + // to the stream. If validation fails after writing, the orphaned + // extension entry corrupts subsequent archive entries. let truncated = match str::from_utf8(&data[..max]) { Ok(s) => s, Err(e) => str::from_utf8(&data[..e.valid_up_to()]).unwrap(), }; - header.set_truncated_path_for_gnu_header(&truncated)?; + header.set_truncated_path_for_gnu_header(truncated)?; + + let header2 = prepare_header(data.len() as u64, b'L'); + // null-terminated string + let mut data2 = data.chain(io::repeat(0).take(1)); + append(dst, &header2, &mut data2)?; } Ok(()) } @@ -781,8 +785,8 @@ fn prepare_header_link( link_name: &Path, ) -> io::Result<()> { // Same as previous function but for linkname - if let Err(e) = header.set_link_name(&link_name) { - let data = path2bytes(&link_name)?; + if let Err(e) = header.set_link_name(link_name) { + let data = path2bytes(link_name)?; if data.len() < header.as_old().linkname.len() { return Err(e); } @@ -876,7 +880,7 @@ fn append_dir_all( ) -> io::Result<()> { let mut stack = vec![(src_path.to_path_buf(), true, false)]; while let Some((src, is_dir, is_symlink)) = stack.pop() { - let dest = path.join(src.strip_prefix(&src_path).unwrap()); + let dest = path.join(src.strip_prefix(src_path).unwrap()); // In case of a symlink pointing to a directory, is_dir is false, but src.is_dir() will return true if is_dir || (is_symlink && options.follow && src.is_dir()) { for entry in fs::read_dir(&src)? { @@ -926,11 +930,10 @@ struct SparseEntry { /// Find sparse entries in a file. Returns: /// * `Ok(Some(_))` if the file is sparse. -/// * `Ok(None)` if the file is not sparse, or if the file system does not -/// support sparse files. +/// * `Ok(None)` if the file is not sparse, or if the file system does not support sparse files. /// * `Err(_)` if an error occurred. The lack of support for sparse files is not -/// considered an error. It might return an error if the file is modified -/// while reading. +/// considered an error. It might return an error if the file is modified +/// while reading. fn find_sparse_entries( file: &mut fs::File, stat: &fs::Metadata, @@ -983,7 +986,7 @@ fn find_sparse_entries_seek( }); } - // On most Unices, we need to read `_PC_MIN_HOLE_SIZE` to see if the file + // On most Unixes, we need to read `_PC_MIN_HOLE_SIZE` to see if the file // system supports `SEEK_HOLE`. // FreeBSD: https://man.freebsd.org/cgi/man.cgi?query=lseek&sektion=2&manpath=FreeBSD+14.1-STABLE #[cfg(not(any(target_os = "linux", target_os = "android")))] @@ -1208,7 +1211,7 @@ mod tests { "| |####|####| |", &[ SparseEntry { - offset: 1 * SPARSE_BLOCK_SIZE, + offset: SPARSE_BLOCK_SIZE, num_bytes: 2 * SPARSE_BLOCK_SIZE, }, SparseEntry { diff --git a/src/entry.rs b/src/entry.rs index 843719f0..fbc2efb9 100644 --- a/src/entry.rs +++ b/src/entry.rs @@ -74,7 +74,7 @@ impl<'a, R: Read> Entry<'a, R> { /// /// It is recommended to use this method instead of inspecting the `header` /// directly to ensure that various archive formats are handled correctly. - pub fn path(&self) -> io::Result> { + pub fn path(&self) -> io::Result> { self.fields.path() } @@ -84,7 +84,7 @@ impl<'a, R: Read> Entry<'a, R> { /// separators, and it will not always return the same value as /// `self.header().path_bytes()` as some archive formats have support for /// longer path names described in separate entries. - pub fn path_bytes(&self) -> Cow<[u8]> { + pub fn path_bytes(&self) -> Cow<'_, [u8]> { self.fields.path_bytes() } @@ -101,7 +101,7 @@ impl<'a, R: Read> Entry<'a, R> { /// /// It is recommended to use this method instead of inspecting the `header` /// directly to ensure that various archive formats are handled correctly. - pub fn link_name(&self) -> io::Result>> { + pub fn link_name(&self) -> io::Result>> { self.fields.link_name() } @@ -110,7 +110,7 @@ impl<'a, R: Read> Entry<'a, R> { /// Note that this will not always return the same value as /// `self.header().link_name_bytes()` as some archive formats have support for /// longer path names described in separate entries. - pub fn link_name_bytes(&self) -> Option> { + pub fn link_name_bytes(&self) -> Option> { self.fields.link_name_bytes() } @@ -132,7 +132,7 @@ impl<'a, R: Read> Entry<'a, R> { /// /// Also note that this function will read the entire entry if the entry /// itself is a list of extensions. - pub fn pax_extensions(&mut self) -> io::Result> { + pub fn pax_extensions(&mut self) -> io::Result>> { self.fields.pax_extensions() } @@ -212,8 +212,9 @@ impl<'a, R: Read> Entry<'a, R> { /// also be propagated to the path `dst`. Any existing file at the location /// `dst` will be overwritten. /// - /// This function carefully avoids writing outside of `dst`. If the file has - /// a '..' in its path, this function will skip it and return false. + /// # Security + /// + /// See [`Archive::unpack`]. /// /// # Examples /// @@ -300,11 +301,11 @@ impl<'a> EntryFields<'a> { self.read_to_end(&mut v).map(|_| v) } - fn path(&self) -> io::Result> { + fn path(&self) -> io::Result> { bytes2path(self.path_bytes()) } - fn path_bytes(&self) -> Cow<[u8]> { + fn path_bytes(&self) -> Cow<'_, [u8]> { match self.long_pathname { Some(ref bytes) => { if let Some(&0) = bytes.last() { @@ -333,14 +334,14 @@ impl<'a> EntryFields<'a> { String::from_utf8_lossy(&self.path_bytes()).to_string() } - fn link_name(&self) -> io::Result>> { + fn link_name(&self) -> io::Result>> { match self.link_name_bytes() { Some(bytes) => bytes2path(bytes).map(Some), None => Ok(None), } } - fn link_name_bytes(&self) -> Option> { + fn link_name_bytes(&self) -> Option> { match self.long_linkname { Some(ref bytes) => { if let Some(&0) = bytes.last() { @@ -364,7 +365,7 @@ impl<'a> EntryFields<'a> { } } - fn pax_extensions(&mut self) -> io::Result> { + fn pax_extensions(&mut self) -> io::Result>> { if self.pax_extensions.is_none() { if !self.header.entry_type().is_pax_global_extensions() && !self.header.entry_type().is_pax_local_extensions() @@ -430,10 +431,10 @@ impl<'a> EntryFields<'a> { None => return Ok(false), }; - self.ensure_dir_created(&dst, parent) + self.ensure_dir_created(dst, parent) .map_err(|e| TarError::new(format!("failed to create `{}`", parent.display()), e))?; - let canon_target = self.validate_inside_dst(&dst, parent)?; + let canon_target = self.validate_inside_dst(dst, parent)?; self.unpack(Some(&canon_target), &file_dst) .map_err(|e| TarError::new(format!("failed to unpack `{}`", file_dst.display()), e))?; @@ -446,7 +447,7 @@ impl<'a> EntryFields<'a> { // If the directory already exists just let it slide fs::create_dir(dst).or_else(|err| { if err.kind() == ErrorKind::AlreadyExists { - let prev = fs::metadata(dst); + let prev = fs::symlink_metadata(dst); if prev.map(|m| m.is_dir()).unwrap_or(false) { return Ok(()); } @@ -537,7 +538,7 @@ impl<'a> EntryFields<'a> { // use canonicalization to ensure this guarantee. For hard // links though they're canonicalized to their existing path // so we need to validate at this time. - Some(ref p) => { + Some(p) => { let link_src = p.join(src); self.validate_inside_dst(p, &link_src)?; link_src diff --git a/src/entry_type.rs b/src/entry_type.rs index 7f2494ab..f7773552 100644 --- a/src/entry_type.rs +++ b/src/entry_type.rs @@ -1,10 +1,8 @@ // See https://en.wikipedia.org/wiki/Tar_%28computing%29#UStar_format -/// Indicate for the type of file described by a header. +/// Indicate the type of content described by a header. /// -/// Each `Header` has an `entry_type` method returning an instance of this type -/// which can be used to inspect what the header is describing. - -/// A non-exhaustive enum representing the possible entry types +/// This is returned by [`crate::Header::entry_type()`] and should be used to +/// distinguish between types of content. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum EntryType { /// Regular file diff --git a/src/header.rs b/src/header.rs index 87cbcc86..8413d10f 100644 --- a/src/header.rs +++ b/src/header.rs @@ -7,7 +7,6 @@ use std::borrow::Cow; use std::fmt; use std::fs; use std::io; -use std::iter; use std::iter::{once, repeat}; use std::mem; use std::path::{Component, Path, PathBuf}; @@ -22,7 +21,7 @@ use crate::EntryType; /// This value, chosen after careful deliberation, corresponds to _Jul 23, 2006_, /// which is the date of the first commit for what would become Rust. #[cfg(all(any(unix, windows), not(target_arch = "wasm32")))] -const DETERMINISTIC_TIMESTAMP: u64 = 1153704088; +pub const DETERMINISTIC_TIMESTAMP: u64 = 1153704088; pub(crate) const BLOCK_SIZE: u64 = 512; @@ -124,7 +123,7 @@ pub struct GnuHeader { pub pad: [u8; 17], } -/// Description of the header of a spare entry. +/// Description of the header of a sparse entry. /// /// Specifies the offset/number of bytes of a chunk of data in octal. #[repr(C)] @@ -351,7 +350,7 @@ impl Header { /// /// Note that this function will convert any `\` characters to directory /// separators. - pub fn path(&self) -> io::Result> { + pub fn path(&self) -> io::Result> { bytes2path(self.path_bytes()) } @@ -362,7 +361,7 @@ impl Header { /// /// Note that this function will convert any `\` characters to directory /// separators. - pub fn path_bytes(&self) -> Cow<[u8]> { + pub fn path_bytes(&self) -> Cow<'_, [u8]> { if let Some(ustar) = self.as_ustar() { ustar.path_bytes() } else { @@ -426,7 +425,7 @@ impl Header { /// /// Note that this function will convert any `\` characters to directory /// separators. - pub fn link_name(&self) -> io::Result>> { + pub fn link_name(&self) -> io::Result>> { match self.link_name_bytes() { Some(bytes) => bytes2path(bytes).map(Some), None => Ok(None), @@ -440,7 +439,7 @@ impl Header { /// /// Note that this function will convert any `\` characters to directory /// separators. - pub fn link_name_bytes(&self) -> Option> { + pub fn link_name_bytes(&self) -> Option> { let old = self.as_old(); if old.linkname[0] != 0 { Some(Cow::Borrowed(truncate(&old.linkname))) @@ -505,14 +504,12 @@ impl Header { /// /// May return an error if the field is corrupted. pub fn uid(&self) -> io::Result { - num_field_wrapper_from(&self.as_old().uid) - .map(|u| u as u64) - .map_err(|err| { - io::Error::new( - err.kind(), - format!("{} when getting uid for {}", err, self.path_lossy()), - ) - }) + num_field_wrapper_from(&self.as_old().uid).map_err(|err| { + io::Error::new( + err.kind(), + format!("{} when getting uid for {}", err, self.path_lossy()), + ) + }) } /// Encodes the `uid` provided into this header. @@ -522,14 +519,12 @@ impl Header { /// Returns the value of the group's user ID field pub fn gid(&self) -> io::Result { - num_field_wrapper_from(&self.as_old().gid) - .map(|u| u as u64) - .map_err(|err| { - io::Error::new( - err.kind(), - format!("{} when getting gid for {}", err, self.path_lossy()), - ) - }) + num_field_wrapper_from(&self.as_old().gid).map_err(|err| { + io::Error::new( + err.kind(), + format!("{} when getting gid for {}", err, self.path_lossy()), + ) + }) } /// Encodes the `gid` provided into this header. @@ -744,7 +739,7 @@ impl Header { let len = old.cksum.len(); self.bytes[0..offset] .iter() - .chain(iter::repeat(&b' ').take(len)) + .chain(repeat(&b' ').take(len)) .chain(&self.bytes[offset + len..]) .fold(0, |a, b| a + (*b as u32)) } @@ -780,7 +775,7 @@ impl Header { self.set_mtime(meta.mtime() as u64); self.set_uid(meta.uid() as u64); self.set_gid(meta.gid() as u64); - self.set_mode(meta.mode() as u32); + self.set_mode(meta.mode()); } HeaderMode::Deterministic => { // We could in theory set the mtime to zero here, but not all tools seem to behave @@ -977,7 +972,7 @@ impl fmt::Debug for OldHeader { impl UstarHeader { /// See `Header::path_bytes` - pub fn path_bytes(&self) -> Cow<[u8]> { + pub fn path_bytes(&self) -> Cow<'_, [u8]> { if self.prefix[0] == 0 && !self.name.contains(&b'\\') { Cow::Borrowed(truncate(&self.name)) } else { @@ -1545,7 +1540,7 @@ fn truncate(slice: &[u8]) -> &[u8] { fn copy_into(slot: &mut [u8], bytes: &[u8]) -> io::Result<()> { if bytes.len() > slot.len() { Err(other("provided value is too long")) - } else if bytes.iter().any(|b| *b == 0) { + } else if bytes.contains(&0) { Err(other("provided value contains a nul byte")) } else { for (slot, val) in slot.iter_mut().zip(bytes.iter().chain(Some(&0))) { @@ -1591,7 +1586,7 @@ fn copy_path_into_inner( return Err(other("path component in archive cannot contain `/`")); } } - copy(&mut slot, &*bytes)?; + copy(&mut slot, &bytes)?; if &*bytes != b"/" { needs_slash = true; } @@ -1601,13 +1596,13 @@ fn copy_path_into_inner( return Err(other("paths in archives must have at least one component")); } if ends_with_slash(path) { - copy(&mut slot, &[b'/'])?; + copy(&mut slot, b"/")?; } return Ok(()); fn copy(slot: &mut &mut [u8], bytes: &[u8]) -> io::Result<()> { - copy_into(*slot, bytes)?; - let tmp = mem::replace(slot, &mut []); + copy_into(slot, bytes)?; + let tmp = mem::take(slot); *slot = &mut tmp[bytes.len()..]; Ok(()) } @@ -1652,11 +1647,11 @@ fn ends_with_slash(p: &Path) -> bool { #[cfg(all(unix, not(target_arch = "wasm32")))] fn ends_with_slash(p: &Path) -> bool { - p.as_os_str().as_bytes().ends_with(&[b'/']) + p.as_os_str().as_bytes().ends_with(b"/") } #[cfg(any(windows, target_arch = "wasm32"))] -pub fn path2bytes(p: &Path) -> io::Result> { +pub fn path2bytes(p: &Path) -> io::Result> { p.as_os_str() .to_str() .map(|s| s.as_bytes()) @@ -1679,8 +1674,8 @@ pub fn path2bytes(p: &Path) -> io::Result> { #[cfg(all(unix, not(target_arch = "wasm32")))] /// On unix this will never fail -pub fn path2bytes(p: &Path) -> io::Result> { - Ok(p.as_os_str().as_bytes()).map(Cow::Borrowed) +pub fn path2bytes(p: &Path) -> io::Result> { + Ok(Cow::Borrowed(p.as_os_str().as_bytes())) } #[cfg(windows)] diff --git a/src/lib.rs b/src/lib.rs index 78d89a05..db890001 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,8 @@ pub use crate::builder::{Builder, EntryWriter}; pub use crate::entry::{Entry, Unpacked}; pub use crate::entry_type::EntryType; pub use crate::header::GnuExtSparseHeader; +#[cfg(all(any(unix, windows), not(target_arch = "wasm32")))] +pub use crate::header::DETERMINISTIC_TIMESTAMP; pub use crate::header::{GnuHeader, GnuSparseHeader, Header, HeaderMode, OldHeader, UstarHeader}; pub use crate::pax::{PaxExtension, PaxExtensions}; diff --git a/src/pax.rs b/src/pax.rs index d1494282..c62e5f47 100644 --- a/src/pax.rs +++ b/src/pax.rs @@ -89,7 +89,7 @@ impl<'entry> Iterator for PaxExtensions<'entry> { fn next(&mut self) -> Option>> { let line = match self.data.next() { - Some(line) if line.is_empty() => return None, + Some([]) => return None, Some(line) => line, None => return None, }; diff --git a/tests/all.rs b/tests/all.rs index 0ad67f98..39f0bac0 100644 --- a/tests/all.rs +++ b/tests/all.rs @@ -11,16 +11,51 @@ use std::iter::repeat; use std::path::{Path, PathBuf}; use filetime::FileTime; +use rand::rngs::SmallRng; +use rand::{Rng, SeedableRng}; use tar::{Archive, Builder, Entries, Entry, EntryType, Header, HeaderMode}; use tempfile::{Builder as TempBuilder, TempDir}; -macro_rules! t { - ($e:expr) => { - match $e { - Ok(v) => v, - Err(e) => panic!("{} returned {}", stringify!($e), e), +/// A reader wrapper that returns partial results from `read()` to exercise +/// parsers that might assume `read()` fills the entire buffer. +/// +/// Each call returns between 1 and buf.len() bytes, biased toward small +/// reads by taking the minimum of two uniform samples. This gives roughly +/// quadratic density toward 1, so small reads (1-10 bytes) occur frequently +/// while large reads still happen. Uses a deterministic seeded RNG so +/// tests remain reproducible. +struct RandomReader { + inner: R, + rng: SmallRng, +} + +impl RandomReader { + fn new(inner: R) -> Self { + RandomReader { + inner, + rng: SmallRng::seed_from_u64(0), } - }; + } +} + +impl Read for RandomReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if buf.is_empty() { + return self.inner.read(buf); + } + // Take the min of two uniform samples to bias toward small reads. + let a = self.rng.gen_range(1..=buf.len()); + let b = self.rng.gen_range(1..=buf.len()); + self.inner.read(&mut buf[..a.min(b)]) + } +} + +/// Convenience: wrap a byte slice in a RandomReader>. +/// +/// The RNG is seeded from a hash of the data, so different archives +/// exercise different read-size sequences while remaining deterministic. +fn random_cursor_reader>(data: D) -> RandomReader> { + RandomReader::new(Cursor::new(data)) } macro_rules! tar { @@ -39,14 +74,15 @@ fn simple_concat() { let mut archive_bytes = Vec::new(); archive_bytes.extend(bytes); - let original_names: Vec = decode_names(&mut Archive::new(Cursor::new(&archive_bytes))); + let original_names: Vec = + decode_names(&mut Archive::new(random_cursor_reader(&archive_bytes))); let expected: Vec<&str> = original_names.iter().map(|n| n.as_str()).collect(); // concat two archives (with null in-between); archive_bytes.extend(bytes); // test now that when we read the archive, it stops processing at the first zero header. - let actual = decode_names(&mut Archive::new(Cursor::new(&archive_bytes))); + let actual = decode_names(&mut Archive::new(random_cursor_reader(&archive_bytes))); assert_eq!(expected, actual); // extend expected by itself. @@ -57,7 +93,7 @@ fn simple_concat() { o }; - let mut ar = Archive::new(Cursor::new(&archive_bytes)); + let mut ar = Archive::new(random_cursor_reader(&archive_bytes)); ar.set_ignore_zeros(true); let actual = decode_names(&mut ar); @@ -69,9 +105,9 @@ fn simple_concat() { { let mut names = Vec::new(); - for entry in t!(ar.entries()) { - let e = t!(entry); - names.push(t!(::std::str::from_utf8(&e.path_bytes())).to_string()); + for entry in ar.entries().unwrap() { + let e = entry.unwrap(); + names.push(::std::str::from_utf8(&e.path_bytes()).unwrap().to_string()); } names @@ -80,11 +116,11 @@ fn simple_concat() { #[test] fn header_impls() { - let mut ar = Archive::new(Cursor::new(tar!("simple.tar"))); + let mut ar = Archive::new(random_cursor_reader(tar!("simple.tar"))); let hn = Header::new_old(); let hnb = hn.as_bytes(); - for file in t!(ar.entries()) { - let file = t!(file); + for file in ar.entries().unwrap() { + let file = file.unwrap(); let h1 = file.header(); let h1b = h1.as_bytes(); let h2 = h1.clone(); @@ -95,11 +131,11 @@ fn header_impls() { #[test] fn header_impls_missing_last_header() { - let mut ar = Archive::new(Cursor::new(tar!("simple_missing_last_header.tar"))); + let mut ar = Archive::new(random_cursor_reader(tar!("simple_missing_last_header.tar"))); let hn = Header::new_old(); let hnb = hn.as_bytes(); - for file in t!(ar.entries()) { - let file = t!(file); + for file in ar.entries().unwrap() { + let file = file.unwrap(); let h1 = file.header(); let h1b = h1.as_bytes(); let h2 = h1.clone(); @@ -110,20 +146,20 @@ fn header_impls_missing_last_header() { #[test] fn reading_files() { - let rdr = Cursor::new(tar!("reading_files.tar")); + let rdr = random_cursor_reader(tar!("reading_files.tar")); let mut ar = Archive::new(rdr); - let mut entries = t!(ar.entries()); + let mut entries = ar.entries().unwrap(); - let mut a = t!(entries.next().unwrap()); + let mut a = entries.next().unwrap().unwrap(); assert_eq!(&*a.header().path_bytes(), b"a"); let mut s = String::new(); - t!(a.read_to_string(&mut s)); + a.read_to_string(&mut s).unwrap(); assert_eq!(s, "a\na\na\na\na\na\na\na\na\na\na\n"); - let mut b = t!(entries.next().unwrap()); + let mut b = entries.next().unwrap().unwrap(); assert_eq!(&*b.header().path_bytes(), b"b"); s.truncate(0); - t!(b.read_to_string(&mut s)); + b.read_to_string(&mut s).unwrap(); assert_eq!(s, "b\nb\nb\nb\nb\nb\nb\nb\nb\nb\nb\n"); assert!(entries.next().is_none()); @@ -132,22 +168,23 @@ fn reading_files() { #[test] fn writing_files() { let mut ar = Builder::new(Vec::new()); - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let path = td.path().join("test"); - t!(t!(File::create(&path)).write_all(b"test")); + File::create(&path).unwrap().write_all(b"test").unwrap(); - t!(ar.append_file("test2", &mut t!(File::open(&path)))); + ar.append_file("test2", &mut File::open(&path).unwrap()) + .unwrap(); - let data = t!(ar.into_inner()); + let data = ar.into_inner().unwrap(); let mut ar = Archive::new(Cursor::new(data)); - let mut entries = t!(ar.entries()); - let mut f = t!(entries.next().unwrap()); + let mut entries = ar.entries().unwrap(); + let mut f = entries.next().unwrap().unwrap(); assert_eq!(&*f.header().path_bytes(), b"test2"); assert_eq!(f.header().size().unwrap(), 4); let mut s = String::new(); - t!(f.read_to_string(&mut s)); + f.read_to_string(&mut s).unwrap(); assert_eq!(s, "test"); assert!(entries.next().is_none()); @@ -156,31 +193,33 @@ fn writing_files() { #[test] fn large_filename() { let mut ar = Builder::new(Vec::new()); - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let path = td.path().join("test"); - t!(t!(File::create(&path)).write_all(b"test")); + File::create(&path).unwrap().write_all(b"test").unwrap(); - let filename = repeat("abcd/").take(50).collect::(); + let filename = "abcd/".repeat(50); let mut header = Header::new_ustar(); header.set_path(&filename).unwrap(); - header.set_metadata(&t!(fs::metadata(&path))); + header.set_metadata(&fs::metadata(&path).unwrap()); header.set_cksum(); - t!(ar.append(&header, &b"test"[..])); - let too_long = repeat("abcd").take(200).collect::(); - t!(ar.append_file(&too_long, &mut t!(File::open(&path)))); - t!(ar.append_data(&mut header, &too_long, &b"test"[..])); - - let rd = Cursor::new(t!(ar.into_inner())); + ar.append(&header, &b"test"[..]).unwrap(); + let too_long = "abcd".repeat(200); + ar.append_file(&too_long, &mut File::open(&path).unwrap()) + .unwrap(); + ar.append_data(&mut header, &too_long, &b"test"[..]) + .unwrap(); + + let rd = Cursor::new(ar.into_inner().unwrap()); let mut ar = Archive::new(rd); - let mut entries = t!(ar.entries()); + let mut entries = ar.entries().unwrap(); // The short entry added with `append` let mut f = entries.next().unwrap().unwrap(); assert_eq!(&*f.header().path_bytes(), filename.as_bytes()); assert_eq!(f.header().size().unwrap(), 4); let mut s = String::new(); - t!(f.read_to_string(&mut s)); + f.read_to_string(&mut s).unwrap(); assert_eq!(s, "test"); // The long entry added with `append_file` @@ -188,7 +227,7 @@ fn large_filename() { assert_eq!(&*f.path_bytes(), too_long.as_bytes()); assert_eq!(f.header().size().unwrap(), 4); let mut s = String::new(); - t!(f.read_to_string(&mut s)); + f.read_to_string(&mut s).unwrap(); assert_eq!(s, "test"); // The long entry added with `append_data` @@ -197,7 +236,7 @@ fn large_filename() { assert_eq!(&*f.path_bytes(), too_long.as_bytes()); assert_eq!(f.header().size().unwrap(), 4); let mut s = String::new(); - t!(f.read_to_string(&mut s)); + f.read_to_string(&mut s).unwrap(); assert_eq!(s, "test"); assert!(entries.next().is_none()); @@ -217,44 +256,45 @@ fn large_filename_with_dot_dot_at_100_byte_mark() { let mut long_name_with_dot_dot = "tdir/".repeat(19); long_name_with_dot_dot.push_str("tt/..file"); - t!(ar.append_data(&mut header, &long_name_with_dot_dot, b"test".as_slice())); + ar.append_data(&mut header, &long_name_with_dot_dot, b"test".as_slice()) + .unwrap(); - let rd = Cursor::new(t!(ar.into_inner())); + let rd = Cursor::new(ar.into_inner().unwrap()); let mut ar = Archive::new(rd); - let mut entries = t!(ar.entries()); + let mut entries = ar.entries().unwrap(); let mut f = entries.next().unwrap().unwrap(); assert_eq!(&*f.path_bytes(), long_name_with_dot_dot.as_bytes()); assert_eq!(f.header().size().unwrap(), 4); let mut s = String::new(); - t!(f.read_to_string(&mut s)); + f.read_to_string(&mut s).unwrap(); assert_eq!(s, "test"); assert!(entries.next().is_none()); } fn reading_entries_common(mut entries: Entries) { - let mut a = t!(entries.next().unwrap()); + let mut a = entries.next().unwrap().unwrap(); assert_eq!(&*a.header().path_bytes(), b"a"); let mut s = String::new(); - t!(a.read_to_string(&mut s)); + a.read_to_string(&mut s).unwrap(); assert_eq!(s, "a\na\na\na\na\na\na\na\na\na\na\n"); s.truncate(0); - t!(a.read_to_string(&mut s)); + a.read_to_string(&mut s).unwrap(); assert_eq!(s, ""); - let mut b = t!(entries.next().unwrap()); + let mut b = entries.next().unwrap().unwrap(); assert_eq!(&*b.header().path_bytes(), b"b"); s.truncate(0); - t!(b.read_to_string(&mut s)); + b.read_to_string(&mut s).unwrap(); assert_eq!(s, "b\nb\nb\nb\nb\nb\nb\nb\nb\nb\nb\n"); assert!(entries.next().is_none()); } #[test] fn reading_entries() { - let rdr = Cursor::new(tar!("reading_files.tar")); + let rdr = random_cursor_reader(tar!("reading_files.tar")); let mut ar = Archive::new(rdr); - reading_entries_common(t!(ar.entries())); + reading_entries_common(ar.entries().unwrap()); } #[test] @@ -280,9 +320,8 @@ impl LoggingReader { impl Read for LoggingReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.inner.read(buf).map(|i| { + self.inner.read(buf).inspect(|&i| { self.read_bytes += i as u64; - i }) } } @@ -297,13 +336,17 @@ impl Seek for LoggingReader { fn skipping_entries_with_seek() { let mut reader = LoggingReader::new(Cursor::new(tar!("reading_files.tar"))); let mut ar_reader = Archive::new(&mut reader); - let files: Vec<_> = t!(ar_reader.entries()) + let files: Vec<_> = ar_reader + .entries() + .unwrap() .map(|entry| entry.unwrap().path().unwrap().to_path_buf()) .collect(); let mut seekable_reader = LoggingReader::new(Cursor::new(tar!("reading_files.tar"))); let mut ar_seekable_reader = Archive::new(&mut seekable_reader); - let files_seekable: Vec<_> = t!(ar_seekable_reader.entries_with_seek()) + let files_seekable: Vec<_> = ar_seekable_reader + .entries_with_seek() + .unwrap() .map(|entry| entry.unwrap().path().unwrap().to_path_buf()) .collect(); @@ -323,7 +366,7 @@ fn check_dirtree(td: &TempDir) { #[test] fn extracting_directories() { let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); - let rdr = Cursor::new(tar!("directory.tar")); + let rdr = random_cursor_reader(tar!("directory.tar")); let mut ar = Archive::new(rdr); ar.unpack(td.path()).unwrap(); check_dirtree(&td); @@ -331,11 +374,11 @@ fn extracting_directories() { #[test] fn extracting_duplicate_file_fail() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let path_present = td.path().join("a"); - t!(File::create(path_present)); + File::create(path_present).unwrap(); - let rdr = Cursor::new(tar!("reading_files.tar")); + let rdr = random_cursor_reader(tar!("reading_files.tar")); let mut ar = Archive::new(rdr); ar.set_overwrite(false); if let Err(err) = ar.unpack(td.path()) { @@ -353,24 +396,24 @@ fn extracting_duplicate_file_fail() { #[test] fn extracting_duplicate_file_succeed() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let path_present = td.path().join("a"); - t!(File::create(path_present)); + File::create(path_present).unwrap(); - let rdr = Cursor::new(tar!("reading_files.tar")); + let rdr = random_cursor_reader(tar!("reading_files.tar")); let mut ar = Archive::new(rdr); ar.set_overwrite(true); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); } #[test] #[cfg(unix)] fn extracting_duplicate_link_fail() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let path_present = td.path().join("lnk"); - t!(std::os::unix::fs::symlink("file", path_present)); + std::os::unix::fs::symlink("file", path_present).unwrap(); - let rdr = Cursor::new(tar!("link.tar")); + let rdr = random_cursor_reader(tar!("link.tar")); let mut ar = Archive::new(rdr); ar.set_overwrite(false); if let Err(err) = ar.unpack(td.path()) { @@ -389,14 +432,14 @@ fn extracting_duplicate_link_fail() { #[test] #[cfg(unix)] fn extracting_duplicate_link_succeed() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let path_present = td.path().join("lnk"); - t!(std::os::unix::fs::symlink("file", path_present)); + std::os::unix::fs::symlink("file", path_present).unwrap(); - let rdr = Cursor::new(tar!("link.tar")); + let rdr = random_cursor_reader(tar!("link.tar")); let mut ar = Archive::new(rdr); ar.set_overwrite(true); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); } #[test] @@ -404,11 +447,14 @@ fn extracting_duplicate_link_succeed() { fn xattrs() { // If /tmp is a tmpfs, xattr will fail // The xattr crate's unit tests also use /var/tmp for this reason - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir_in("/var/tmp")); - let rdr = Cursor::new(tar!("xattrs.tar")); + let td = TempBuilder::new() + .prefix("tar-rs") + .tempdir_in("/var/tmp") + .unwrap(); + let rdr = random_cursor_reader(tar!("xattrs.tar")); let mut ar = Archive::new(rdr); ar.set_unpack_xattrs(true); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); let val = xattr::get(td.path().join("a/b"), "user.pax.flags").unwrap(); assert_eq!(val.unwrap(), "epm".as_bytes()); @@ -419,11 +465,14 @@ fn xattrs() { fn no_xattrs() { // If /tmp is a tmpfs, xattr will fail // The xattr crate's unit tests also use /var/tmp for this reason - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir_in("/var/tmp")); - let rdr = Cursor::new(tar!("xattrs.tar")); + let td = TempBuilder::new() + .prefix("tar-rs") + .tempdir_in("/var/tmp") + .unwrap(); + let rdr = random_cursor_reader(tar!("xattrs.tar")); let mut ar = Archive::new(rdr); ar.set_unpack_xattrs(false); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); assert_eq!( xattr::get(td.path().join("a/b"), "user.pax.flags").unwrap(), @@ -433,54 +482,56 @@ fn no_xattrs() { #[test] fn writing_and_extracting_directories() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let mut ar = Builder::new(Vec::new()); let tmppath = td.path().join("tmpfile"); - t!(t!(File::create(&tmppath)).write_all(b"c")); - t!(ar.append_dir("a", ".")); - t!(ar.append_dir("a/b", ".")); - t!(ar.append_file("a/c", &mut t!(File::open(&tmppath)))); - t!(ar.finish()); + File::create(&tmppath).unwrap().write_all(b"c").unwrap(); + ar.append_dir("a", ".").unwrap(); + ar.append_dir("a/b", ".").unwrap(); + ar.append_file("a/c", &mut File::open(&tmppath).unwrap()) + .unwrap(); + ar.finish().unwrap(); - let rdr = Cursor::new(t!(ar.into_inner())); + let rdr = Cursor::new(ar.into_inner().unwrap()); let mut ar = Archive::new(rdr); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); check_dirtree(&td); } #[test] fn writing_and_extracting_directories_complex_permissions() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); // Archive with complex permissions which would fail to unpack if one attempted to do so // without reordering of entries. let mut ar = Builder::new(Vec::new()); let tmppath = td.path().join("tmpfile"); - t!(t!(File::create(&tmppath)).write_all(b"c")); + File::create(&tmppath).unwrap().write_all(b"c").unwrap(); // Root dir with very stringent permissions let data: &[u8] = &[]; let mut header = Header::new_gnu(); header.set_mode(0o555); header.set_entry_type(EntryType::Directory); - t!(header.set_path("a")); + header.set_path("a").unwrap(); header.set_size(0); header.set_cksum(); - t!(ar.append(&header, data)); + ar.append(&header, data).unwrap(); // Nested dir header.set_mode(0o777); header.set_entry_type(EntryType::Directory); - t!(header.set_path("a/b")); + header.set_path("a/b").unwrap(); header.set_cksum(); - t!(ar.append(&header, data)); + ar.append(&header, data).unwrap(); // Nested file. - t!(ar.append_file("a/c", &mut t!(File::open(&tmppath)))); - t!(ar.finish()); + ar.append_file("a/c", &mut File::open(&tmppath).unwrap()) + .unwrap(); + ar.finish().unwrap(); - let rdr = Cursor::new(t!(ar.into_inner())); + let rdr = Cursor::new(ar.into_inner().unwrap()); let mut ar = Archive::new(rdr); ar.unpack(td.path()).unwrap(); check_dirtree(&td); @@ -488,21 +539,27 @@ fn writing_and_extracting_directories_complex_permissions() { #[test] fn writing_directories_recursively() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let base_dir = td.path().join("base"); - t!(fs::create_dir(&base_dir)); - t!(t!(File::create(base_dir.join("file1"))).write_all(b"file1")); + fs::create_dir(&base_dir).unwrap(); + File::create(base_dir.join("file1")) + .unwrap() + .write_all(b"file1") + .unwrap(); let sub_dir = base_dir.join("sub"); - t!(fs::create_dir(&sub_dir)); - t!(t!(File::create(sub_dir.join("file2"))).write_all(b"file2")); + fs::create_dir(&sub_dir).unwrap(); + File::create(sub_dir.join("file2")) + .unwrap() + .write_all(b"file2") + .unwrap(); let mut ar = Builder::new(Vec::new()); - t!(ar.append_dir_all("foobar", base_dir)); - let data = t!(ar.into_inner()); + ar.append_dir_all("foobar", base_dir).unwrap(); + let data = ar.into_inner().unwrap(); let mut ar = Archive::new(Cursor::new(data)); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); let base_dir = td.path().join("foobar"); assert!(fs::metadata(&base_dir).map(|m| m.is_dir()).unwrap_or(false)); let file1_path = base_dir.join("file1"); @@ -519,23 +576,29 @@ fn writing_directories_recursively() { #[test] fn append_dir_all_blank_dest() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let base_dir = td.path().join("base"); - t!(fs::create_dir(&base_dir)); - t!(t!(File::create(base_dir.join("file1"))).write_all(b"file1")); + fs::create_dir(&base_dir).unwrap(); + File::create(base_dir.join("file1")) + .unwrap() + .write_all(b"file1") + .unwrap(); let sub_dir = base_dir.join("sub"); - t!(fs::create_dir(&sub_dir)); - t!(t!(File::create(sub_dir.join("file2"))).write_all(b"file2")); + fs::create_dir(&sub_dir).unwrap(); + File::create(sub_dir.join("file2")) + .unwrap() + .write_all(b"file2") + .unwrap(); let mut ar = Builder::new(Vec::new()); - t!(ar.append_dir_all("", base_dir)); - let data = t!(ar.into_inner()); + ar.append_dir_all("", base_dir).unwrap(); + let data = ar.into_inner().unwrap(); let mut ar = Archive::new(Cursor::new(data)); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); let base_dir = td.path(); - assert!(fs::metadata(&base_dir).map(|m| m.is_dir()).unwrap_or(false)); + assert!(fs::metadata(base_dir).map(|m| m.is_dir()).unwrap_or(false)); let file1_path = base_dir.join("file1"); assert!(fs::metadata(&file1_path) .map(|m| m.is_file()) @@ -550,9 +613,9 @@ fn append_dir_all_blank_dest() { #[test] fn append_dir_all_does_not_work_on_non_directory() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let path = td.path().join("test"); - t!(t!(File::create(&path)).write_all(b"test")); + File::create(&path).unwrap().write_all(b"test").unwrap(); let mut ar = Builder::new(Vec::new()); let result = ar.append_dir_all("test", path); @@ -561,10 +624,10 @@ fn append_dir_all_does_not_work_on_non_directory() { #[test] fn extracting_duplicate_dirs() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); - let rdr = Cursor::new(tar!("duplicate_dirs.tar")); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); + let rdr = random_cursor_reader(tar!("duplicate_dirs.tar")); let mut ar = Archive::new(rdr); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); let some_dir = td.path().join("some_dir"); assert!(fs::metadata(&some_dir).map(|m| m.is_dir()).unwrap_or(false)); @@ -572,60 +635,60 @@ fn extracting_duplicate_dirs() { #[test] fn unpack_old_style_bsd_dir() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let mut ar = Builder::new(Vec::new()); let mut header = Header::new_old(); header.set_entry_type(EntryType::Regular); - t!(header.set_path("testdir/")); + header.set_path("testdir/").unwrap(); header.set_size(0); header.set_cksum(); - t!(ar.append(&header, &mut io::empty())); + ar.append(&header, &mut io::empty()).unwrap(); // Extracting - let rdr = Cursor::new(t!(ar.into_inner())); + let rdr = Cursor::new(ar.into_inner().unwrap()); let mut ar = Archive::new(rdr); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); // Iterating let rdr = Cursor::new(ar.into_inner().into_inner()); let mut ar = Archive::new(rdr); - assert!(t!(ar.entries()).all(|fr| fr.is_ok())); + assert!(ar.entries().unwrap().all(|fr| fr.is_ok())); assert!(td.path().join("testdir").is_dir()); } #[test] fn handling_incorrect_file_size() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let mut ar = Builder::new(Vec::new()); let path = td.path().join("tmpfile"); - t!(File::create(&path)); - let mut file = t!(File::open(&path)); + File::create(&path).unwrap(); + let mut file = File::open(&path).unwrap(); let mut header = Header::new_old(); - t!(header.set_path("somepath")); - header.set_metadata(&t!(file.metadata())); + header.set_path("somepath").unwrap(); + header.set_metadata(&file.metadata().unwrap()); header.set_size(2048); // past the end of file null blocks header.set_cksum(); - t!(ar.append(&header, &mut file)); + ar.append(&header, &mut file).unwrap(); // Extracting - let rdr = Cursor::new(t!(ar.into_inner())); + let rdr = Cursor::new(ar.into_inner().unwrap()); let mut ar = Archive::new(rdr); assert!(ar.unpack(td.path()).is_err()); // Iterating let rdr = Cursor::new(ar.into_inner().into_inner()); let mut ar = Archive::new(rdr); - assert!(t!(ar.entries()).any(|fr| fr.is_err())); + assert!(ar.entries().unwrap().any(|fr| fr.is_err())); } #[test] fn extracting_malicious_tarball() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let mut evil_tar = Vec::new(); @@ -642,7 +705,7 @@ fn extracting_malicious_tarball() { } header.set_size(1); header.set_cksum(); - t!(a.append(&header, io::repeat(1).take(1))); + a.append(&header, io::repeat(1).take(1)).unwrap(); }; append("/tmp/abs_evil.txt"); // std parse `//` as UNC path, see rust-lang/rust#100833 @@ -673,7 +736,7 @@ fn extracting_malicious_tarball() { } let mut ar = Archive::new(&evil_tar[..]); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); assert!(fs::metadata("/tmp/abs_evil.txt").is_err()); assert!(fs::metadata("/tmp/abs_evil.txt2").is_err()); @@ -720,7 +783,7 @@ fn extracting_malicious_tarball() { #[test] fn octal_spaces() { - let rdr = Cursor::new(tar!("spaces.tar")); + let rdr = random_cursor_reader(tar!("spaces.tar")); let mut ar = Archive::new(rdr); let entry = ar.entries().unwrap().next().unwrap().unwrap(); @@ -734,41 +797,43 @@ fn octal_spaces() { #[test] fn extracting_malformed_tar_null_blocks() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let mut ar = Builder::new(Vec::new()); let path1 = td.path().join("tmpfile1"); let path2 = td.path().join("tmpfile2"); - t!(File::create(&path1)); - t!(File::create(&path2)); - t!(ar.append_file("tmpfile1", &mut t!(File::open(&path1)))); - let mut data = t!(ar.into_inner()); + File::create(&path1).unwrap(); + File::create(&path2).unwrap(); + ar.append_file("tmpfile1", &mut File::open(&path1).unwrap()) + .unwrap(); + let mut data = ar.into_inner().unwrap(); let amt = data.len(); data.truncate(amt - 512); let mut ar = Builder::new(data); - t!(ar.append_file("tmpfile2", &mut t!(File::open(&path2)))); - t!(ar.finish()); + ar.append_file("tmpfile2", &mut File::open(&path2).unwrap()) + .unwrap(); + ar.finish().unwrap(); - let data = t!(ar.into_inner()); + let data = ar.into_inner().unwrap(); let mut ar = Archive::new(&data[..]); assert!(ar.unpack(td.path()).is_ok()); } #[test] fn empty_filename() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); - let rdr = Cursor::new(tar!("empty_filename.tar")); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); + let rdr = random_cursor_reader(tar!("empty_filename.tar")); let mut ar = Archive::new(rdr); assert!(ar.unpack(td.path()).is_ok()); } #[test] fn file_times() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); - let rdr = Cursor::new(tar!("file_times.tar")); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); + let rdr = random_cursor_reader(tar!("file_times.tar")); let mut ar = Archive::new(rdr); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); let meta = fs::metadata(td.path().join("a")).unwrap(); let mtime = FileTime::from_last_modification_time(&meta); @@ -781,15 +846,15 @@ fn file_times() { #[test] fn zero_file_times() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let mut ar = Builder::new(Vec::new()); ar.mode(HeaderMode::Deterministic); let path = td.path().join("tmpfile"); - t!(File::create(&path)); - t!(ar.append_path_with_name(&path, "a")); + File::create(&path).unwrap(); + ar.append_path_with_name(&path, "a").unwrap(); - let data = t!(ar.into_inner()); + let data = ar.into_inner().unwrap(); let mut ar = Archive::new(&data[..]); assert!(ar.unpack(td.path()).is_ok()); @@ -803,68 +868,68 @@ fn zero_file_times() { #[test] fn backslash_treated_well() { // Insert a file into an archive with a backslash - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let mut ar = Builder::new(Vec::::new()); - t!(ar.append_dir("foo\\bar", td.path())); - let mut ar = Archive::new(Cursor::new(t!(ar.into_inner()))); - let f = t!(t!(ar.entries()).next().unwrap()); + ar.append_dir("foo\\bar", td.path()).unwrap(); + let mut ar = Archive::new(Cursor::new(ar.into_inner().unwrap())); + let f = ar.entries().unwrap().next().unwrap().unwrap(); if cfg!(unix) { - assert_eq!(t!(f.header().path()).to_str(), Some("foo\\bar")); + assert_eq!(f.header().path().unwrap().to_str(), Some("foo\\bar")); } else { - assert_eq!(t!(f.header().path()).to_str(), Some("foo/bar")); + assert_eq!(f.header().path().unwrap().to_str(), Some("foo/bar")); } // Unpack an archive with a backslash in the name let mut ar = Builder::new(Vec::::new()); let mut header = Header::new_gnu(); - header.set_metadata(&t!(fs::metadata(td.path()))); + header.set_metadata(&fs::metadata(td.path()).unwrap()); header.set_size(0); for (a, b) in header.as_old_mut().name.iter_mut().zip(b"foo\\bar\x00") { *a = *b; } header.set_cksum(); - t!(ar.append(&header, &mut io::empty())); - let data = t!(ar.into_inner()); + ar.append(&header, &mut io::empty()).unwrap(); + let data = ar.into_inner().unwrap(); let mut ar = Archive::new(&data[..]); - let f = t!(t!(ar.entries()).next().unwrap()); - assert_eq!(t!(f.header().path()).to_str(), Some("foo\\bar")); + let f = ar.entries().unwrap().next().unwrap().unwrap(); + assert_eq!(f.header().path().unwrap().to_str(), Some("foo\\bar")); let mut ar = Archive::new(&data[..]); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); assert!(fs::metadata(td.path().join("foo\\bar")).is_ok()); } #[test] #[cfg(unix)] fn set_mask() { - use ::std::os::unix::fs::PermissionsExt; + use std::os::unix::fs::PermissionsExt; let mut ar = tar::Builder::new(Vec::new()); let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("foo")); + header.set_path("foo").unwrap(); header.set_mode(0o777); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("bar")); + header.set_path("bar").unwrap(); header.set_mode(0o421); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); - let bytes = t!(ar.into_inner()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); ar.set_mask(0o211); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); - let md = t!(fs::metadata(td.path().join("foo"))); + let md = fs::metadata(td.path().join("foo")).unwrap(); assert_eq!(md.permissions().mode(), 0o100566); - let md = t!(fs::metadata(td.path().join("bar"))); + let md = fs::metadata(td.path().join("bar")).unwrap(); assert_eq!(md.permissions().mode(), 0o100420); } @@ -875,7 +940,7 @@ fn nul_bytes_in_path() { use std::os::unix::prelude::*; let nul_path = OsStr::from_bytes(b"foo\0"); - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let mut ar = Builder::new(Vec::::new()); let err = ar.append_dir(nul_path, td.path()).unwrap_err(); assert!(err.to_string().contains("contains a nul byte")); @@ -883,48 +948,48 @@ fn nul_bytes_in_path() { #[test] fn links() { - let mut ar = Archive::new(Cursor::new(tar!("link.tar"))); - let mut entries = t!(ar.entries()); - let link = t!(entries.next().unwrap()); + let mut ar = Archive::new(random_cursor_reader(tar!("link.tar"))); + let mut entries = ar.entries().unwrap(); + let link = entries.next().unwrap().unwrap(); assert_eq!( - t!(link.header().link_name()).as_ref().map(|p| &**p), + link.header().link_name().unwrap().as_deref(), Some(Path::new("file")) ); - let other = t!(entries.next().unwrap()); - assert!(t!(other.header().link_name()).is_none()); + let other = entries.next().unwrap().unwrap(); + assert!(other.header().link_name().unwrap().is_none()); } #[test] #[cfg(unix)] // making symlinks on windows is hard fn unpack_links() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); - let mut ar = Archive::new(Cursor::new(tar!("link.tar"))); - t!(ar.unpack(td.path())); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); + let mut ar = Archive::new(random_cursor_reader(tar!("link.tar"))); + ar.unpack(td.path()).unwrap(); - let md = t!(fs::symlink_metadata(td.path().join("lnk"))); + let md = fs::symlink_metadata(td.path().join("lnk")).unwrap(); assert!(md.file_type().is_symlink()); let mtime = FileTime::from_last_modification_time(&md); assert_eq!(mtime.unix_seconds(), 1448291033); assert_eq!( - &*t!(fs::read_link(td.path().join("lnk"))), + &*fs::read_link(td.path().join("lnk")).unwrap(), Path::new("file") ); - t!(File::open(td.path().join("lnk"))); + File::open(td.path().join("lnk")).unwrap(); } #[test] fn pax_size() { - let mut ar = Archive::new(tar!("pax_size.tar")); - let mut entries = t!(ar.entries()); - let mut entry = t!(entries.next().unwrap()); - let mut attributes = t!(entry.pax_extensions()).unwrap(); - - let _first = t!(attributes.next().unwrap()); - let _second = t!(attributes.next().unwrap()); - let _third = t!(attributes.next().unwrap()); - let fourth = t!(attributes.next().unwrap()); + let mut ar = Archive::new(random_cursor_reader(tar!("pax_size.tar"))); + let mut entries = ar.entries().unwrap(); + let mut entry = entries.next().unwrap().unwrap(); + let mut attributes = entry.pax_extensions().unwrap().unwrap(); + + let _first = attributes.next().unwrap().unwrap(); + let _second = attributes.next().unwrap().unwrap(); + let _third = attributes.next().unwrap().unwrap(); + let fourth = attributes.next().unwrap().unwrap(); assert!(attributes.next().is_none()); assert_eq!(fourth.key(), Ok("size")); @@ -936,14 +1001,14 @@ fn pax_size() { #[test] fn pax_simple() { - let mut ar = Archive::new(tar!("pax.tar")); - let mut entries = t!(ar.entries()); - - let mut first = t!(entries.next().unwrap()); - let mut attributes = t!(first.pax_extensions()).unwrap(); - let first = t!(attributes.next().unwrap()); - let second = t!(attributes.next().unwrap()); - let third = t!(attributes.next().unwrap()); + let mut ar = Archive::new(random_cursor_reader(tar!("pax.tar"))); + let mut entries = ar.entries().unwrap(); + + let mut first = entries.next().unwrap().unwrap(); + let mut attributes = first.pax_extensions().unwrap().unwrap(); + let first = attributes.next().unwrap().unwrap(); + let second = attributes.next().unwrap().unwrap(); + let third = attributes.next().unwrap().unwrap(); assert!(attributes.next().is_none()); assert_eq!(first.key(), Ok("mtime")); @@ -956,9 +1021,9 @@ fn pax_simple() { #[test] fn pax_simple_write() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let pax_path = td.path().join("pax.tar"); - let file: File = t!(File::create(&pax_path)); + let file: File = File::create(&pax_path).unwrap(); let mut ar: Builder> = Builder::new(BufWriter::new(file)); let pax_extensions = [ @@ -966,22 +1031,23 @@ fn pax_simple_write() { ("SCHILY.xattr.security.selinux", b"foo_t"), ]; - t!(ar.append_pax_extensions(pax_extensions)); - t!(ar.append_file("test2", &mut t!(File::open(&pax_path)))); - t!(ar.finish()); + ar.append_pax_extensions(pax_extensions).unwrap(); + ar.append_file("test2", &mut File::open(&pax_path).unwrap()) + .unwrap(); + ar.finish().unwrap(); drop(ar); - let mut archive_opened = Archive::new(t!(File::open(pax_path))); - let mut entries = t!(archive_opened.entries()); - let mut f: Entry = t!(entries.next().unwrap()); - let pax_headers = t!(f.pax_extensions()); + let mut archive_opened = Archive::new(File::open(pax_path).unwrap()); + let mut entries = archive_opened.entries().unwrap(); + let mut f: Entry = entries.next().unwrap().unwrap(); + let pax_headers = f.pax_extensions().unwrap(); assert!(pax_headers.is_some(), "pax_headers is None"); let mut pax_headers = pax_headers.unwrap(); - let pax_arbitrary = t!(pax_headers.next().unwrap()); + let pax_arbitrary = pax_headers.next().unwrap().unwrap(); assert_eq!(pax_arbitrary.key(), Ok("arbitrary_pax_key")); assert_eq!(pax_arbitrary.value(), Ok("arbitrary_pax_value")); - let xattr = t!(pax_headers.next().unwrap()); + let xattr = pax_headers.next().unwrap().unwrap(); assert_eq!(xattr.key().unwrap(), pax_extensions[1].0); assert_eq!(xattr.value_bytes(), pax_extensions[1].1); @@ -990,24 +1056,24 @@ fn pax_simple_write() { #[test] fn pax_path() { - let mut ar = Archive::new(tar!("pax2.tar")); - let mut entries = t!(ar.entries()); + let mut ar = Archive::new(random_cursor_reader(tar!("pax2.tar"))); + let mut entries = ar.entries().unwrap(); - let first = t!(entries.next().unwrap()); + let first = entries.next().unwrap().unwrap(); assert!(first.path().unwrap().ends_with("aaaaaaaaaaaaaaa")); } #[test] fn pax_linkpath() { - let mut ar = Archive::new(tar!("pax2.tar")); - let mut links = t!(ar.entries()).skip(3).take(2); + let mut ar = Archive::new(random_cursor_reader(tar!("pax2.tar"))); + let mut links = ar.entries().unwrap().skip(3).take(2); - let long_symlink = t!(links.next().unwrap()); + let long_symlink = links.next().unwrap().unwrap(); let link_name = long_symlink.link_name().unwrap().unwrap(); assert!(link_name.to_str().unwrap().len() > 99); assert!(link_name.ends_with("bbbbbbbbbbbbbbb")); - let long_hardlink = t!(links.next().unwrap()); + let long_hardlink = links.next().unwrap().unwrap(); let link_name = long_hardlink.link_name().unwrap().unwrap(); assert!(link_name.to_str().unwrap().len() > 99); assert!(link_name.ends_with("ccccccccccccccc")); @@ -1018,23 +1084,23 @@ fn long_name_trailing_nul() { let mut b = Builder::new(Vec::::new()); let mut h = Header::new_gnu(); - t!(h.set_path("././@LongLink")); + h.set_path("././@LongLink").unwrap(); h.set_size(4); h.set_entry_type(EntryType::new(b'L')); h.set_cksum(); - t!(b.append(&h, "foo\0".as_bytes())); + b.append(&h, "foo\0".as_bytes()).unwrap(); let mut h = Header::new_gnu(); - t!(h.set_path("bar")); + h.set_path("bar").unwrap(); h.set_size(6); h.set_entry_type(EntryType::file()); h.set_cksum(); - t!(b.append(&h, "foobar".as_bytes())); + b.append(&h, "foobar".as_bytes()).unwrap(); - let contents = t!(b.into_inner()); + let contents = b.into_inner().unwrap(); let mut a = Archive::new(&contents[..]); - let e = t!(t!(a.entries()).next().unwrap()); + let e = a.entries().unwrap().next().unwrap().unwrap(); assert_eq!(&*e.path_bytes(), b"foo"); } @@ -1043,23 +1109,23 @@ fn long_linkname_trailing_nul() { let mut b = Builder::new(Vec::::new()); let mut h = Header::new_gnu(); - t!(h.set_path("././@LongLink")); + h.set_path("././@LongLink").unwrap(); h.set_size(4); h.set_entry_type(EntryType::new(b'K')); h.set_cksum(); - t!(b.append(&h, "foo\0".as_bytes())); + b.append(&h, "foo\0".as_bytes()).unwrap(); let mut h = Header::new_gnu(); - t!(h.set_path("bar")); + h.set_path("bar").unwrap(); h.set_size(6); h.set_entry_type(EntryType::file()); h.set_cksum(); - t!(b.append(&h, "foobar".as_bytes())); + b.append(&h, "foobar".as_bytes()).unwrap(); - let contents = t!(b.into_inner()); + let contents = b.into_inner().unwrap(); let mut a = Archive::new(&contents[..]); - let e = t!(t!(a.entries()).next().unwrap()); + let e = a.entries().unwrap().next().unwrap().unwrap(); assert_eq!(&*e.link_name_bytes().unwrap(), b"foo"); } @@ -1072,12 +1138,12 @@ fn long_linkname_gnu() { h.set_size(0); let path = "usr/lib/.build-id/05/159ed904e45ff5100f7acd3d3b99fa7e27e34f"; let target = "../../../../usr/lib64/qt5/plugins/wayland-graphics-integration-server/libqt-wayland-compositor-xcomposite-egl.so"; - t!(b.append_link(&mut h, path, target)); + b.append_link(&mut h, path, target).unwrap(); - let contents = t!(b.into_inner()); + let contents = b.into_inner().unwrap(); let mut a = Archive::new(&contents[..]); - let e = &t!(t!(a.entries()).next().unwrap()); + let e = &a.entries().unwrap().next().unwrap().unwrap(); assert_eq!(e.header().entry_type(), t); assert_eq!(e.path().unwrap().to_str().unwrap(), path); assert_eq!(e.link_name().unwrap().unwrap().to_str().unwrap(), target); @@ -1094,12 +1160,12 @@ fn linkname_literal() { let path = "usr/lib/systemd/systemd-sysv-install"; let target = "../../..//sbin/chkconfig"; h.set_link_name_literal(target).unwrap(); - t!(b.append_data(&mut h, path, std::io::empty())); + b.append_data(&mut h, path, std::io::empty()).unwrap(); - let contents = t!(b.into_inner()); + let contents = b.into_inner().unwrap(); let mut a = Archive::new(&contents[..]); - let e = &t!(t!(a.entries()).next().unwrap()); + let e = &a.entries().unwrap().next().unwrap().unwrap(); assert_eq!(e.header().entry_type(), t); assert_eq!(e.path().unwrap().to_str().unwrap(), path); assert_eq!(e.link_name().unwrap().unwrap().to_str().unwrap(), target); @@ -1112,56 +1178,57 @@ fn append_writer() { let mut h = Header::new_gnu(); h.set_uid(42); - let mut writer = t!(b.append_writer(&mut h, "file1")); - t!(writer.write_all(b"foo")); - t!(writer.write_all(b"barbaz")); - t!(writer.finish()); + let mut writer = b.append_writer(&mut h, "file1").unwrap(); + writer.write_all(b"foo").unwrap(); + writer.write_all(b"barbaz").unwrap(); + writer.finish().unwrap(); let mut h = Header::new_gnu(); h.set_uid(43); let long_path: PathBuf = repeat("abcd").take(50).collect(); - let mut writer = t!(b.append_writer(&mut h, &long_path)); + let mut writer = b.append_writer(&mut h, &long_path).unwrap(); let long_data = repeat(b'x').take(513).collect::>(); - t!(writer.write_all(&long_data)); - t!(writer.finish()); + writer.write_all(&long_data).unwrap(); + writer.finish().unwrap(); - let contents = t!(b.into_inner()).into_inner(); + let contents = b.into_inner().unwrap().into_inner(); let mut ar = Archive::new(&contents[..]); - let mut entries = t!(ar.entries()); + let mut entries = ar.entries().unwrap(); - let e = &mut t!(entries.next().unwrap()); + let e = &mut entries.next().unwrap().unwrap(); assert_eq!(e.header().uid().unwrap(), 42); assert_eq!(&*e.path_bytes(), b"file1"); let mut r = Vec::new(); - t!(e.read_to_end(&mut r)); + e.read_to_end(&mut r).unwrap(); assert_eq!(&r[..], b"foobarbaz"); - let e = &mut t!(entries.next().unwrap()); + let e = &mut entries.next().unwrap().unwrap(); assert_eq!(e.header().uid().unwrap(), 43); - assert_eq!(t!(e.path()), long_path.as_path()); + assert_eq!(e.path().unwrap(), long_path.as_path()); let mut r = Vec::new(); - t!(e.read_to_end(&mut r)); + e.read_to_end(&mut r).unwrap(); assert_eq!(r.len(), 513); assert!(r.iter().all(|b| *b == b'x')); } #[test] fn encoded_long_name_has_trailing_nul() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let path = td.path().join("foo"); - t!(t!(File::create(&path)).write_all(b"test")); + File::create(&path).unwrap().write_all(b"test").unwrap(); let mut b = Builder::new(Vec::::new()); - let long = repeat("abcd").take(200).collect::(); + let long = "abcd".repeat(200); - t!(b.append_file(&long, &mut t!(File::open(&path)))); + b.append_file(&long, &mut File::open(&path).unwrap()) + .unwrap(); - let contents = t!(b.into_inner()); + let contents = b.into_inner().unwrap(); let mut a = Archive::new(&contents[..]); - let mut e = t!(t!(a.entries()).raw(true).next().unwrap()); + let mut e = a.entries().unwrap().raw(true).next().unwrap().unwrap(); let mut name = Vec::new(); - t!(e.read_to_end(&mut name)); + e.read_to_end(&mut name).unwrap(); assert_eq!(name[name.len() - 1], 0); let header_name = &e.header().as_gnu().unwrap().name; @@ -1170,28 +1237,28 @@ fn encoded_long_name_has_trailing_nul() { #[test] fn reading_sparse() { - let rdr = Cursor::new(tar!("sparse.tar")); + let rdr = random_cursor_reader(tar!("sparse.tar")); let mut ar = Archive::new(rdr); - let mut entries = t!(ar.entries()); + let mut entries = ar.entries().unwrap(); - let mut a = t!(entries.next().unwrap()); + let mut a = entries.next().unwrap().unwrap(); let mut s = String::new(); assert_eq!(&*a.header().path_bytes(), b"sparse_begin.txt"); - t!(a.read_to_string(&mut s)); + a.read_to_string(&mut s).unwrap(); assert_eq!(&s[..5], "test\n"); assert!(s[5..].chars().all(|x| x == '\u{0}')); - let mut a = t!(entries.next().unwrap()); + let mut a = entries.next().unwrap().unwrap(); let mut s = String::new(); assert_eq!(&*a.header().path_bytes(), b"sparse_end.txt"); - t!(a.read_to_string(&mut s)); + a.read_to_string(&mut s).unwrap(); assert!(s[..s.len() - 9].chars().all(|x| x == '\u{0}')); assert_eq!(&s[s.len() - 9..], "test_end\n"); - let mut a = t!(entries.next().unwrap()); + let mut a = entries.next().unwrap().unwrap(); let mut s = String::new(); assert_eq!(&*a.header().path_bytes(), b"sparse_ext.txt"); - t!(a.read_to_string(&mut s)); + a.read_to_string(&mut s).unwrap(); assert!(s[..0x1000].chars().all(|x| x == '\u{0}')); assert_eq!(&s[0x1000..0x1000 + 5], "text\n"); assert!(s[0x1000 + 5..0x3000].chars().all(|x| x == '\u{0}')); @@ -1205,10 +1272,10 @@ fn reading_sparse() { assert!(s[0x9000 + 5..0xb000].chars().all(|x| x == '\u{0}')); assert_eq!(&s[0xb000..0xb000 + 5], "text\n"); - let mut a = t!(entries.next().unwrap()); + let mut a = entries.next().unwrap().unwrap(); let mut s = String::new(); assert_eq!(&*a.header().path_bytes(), b"sparse.txt"); - t!(a.read_to_string(&mut s)); + a.read_to_string(&mut s).unwrap(); assert!(s[..0x1000].chars().all(|x| x == '\u{0}')); assert_eq!(&s[0x1000..0x1000 + 6], "hello\n"); assert!(s[0x1000 + 6..0x2fa0].chars().all(|x| x == '\u{0}')); @@ -1220,23 +1287,32 @@ fn reading_sparse() { #[test] fn extract_sparse() { - let rdr = Cursor::new(tar!("sparse.tar")); + let rdr = random_cursor_reader(tar!("sparse.tar")); let mut ar = Archive::new(rdr); - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); - t!(ar.unpack(td.path())); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); + ar.unpack(td.path()).unwrap(); let mut s = String::new(); - t!(t!(File::open(td.path().join("sparse_begin.txt"))).read_to_string(&mut s)); + File::open(td.path().join("sparse_begin.txt")) + .unwrap() + .read_to_string(&mut s) + .unwrap(); assert_eq!(&s[..5], "test\n"); assert!(s[5..].chars().all(|x| x == '\u{0}')); s.truncate(0); - t!(t!(File::open(td.path().join("sparse_end.txt"))).read_to_string(&mut s)); + File::open(td.path().join("sparse_end.txt")) + .unwrap() + .read_to_string(&mut s) + .unwrap(); assert!(s[..s.len() - 9].chars().all(|x| x == '\u{0}')); assert_eq!(&s[s.len() - 9..], "test_end\n"); s.truncate(0); - t!(t!(File::open(td.path().join("sparse_ext.txt"))).read_to_string(&mut s)); + File::open(td.path().join("sparse_ext.txt")) + .unwrap() + .read_to_string(&mut s) + .unwrap(); assert!(s[..0x1000].chars().all(|x| x == '\u{0}')); assert_eq!(&s[0x1000..0x1000 + 5], "text\n"); assert!(s[0x1000 + 5..0x3000].chars().all(|x| x == '\u{0}')); @@ -1251,7 +1327,10 @@ fn extract_sparse() { assert_eq!(&s[0xb000..0xb000 + 5], "text\n"); s.truncate(0); - t!(t!(File::open(td.path().join("sparse.txt"))).read_to_string(&mut s)); + File::open(td.path().join("sparse.txt")) + .unwrap() + .read_to_string(&mut s) + .unwrap(); assert!(s[..0x1000].chars().all(|x| x == '\u{0}')); assert_eq!(&s[0x1000..0x1000 + 6], "hello\n"); assert!(s[0x1000 + 6..0x2fa0].chars().all(|x| x == '\u{0}')); @@ -1261,24 +1340,24 @@ fn extract_sparse() { #[test] fn large_sparse() { - let rdr = Cursor::new(tar!("sparse-large.tar")); + let rdr = random_cursor_reader(tar!("sparse-large.tar")); let mut ar = Archive::new(rdr); - let mut entries = t!(ar.entries()); + let mut entries = ar.entries().unwrap(); // Only check the header info without extracting, as the file is very large, // and not all filesystems support sparse files. - let a = t!(entries.next().unwrap()); + let a = entries.next().unwrap().unwrap(); let h = a.header().as_gnu().unwrap(); assert_eq!(h.real_size().unwrap(), 12626929280); } #[test] fn sparse_with_trailing() { - let rdr = Cursor::new(tar!("sparse-1.tar")); + let rdr = random_cursor_reader(tar!("sparse-1.tar")); let mut ar = Archive::new(rdr); - let mut entries = t!(ar.entries()); - let mut a = t!(entries.next().unwrap()); + let mut entries = ar.entries().unwrap(); + let mut a = entries.next().unwrap().unwrap(); let mut s = String::new(); - t!(a.read_to_string(&mut s)); + a.read_to_string(&mut s).unwrap(); assert_eq!(0x100_00c, s.len()); assert_eq!(&s[..0xc], "0MB through\n"); assert!(s[0xc..0x100_000].chars().all(|x| x == '\u{0}')); @@ -1286,29 +1365,32 @@ fn sparse_with_trailing() { } #[test] +#[allow(clippy::option_map_unit_fn)] fn writing_sparse() { let mut ar = Builder::new(Vec::new()); - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let mut files = Vec::new(); let mut append_file = |name: &str, chunks: &[(u64, u64)]| { let path = td.path().join(name); - let mut file = t!(File::create(&path)); - t!(file.set_len( + let mut file = File::create(&path).unwrap(); + file.set_len( chunks .iter() .map(|&(off, len)| off + len) .max() .unwrap_or(0), - )); + ) + .unwrap(); for (i, &(off, len)) in chunks.iter().enumerate() { - t!(file.seek(io::SeekFrom::Start(off))); + file.seek(io::SeekFrom::Start(off)).unwrap(); let mut data = vec![i as u8 + b'a'; len as usize]; data.first_mut().map(|x| *x = b'['); data.last_mut().map(|x| *x = b']'); - t!(file.write_all(&data)); + file.write_all(&data).unwrap(); } - t!(ar.append_path_with_name(&path, path.file_name().unwrap())); + ar.append_path_with_name(&path, path.file_name().unwrap()) + .unwrap(); files.push(path); }; @@ -1320,9 +1402,9 @@ fn writing_sparse() { append_file("x_x_", &[(0, 0x1_000), (0x20_000, 0x1_000), (0x40_000, 0)]); append_file("uneven", &[(0x20_333, 0x555), (0x40_777, 0x999)]); - t!(ar.finish()); + ar.finish().unwrap(); - let data = t!(ar.into_inner()); + let data = ar.into_inner().unwrap(); // Without sparse support, the size of the tarball exceed 1MiB. #[cfg(target_os = "linux")] @@ -1331,14 +1413,14 @@ fn writing_sparse() { assert!(data.len() <= 273 * 1024); // UFS (defaults to 32k block size, last block isn't a hole) let mut ar = Archive::new(&data[..]); - let mut entries = t!(ar.entries()); + let mut entries = ar.entries().unwrap(); for path in files { - let mut f = t!(entries.next().unwrap()); + let mut f = entries.next().unwrap().unwrap(); let mut s = String::new(); - t!(f.read_to_string(&mut s)); + f.read_to_string(&mut s).unwrap(); - let expected = t!(fs::read_to_string(&path)); + let expected = fs::read_to_string(&path).unwrap(); assert!(s == expected, "path: {path:?}"); } @@ -1349,10 +1431,10 @@ fn writing_sparse() { #[test] fn path_separators() { let mut ar = Builder::new(Vec::new()); - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let path = td.path().join("test"); - t!(t!(File::create(&path)).write_all(b"test")); + File::create(&path).unwrap().write_all(b"test").unwrap(); let short_path: PathBuf = repeat("abcd").take(2).collect(); let long_path: PathBuf = repeat("abcd").take(50).collect(); @@ -1360,29 +1442,31 @@ fn path_separators() { // Make sure UStar headers normalize to Unix path separators let mut header = Header::new_ustar(); - t!(header.set_path(&short_path)); - assert_eq!(t!(header.path()), short_path); + header.set_path(&short_path).unwrap(); + assert_eq!(header.path().unwrap(), short_path); assert!(!header.path_bytes().contains(&b'\\')); - t!(header.set_path(&long_path)); - assert_eq!(t!(header.path()), long_path); + header.set_path(&long_path).unwrap(); + assert_eq!(header.path().unwrap(), long_path); assert!(!header.path_bytes().contains(&b'\\')); // Make sure GNU headers normalize to Unix path separators, // including the `@LongLink` fallback used by `append_file`. - t!(ar.append_file(&short_path, &mut t!(File::open(&path)))); - t!(ar.append_file(&long_path, &mut t!(File::open(&path)))); + ar.append_file(&short_path, &mut File::open(&path).unwrap()) + .unwrap(); + ar.append_file(&long_path, &mut File::open(&path).unwrap()) + .unwrap(); - let rd = Cursor::new(t!(ar.into_inner())); + let rd = Cursor::new(ar.into_inner().unwrap()); let mut ar = Archive::new(rd); - let mut entries = t!(ar.entries()); + let mut entries = ar.entries().unwrap(); - let entry = t!(entries.next().unwrap()); - assert_eq!(t!(entry.path()), short_path); + let entry = entries.next().unwrap().unwrap(); + assert_eq!(entry.path().unwrap(), short_path); assert!(!entry.path_bytes().contains(&b'\\')); - let entry = t!(entries.next().unwrap()); - assert_eq!(t!(entry.path()), long_path); + let entry = entries.next().unwrap().unwrap(); + assert_eq!(entry.path().unwrap(), long_path); assert!(!entry.path_bytes().contains(&b'\\')); assert!(entries.next().is_none()); @@ -1397,81 +1481,81 @@ fn append_path_symlink() { let mut ar = Builder::new(Vec::new()); ar.follow_symlinks(false); - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); - let long_linkname = repeat("abcd").take(30).collect::(); - let long_pathname = repeat("dcba").take(30).collect::(); - t!(env::set_current_dir(td.path())); + let long_linkname = "abcd".repeat(30); + let long_pathname = "dcba".repeat(30); + env::set_current_dir(td.path()).unwrap(); // "short" path name / short link name - t!(symlink("testdest", "test")); - t!(ar.append_path("test")); + symlink("testdest", "test").unwrap(); + ar.append_path("test").unwrap(); // short path name / long link name - t!(symlink(&long_linkname, "test2")); - t!(ar.append_path("test2")); + symlink(&long_linkname, "test2").unwrap(); + ar.append_path("test2").unwrap(); // long path name / long link name - t!(symlink(&long_linkname, &long_pathname)); - t!(ar.append_path(&long_pathname)); + symlink(&long_linkname, &long_pathname).unwrap(); + ar.append_path(&long_pathname).unwrap(); - let rd = Cursor::new(t!(ar.into_inner())); + let rd = Cursor::new(ar.into_inner().unwrap()); let mut ar = Archive::new(rd); - let mut entries = t!(ar.entries()); + let mut entries = ar.entries().unwrap(); - let entry = t!(entries.next().unwrap()); - assert_eq!(t!(entry.path()), Path::new("test")); + let entry = entries.next().unwrap().unwrap(); + assert_eq!(entry.path().unwrap(), Path::new("test")); assert_eq!( - t!(entry.link_name()), + entry.link_name().unwrap(), Some(Cow::from(Path::new("testdest"))) ); - assert_eq!(t!(entry.header().size()), 0); + assert_eq!(entry.header().size().unwrap(), 0); - let entry = t!(entries.next().unwrap()); - assert_eq!(t!(entry.path()), Path::new("test2")); + let entry = entries.next().unwrap().unwrap(); + assert_eq!(entry.path().unwrap(), Path::new("test2")); assert_eq!( - t!(entry.link_name()), + entry.link_name().unwrap(), Some(Cow::from(Path::new(&long_linkname))) ); - assert_eq!(t!(entry.header().size()), 0); + assert_eq!(entry.header().size().unwrap(), 0); - let entry = t!(entries.next().unwrap()); - assert_eq!(t!(entry.path()), Path::new(&long_pathname)); + let entry = entries.next().unwrap().unwrap(); + assert_eq!(entry.path().unwrap(), Path::new(&long_pathname)); assert_eq!( - t!(entry.link_name()), + entry.link_name().unwrap(), Some(Cow::from(Path::new(&long_linkname))) ); - assert_eq!(t!(entry.header().size()), 0); + assert_eq!(entry.header().size().unwrap(), 0); assert!(entries.next().is_none()); } #[test] fn name_with_slash_doesnt_fool_long_link_and_bsd_compat() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let mut ar = Builder::new(Vec::new()); let mut h = Header::new_gnu(); - t!(h.set_path("././@LongLink")); + h.set_path("././@LongLink").unwrap(); h.set_size(4); h.set_entry_type(EntryType::new(b'L')); h.set_cksum(); - t!(ar.append(&h, "foo\0".as_bytes())); + ar.append(&h, "foo\0".as_bytes()).unwrap(); let mut header = Header::new_gnu(); header.set_entry_type(EntryType::Regular); - t!(header.set_path("testdir/")); + header.set_path("testdir/").unwrap(); header.set_size(0); header.set_cksum(); - t!(ar.append(&header, &mut io::empty())); + ar.append(&header, &mut io::empty()).unwrap(); // Extracting - let rdr = Cursor::new(t!(ar.into_inner())); + let rdr = Cursor::new(ar.into_inner().unwrap()); let mut ar = Archive::new(rdr); - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); // Iterating let rdr = Cursor::new(ar.into_inner().into_inner()); let mut ar = Archive::new(rdr); - assert!(t!(ar.entries()).all(|fr| fr.is_ok())); + assert!(ar.entries().unwrap().all(|fr| fr.is_ok())); assert!(td.path().join("foo").is_file()); } @@ -1479,21 +1563,21 @@ fn name_with_slash_doesnt_fool_long_link_and_bsd_compat() { #[test] fn insert_local_file_different_name() { let mut ar = Builder::new(Vec::new()); - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let path = td.path().join("directory"); - t!(fs::create_dir(&path)); + fs::create_dir(&path).unwrap(); ar.append_path_with_name(&path, "archive/dir").unwrap(); let path = td.path().join("file"); - t!(t!(File::create(&path)).write_all(b"test")); + File::create(&path).unwrap().write_all(b"test").unwrap(); ar.append_path_with_name(&path, "archive/dir/f").unwrap(); - let rd = Cursor::new(t!(ar.into_inner())); + let rd = Cursor::new(ar.into_inner().unwrap()); let mut ar = Archive::new(rd); - let mut entries = t!(ar.entries()); - let entry = t!(entries.next().unwrap()); - assert_eq!(t!(entry.path()), Path::new("archive/dir")); - let entry = t!(entries.next().unwrap()); - assert_eq!(t!(entry.path()), Path::new("archive/dir/f")); + let mut entries = ar.entries().unwrap(); + let entry = entries.next().unwrap().unwrap(); + assert_eq!(entry.path().unwrap(), Path::new("archive/dir")); + let entry = entries.next().unwrap().unwrap(); + assert_eq!(entry.path().unwrap(), Path::new("archive/dir/f")); assert!(entries.next().is_none()); } @@ -1502,11 +1586,11 @@ fn insert_local_file_different_name() { fn tar_directory_containing_symlink_to_directory() { use std::os::unix::fs::symlink; - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); - let dummy_src = t!(TempBuilder::new().prefix("dummy_src").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); + let dummy_src = TempBuilder::new().prefix("dummy_src").tempdir().unwrap(); let dummy_dst = td.path().join("dummy_dst"); let mut ar = Builder::new(Vec::new()); - t!(symlink(dummy_src.path().display().to_string(), &dummy_dst)); + symlink(dummy_src.path().display().to_string(), &dummy_dst).unwrap(); assert!(dummy_dst.read_link().is_ok()); assert!(dummy_dst.read_link().unwrap().is_dir()); @@ -1516,8 +1600,8 @@ fn tar_directory_containing_symlink_to_directory() { #[test] fn long_path() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); - let rdr = Cursor::new(tar!("7z_long_path.tar")); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); + let rdr = random_cursor_reader(tar!("7z_long_path.tar")); let mut ar = Archive::new(rdr); assert!(ar.unpack(td.path()).is_ok()); } @@ -1527,9 +1611,12 @@ fn unpack_path_larger_than_windows_max_path() { let dir_name = "iamaprettylongnameandtobepreciseiam91characterslongwhichsomethinkisreallylongandothersdonot"; // 183 character directory name let really_long_path = format!("{}{}", dir_name, dir_name); - let td = t!(TempBuilder::new().prefix(&really_long_path).tempdir()); + let td = TempBuilder::new() + .prefix(&really_long_path) + .tempdir() + .unwrap(); // directory in 7z_long_path.tar is over 100 chars - let rdr = Cursor::new(tar!("7z_long_path.tar")); + let rdr = random_cursor_reader(tar!("7z_long_path.tar")); let mut ar = Archive::new(rdr); // should unpack path greater than windows MAX_PATH length of 260 characters assert!(ar.unpack(td.path()).is_ok()); @@ -1550,26 +1637,26 @@ fn append_long_multibyte() { #[test] fn read_only_directory_containing_files() { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let mut b = Builder::new(Vec::::new()); let mut h = Header::new_gnu(); - t!(h.set_path("dir/")); + h.set_path("dir/").unwrap(); h.set_size(0); h.set_entry_type(EntryType::dir()); h.set_mode(0o444); h.set_cksum(); - t!(b.append(&h, "".as_bytes())); + b.append(&h, "".as_bytes()).unwrap(); let mut h = Header::new_gnu(); - t!(h.set_path("dir/file")); + h.set_path("dir/file").unwrap(); h.set_size(2); h.set_entry_type(EntryType::file()); h.set_cksum(); - t!(b.append(&h, "hi".as_bytes())); + b.append(&h, "hi".as_bytes()).unwrap(); - let contents = t!(b.into_inner()); + let contents = b.into_inner().unwrap(); let mut ar = Archive::new(&contents[..]); assert!(ar.unpack(td.path()).is_ok()); } @@ -1581,11 +1668,11 @@ fn tar_directory_containing_special_files() { use std::env; use std::ffi::CString; - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let fifo = td.path().join("fifo"); unsafe { - let fifo_path = t!(CString::new(fifo.to_str().unwrap())); + let fifo_path = CString::new(fifo.to_str().unwrap()).unwrap(); let ret = libc::mknod(fifo_path.as_ptr(), libc::S_IFIFO | 0o644, 0); if ret != 0 { libc::perror(fifo_path.as_ptr()); @@ -1593,15 +1680,15 @@ fn tar_directory_containing_special_files() { } } - t!(env::set_current_dir(td.path())); + env::set_current_dir(td.path()).unwrap(); let mut ar = Builder::new(Vec::new()); // append_path has a different logic for processing files, so we need to test it as well - t!(ar.append_path("fifo")); - t!(ar.append_dir_all("special", td.path())); - t!(env::set_current_dir("/dev/")); + ar.append_path("fifo").unwrap(); + ar.append_dir_all("special", td.path()).unwrap(); + env::set_current_dir("/dev/").unwrap(); // CI systems seem to have issues with creating a chr device - t!(ar.append_path("null")); - t!(ar.finish()); + ar.append_path("null").unwrap(); + ar.finish().unwrap(); } #[test] @@ -1611,8 +1698,8 @@ fn header_size_overflow() { let mut header = Header::new_gnu(); header.set_size(u64::MAX); header.set_cksum(); - ar.append(&mut header, "x".as_bytes()).unwrap(); - let result = t!(ar.into_inner()); + ar.append(&header, "x".as_bytes()).unwrap(); + let result = ar.into_inner().unwrap(); let mut ar = Archive::new(&result[..]); let mut e = ar.entries().unwrap(); let err = e.next().unwrap().err().unwrap(); @@ -1627,12 +1714,12 @@ fn header_size_overflow() { let mut header = Header::new_gnu(); header.set_size(1_000); header.set_cksum(); - ar.append(&mut header, &[0u8; 1_000][..]).unwrap(); + ar.append(&header, &[0u8; 1_000][..]).unwrap(); let mut header = Header::new_gnu(); header.set_size(u64::MAX - 513); header.set_cksum(); - ar.append(&mut header, "x".as_bytes()).unwrap(); - let result = t!(ar.into_inner()); + ar.append(&header, "x".as_bytes()).unwrap(); + let result = ar.into_inner().unwrap(); let mut ar = Archive::new(&result[..]); let mut e = ar.entries().unwrap(); e.next().unwrap().unwrap(); @@ -1656,39 +1743,40 @@ fn ownership_preserving() { // file 1 with uid = 580800000, gid = 580800000 header.set_gid(580800000); header.set_uid(580800000); - t!(header.set_path("iamuid580800000")); + header.set_path("iamuid580800000").unwrap(); header.set_size(0); header.set_cksum(); - t!(ar.append(&header, data)); + ar.append(&header, data).unwrap(); // file 2 with uid = 580800001, gid = 580800000 header.set_uid(580800001); - t!(header.set_path("iamuid580800001")); + header.set_path("iamuid580800001").unwrap(); header.set_cksum(); - t!(ar.append(&header, data)); + ar.append(&header, data).unwrap(); // file 3 with uid = 580800002, gid = 580800002 header.set_gid(580800002); header.set_uid(580800002); - t!(header.set_path("iamuid580800002")); + header.set_path("iamuid580800002").unwrap(); header.set_cksum(); - t!(ar.append(&header, data)); + ar.append(&header, data).unwrap(); // directory 1 with uid = 580800002, gid = 580800002 header.set_entry_type(EntryType::Directory); header.set_gid(580800002); header.set_uid(580800002); - t!(header.set_path("iamuid580800002dir")); + header.set_path("iamuid580800002dir").unwrap(); header.set_cksum(); - t!(ar.append(&header, data)); + ar.append(&header, data).unwrap(); // symlink to file 1 header.set_entry_type(EntryType::Symlink); header.set_gid(580800002); header.set_uid(580800002); - t!(header.set_path("iamuid580800000symlink")); + header.set_path("iamuid580800000symlink").unwrap(); header.set_cksum(); - t!(ar.append_link(&mut header, "iamuid580800000symlink", "iamuid580800000")); - t!(ar.finish()); + ar.append_link(&mut header, "iamuid580800000symlink", "iamuid580800000") + .unwrap(); + ar.finish().unwrap(); - let rdr = Cursor::new(t!(ar.into_inner())); - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); + let rdr = Cursor::new(ar.into_inner().unwrap()); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); let mut ar = Archive::new(rdr); ar.set_preserve_ownerships(true); @@ -1724,13 +1812,13 @@ fn pax_and_gnu_uid_gid() { let tarlist = [tar!("biguid_gnu.tar"), tar!("biguid_pax.tar")]; for file in &tarlist { - let td = t!(TempBuilder::new().prefix("tar-rs").tempdir()); - let rdr = Cursor::new(file); + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); + let rdr = random_cursor_reader(file); let mut ar = Archive::new(rdr); ar.set_preserve_ownerships(true); if unsafe { libc::getuid() } == 0 { - t!(ar.unpack(td.path())); + ar.unpack(td.path()).unwrap(); let meta = fs::metadata(td.path().join("test.txt")).unwrap(); let uid = std::os::unix::prelude::MetadataExt::uid(&meta); let gid = std::os::unix::prelude::MetadataExt::gid(&meta); @@ -1744,3 +1832,193 @@ fn pax_and_gnu_uid_gid() { } } } + +#[test] +fn append_data_error_does_not_corrupt_subsequent_entries() { + // When append_data fails (e.g., path contains ".."), subsequent + // successful writes must not be corrupted by an orphaned GNU + // long-name extension entry left in the stream. + let mut ar = Builder::new(Vec::new()); + + // First write: a long path (>100 bytes to trigger GNU long-name extension) + // containing ".." not as the last component, which will fail validation + // in set_truncated_path_for_gnu_header. + let dotdot_path = "a/../b/".to_string() + &"c".repeat(100); + let mut header = Header::new_gnu(); + header.set_size(5); + header.set_cksum(); + let result = ar.append_data(&mut header, &dotdot_path, &b"first"[..]); + assert!(result.is_err()); + + // Second write: a clean path that should succeed normally. + let mut header = Header::new_gnu(); + header.set_size(6); + header.set_cksum(); + ar.append_data(&mut header, "clean.txt", &b"second"[..]) + .unwrap(); + + // Verify: the archive should contain exactly one entry at "clean.txt" + // with content "second". Before the fix, it contained an entry at the + // dotdot path with content "second" — the orphaned long-name stole the data. + let data = ar.into_inner().unwrap(); + let mut archive = Archive::new(&data[..]); + let entries: Vec<_> = archive + .entries() + .unwrap() + .collect::>() + .unwrap(); + + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].path().unwrap().to_str().unwrap(), "clean.txt"); +} + +/// Build the PAX size smuggling archive described in the original report. +/// +/// A PAX extended header declares `size=2048` for a regular file whose +/// actual header size field is 8. A symlink entry is hidden inside the +/// inflated region. A correct parser honours the PAX size and skips over +/// the symlink; a buggy one reads only the header size and exposes it. +fn build_pax_smuggle_archive() -> Vec { + const B: usize = 512; + const INFLATED: usize = 2048; + let end_of_archive = || std::iter::repeat(0u8).take(B * 2); + + let mut ar: Vec = Vec::new(); + + // PAX extended header declaring size=2048 for the next entry. + let pax_rec = format!("13 size={INFLATED}\n"); + let mut pax_hdr = Header::new_ustar(); + pax_hdr.set_path("./PaxHeaders/regular").unwrap(); + pax_hdr.set_size(pax_rec.as_bytes().len() as u64); + pax_hdr.set_entry_type(EntryType::XHeader); + pax_hdr.set_cksum(); + ar.extend_from_slice(pax_hdr.as_bytes()); + ar.extend_from_slice(pax_rec.as_bytes()); + ar.resize(ar.len().next_multiple_of(B), 0); + + // Regular file whose header says size=8, but PAX says 2048. + let content = b"regular\n"; + let mut file_hdr = Header::new_ustar(); + file_hdr.set_path("regular.txt").unwrap(); + file_hdr.set_size(content.len() as u64); + file_hdr.set_entry_type(EntryType::Regular); + file_hdr.set_cksum(); + ar.extend_from_slice(file_hdr.as_bytes()); + let mark = ar.len(); + ar.extend_from_slice(content); + ar.resize(ar.len().next_multiple_of(B), 0); + + // Smuggled symlink hidden in the inflated region. + let mut sym_hdr = Header::new_ustar(); + sym_hdr.set_path("smuggled").unwrap(); + sym_hdr.set_size(0); + sym_hdr.set_entry_type(EntryType::Symlink); + sym_hdr.set_link_name("/etc/shadow").unwrap(); + sym_hdr.set_cksum(); + ar.extend_from_slice(sym_hdr.as_bytes()); + ar.extend(end_of_archive()); + + // Pad to fill the inflated window. + let used = ar.len() - mark; + let pad = INFLATED.saturating_sub(used); + ar.extend(std::iter::repeat(0u8).take(pad.next_multiple_of(B))); + + // End-of-archive. + ar.extend(end_of_archive()); + ar +} + +/// Regression test for PAX size smuggling. +/// +/// A crafted archive uses a PAX extended header to declare a file size (2048) +/// larger than the header's octal size field (8). Before the fix, `tar-rs` +/// only applied the PAX size override when the header size was 0, so it would +/// read the small header size, advance too little, and expose a symlink entry +/// hidden in the "padding" area. After the fix, the PAX size unconditionally +/// overrides the header size, causing the parser to skip over the smuggled +/// symlink — matching the behavior of compliant parsers. +#[test] +fn pax_size_smuggled_symlink() { + let data = build_pax_smuggle_archive(); + + let mut archive = Archive::new(random_cursor_reader(&data[..])); + let entries: Vec<_> = archive + .entries() + .unwrap() + .map(|e| { + let e = e.unwrap(); + let path = e.path().unwrap().to_path_buf(); + let kind = e.header().entry_type(); + let link = e.link_name().unwrap().map(|l| l.to_path_buf()); + (path, kind, link) + }) + .collect(); + + // With the fix applied, only "regular.txt" should be visible. + // The smuggled symlink must NOT appear. + let expected: Vec<(PathBuf, EntryType, Option)> = + vec![(PathBuf::from("regular.txt"), EntryType::Regular, None)]; + assert_eq!( + entries, expected, + "smuggled symlink visible or unexpected entries\n\ + got: {entries:?}" + ); +} + +/// Cross-validate that `tar` and `astral-tokio-tar` parse the PAX size +/// smuggling archive identically, guarding against parsing differentials. +#[tokio::test] +async fn pax_size_smuggle_matches_astral_tokio_tar() { + use tokio_stream::StreamExt; + + let data = build_pax_smuggle_archive(); + + // Parse with sync tar. + let sync_entries: Vec<_> = { + let mut ar = Archive::new(&data[..]); + ar.entries() + .unwrap() + .map(|e| { + let e = e.unwrap(); + let path = e.path().unwrap().to_path_buf(); + let kind = e.header().entry_type(); + let link = e.link_name().unwrap().map(|l| l.to_path_buf()); + (path, kind, link) + }) + .collect() + }; + + // Parse with async astral-tokio-tar. + let async_entries: Vec<_> = { + let mut ar = tokio_tar::Archive::new(&data[..]); + let mut entries = ar.entries().unwrap(); + let mut result = Vec::new(); + while let Some(e) = entries.next().await { + let e = e.unwrap(); + let entry_type = e.header().entry_type(); + result.push(( + e.path().unwrap().to_path_buf(), + // Map through the raw byte so the two crates' EntryTypes compare. + EntryType::new(entry_type.as_byte()), + e.link_name().unwrap().map(|l| l.to_path_buf()), + )); + } + result + }; + + // Assert exact expected content for both parsers independently, + // so we verify correctness — not just mutual agreement. + let expected: Vec<(PathBuf, EntryType, Option)> = + vec![(PathBuf::from("regular.txt"), EntryType::Regular, None)]; + + assert_eq!( + sync_entries, expected, + "tar-rs produced unexpected entries (smuggled symlink visible?)\n\ + got: {sync_entries:?}" + ); + assert_eq!( + async_entries, expected, + "astral-tokio-tar produced unexpected entries (smuggled symlink visible?)\n\ + got: {async_entries:?}" + ); +} diff --git a/tests/entry.rs b/tests/entry.rs index 4d612e2c..6428b968 100644 --- a/tests/entry.rs +++ b/tests/entry.rs @@ -7,15 +7,6 @@ use std::io::Read; use tempfile::Builder; -macro_rules! t { - ($e:expr) => { - match $e { - Ok(v) => v, - Err(e) => panic!("{} returned {}", stringify!($e), e), - } - }; -} - #[test] fn absolute_symlink() { let mut ar = tar::Builder::new(Vec::new()); @@ -23,52 +14,52 @@ fn absolute_symlink() { let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Symlink); - t!(header.set_path("foo")); - t!(header.set_link_name("/bar")); + header.set_path("foo").unwrap(); + header.set_link_name("/bar").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let td = t!(Builder::new().prefix("tar").tempdir()); - t!(ar.unpack(td.path())); + let td = Builder::new().prefix("tar").tempdir().unwrap(); + ar.unpack(td.path()).unwrap(); - t!(td.path().join("foo").symlink_metadata()); + td.path().join("foo").symlink_metadata().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let mut entries = t!(ar.entries()); - let entry = t!(entries.next().unwrap()); + let mut entries = ar.entries().unwrap(); + let entry = entries.next().unwrap().unwrap(); assert_eq!(&*entry.link_name_bytes().unwrap(), b"/bar"); } #[test] fn absolute_hardlink() { - let td = t!(Builder::new().prefix("tar").tempdir()); + let td = Builder::new().prefix("tar").tempdir().unwrap(); let mut ar = tar::Builder::new(Vec::new()); let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("foo")); + header.set_path("foo").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Link); - t!(header.set_path("bar")); + header.set_path("bar").unwrap(); // This absolute path under tempdir will be created at unpack time - t!(header.set_link_name(td.path().join("foo"))); + header.set_link_name(td.path().join("foo")).unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - t!(ar.unpack(td.path())); - t!(td.path().join("foo").metadata()); - t!(td.path().join("bar").metadata()); + ar.unpack(td.path()).unwrap(); + td.path().join("foo").metadata().unwrap(); + td.path().join("bar").metadata().unwrap(); } #[test] @@ -78,25 +69,25 @@ fn relative_hardlink() { let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("foo")); + header.set_path("foo").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Link); - t!(header.set_path("bar")); - t!(header.set_link_name("foo")); + header.set_path("bar").unwrap(); + header.set_link_name("foo").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let td = t!(Builder::new().prefix("tar").tempdir()); - t!(ar.unpack(td.path())); - t!(td.path().join("foo").metadata()); - t!(td.path().join("bar").metadata()); + let td = Builder::new().prefix("tar").tempdir().unwrap(); + ar.unpack(td.path()).unwrap(); + td.path().join("foo").metadata().unwrap(); + td.path().join("bar").metadata().unwrap(); } #[test] @@ -106,24 +97,24 @@ fn absolute_link_deref_error() { let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Symlink); - t!(header.set_path("foo")); - t!(header.set_link_name("/")); + header.set_path("foo").unwrap(); + header.set_link_name("/").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("foo/bar")); + header.set_path("foo/bar").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let td = t!(Builder::new().prefix("tar").tempdir()); + let td = Builder::new().prefix("tar").tempdir().unwrap(); assert!(ar.unpack(td.path()).is_err()); - t!(td.path().join("foo").symlink_metadata()); + td.path().join("foo").symlink_metadata().unwrap(); assert!(File::open(td.path().join("foo").join("bar")).is_err()); } @@ -134,24 +125,24 @@ fn relative_link_deref_error() { let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Symlink); - t!(header.set_path("foo")); - t!(header.set_link_name("../../../../")); + header.set_path("foo").unwrap(); + header.set_link_name("../../../../").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("foo/bar")); + header.set_path("foo/bar").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let td = t!(Builder::new().prefix("tar").tempdir()); + let td = Builder::new().prefix("tar").tempdir().unwrap(); assert!(ar.unpack(td.path()).is_err()); - t!(td.path().join("foo").symlink_metadata()); + td.path().join("foo").symlink_metadata().unwrap(); assert!(File::open(td.path().join("foo").join("bar")).is_err()); } @@ -165,18 +156,18 @@ fn directory_maintains_permissions() { let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Directory); - t!(header.set_path("foo")); + header.set_path("foo").unwrap(); header.set_mode(0o777); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let td = t!(Builder::new().prefix("tar").tempdir()); - t!(ar.unpack(td.path())); - let f = t!(File::open(td.path().join("foo"))); - let md = t!(f.metadata()); + let td = Builder::new().prefix("tar").tempdir().unwrap(); + ar.unpack(td.path()).unwrap(); + let f = File::open(td.path().join("foo")).unwrap(); + let md = f.metadata().unwrap(); assert!(md.is_dir()); assert_eq!(md.permissions().mode(), 0o40777); } @@ -191,23 +182,23 @@ fn set_entry_mask() { let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("foo")); + header.set_path("foo").unwrap(); header.set_mode(0o777); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let td = t!(Builder::new().prefix("tar").tempdir()); + let td = Builder::new().prefix("tar").tempdir().unwrap(); let foo_path = td.path().join("foo"); - let mut entries = t!(ar.entries()); - let mut foo = t!(entries.next().unwrap()); + let mut entries = ar.entries().unwrap(); + let mut foo = entries.next().unwrap().unwrap(); foo.set_mask(0o027); - t!(foo.unpack(&foo_path)); + foo.unpack(&foo_path).unwrap(); - let f = t!(File::open(foo_path)); - let md = t!(f.metadata()); + let f = File::open(foo_path).unwrap(); + let md = f.metadata().unwrap(); assert!(md.is_file()); assert_eq!(md.permissions().mode(), 0o100750); } @@ -220,35 +211,35 @@ fn modify_link_just_created() { let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Symlink); - t!(header.set_path("foo")); - t!(header.set_link_name("bar")); + header.set_path("foo").unwrap(); + header.set_link_name("bar").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("bar/foo")); + header.set_path("bar/foo").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("foo/bar")); + header.set_path("foo/bar").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let td = t!(Builder::new().prefix("tar").tempdir()); - t!(ar.unpack(td.path())); + let td = Builder::new().prefix("tar").tempdir().unwrap(); + ar.unpack(td.path()).unwrap(); - t!(File::open(td.path().join("bar/foo"))); - t!(File::open(td.path().join("bar/bar"))); - t!(File::open(td.path().join("foo/foo"))); - t!(File::open(td.path().join("foo/bar"))); + File::open(td.path().join("bar/foo")).unwrap(); + File::open(td.path().join("bar/bar")).unwrap(); + File::open(td.path().join("foo/foo")).unwrap(); + File::open(td.path().join("foo/bar")).unwrap(); } #[test] @@ -259,22 +250,22 @@ fn modify_outside_with_relative_symlink() { let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Symlink); - t!(header.set_path("symlink")); - t!(header.set_link_name("..")); + header.set_path("symlink").unwrap(); + header.set_link_name("..").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("symlink/foo/bar")); + header.set_path("symlink/foo/bar").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let td = t!(Builder::new().prefix("tar").tempdir()); + let td = Builder::new().prefix("tar").tempdir().unwrap(); let tar_dir = td.path().join("tar"); create_dir(&tar_dir).unwrap(); assert!(ar.unpack(tar_dir).is_err()); @@ -288,24 +279,24 @@ fn parent_paths_error() { let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Symlink); - t!(header.set_path("foo")); - t!(header.set_link_name("..")); + header.set_path("foo").unwrap(); + header.set_link_name("..").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("foo/bar")); + header.set_path("foo/bar").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let td = t!(Builder::new().prefix("tar").tempdir()); + let td = Builder::new().prefix("tar").tempdir().unwrap(); assert!(ar.unpack(td.path()).is_err()); - t!(td.path().join("foo").symlink_metadata()); + td.path().join("foo").symlink_metadata().unwrap(); assert!(File::open(td.path().join("foo").join("bar")).is_err()); } @@ -318,26 +309,28 @@ fn good_parent_paths_ok() { let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Symlink); - t!(header.set_path(PathBuf::from("foo").join("bar"))); - t!(header.set_link_name(PathBuf::from("..").join("bar"))); + header.set_path(PathBuf::from("foo").join("bar")).unwrap(); + header + .set_link_name(PathBuf::from("..").join("bar")) + .unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("bar")); + header.set_path("bar").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let td = t!(Builder::new().prefix("tar").tempdir()); - t!(ar.unpack(td.path())); - t!(td.path().join("foo").join("bar").read_link()); - let dst = t!(td.path().join("foo").join("bar").canonicalize()); - t!(File::open(dst)); + let td = Builder::new().prefix("tar").tempdir().unwrap(); + ar.unpack(td.path()).unwrap(); + td.path().join("foo").join("bar").read_link().unwrap(); + let dst = td.path().join("foo").join("bar").canonicalize().unwrap(); + File::open(dst).unwrap(); } #[test] @@ -347,31 +340,34 @@ fn modify_hard_link_just_created() { let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Link); - t!(header.set_path("foo")); - t!(header.set_link_name("../test")); + header.set_path("foo").unwrap(); + header.set_link_name("../test").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); let mut header = tar::Header::new_gnu(); header.set_size(1); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("foo")); + header.set_path("foo").unwrap(); header.set_cksum(); - t!(ar.append(&header, &b"x"[..])); + ar.append(&header, &b"x"[..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let td = t!(Builder::new().prefix("tar").tempdir()); + let td = Builder::new().prefix("tar").tempdir().unwrap(); let test = td.path().join("test"); - t!(File::create(&test)); + File::create(&test).unwrap(); let dir = td.path().join("dir"); assert!(ar.unpack(&dir).is_err()); let mut contents = Vec::new(); - t!(t!(File::open(&test)).read_to_end(&mut contents)); + File::open(&test) + .unwrap() + .read_to_end(&mut contents) + .unwrap(); assert_eq!(contents.len(), 0); } @@ -382,30 +378,89 @@ fn modify_symlink_just_created() { let mut header = tar::Header::new_gnu(); header.set_size(0); header.set_entry_type(tar::EntryType::Symlink); - t!(header.set_path("foo")); - t!(header.set_link_name("../test")); + header.set_path("foo").unwrap(); + header.set_link_name("../test").unwrap(); header.set_cksum(); - t!(ar.append(&header, &[][..])); + ar.append(&header, &[][..]).unwrap(); let mut header = tar::Header::new_gnu(); header.set_size(1); header.set_entry_type(tar::EntryType::Regular); - t!(header.set_path("foo")); + header.set_path("foo").unwrap(); header.set_cksum(); - t!(ar.append(&header, &b"x"[..])); + ar.append(&header, &b"x"[..]).unwrap(); - let bytes = t!(ar.into_inner()); + let bytes = ar.into_inner().unwrap(); let mut ar = tar::Archive::new(&bytes[..]); - let td = t!(Builder::new().prefix("tar").tempdir()); + let td = Builder::new().prefix("tar").tempdir().unwrap(); let test = td.path().join("test"); - t!(File::create(&test)); + File::create(&test).unwrap(); let dir = td.path().join("dir"); - t!(ar.unpack(&dir)); + ar.unpack(&dir).unwrap(); let mut contents = Vec::new(); - t!(t!(File::open(&test)).read_to_end(&mut contents)); + File::open(&test) + .unwrap() + .read_to_end(&mut contents) + .unwrap(); assert_eq!(contents.len(), 0); } + +/// Test that unpacking a tarball with a symlink followed by a directory entry +/// with the same name does not allow modifying permissions of arbitrary directories +/// outside the extraction path. +#[test] +#[cfg(unix)] +fn symlink_dir_collision_does_not_modify_external_dir_permissions() { + use ::std::fs; + use ::std::os::unix::fs::PermissionsExt; + + let td = Builder::new().prefix("tar").tempdir().unwrap(); + + let target_dir = td.path().join("target-dir"); + fs::create_dir(&target_dir).unwrap(); + fs::set_permissions(&target_dir, fs::Permissions::from_mode(0o700)).unwrap(); + let before_mode = fs::metadata(&target_dir).unwrap().permissions().mode() & 0o7777; + assert_eq!(before_mode, 0o700); + + let extract_dir = td.path().join("extract-dir"); + fs::create_dir(&extract_dir).unwrap(); + + let mut ar = tar::Builder::new(Vec::new()); + + let mut header = tar::Header::new_gnu(); + header.set_size(0); + header.set_entry_type(tar::EntryType::Symlink); + header.set_path("foo").unwrap(); + header.set_link_name(&target_dir).unwrap(); + header.set_mode(0o777); + header.set_cksum(); + ar.append(&header, &[][..]).unwrap(); + + let mut header = tar::Header::new_gnu(); + header.set_size(0); + header.set_entry_type(tar::EntryType::Directory); + header.set_path("foo").unwrap(); + header.set_mode(0o777); + header.set_cksum(); + ar.append(&header, &[][..]).unwrap(); + + let bytes = ar.into_inner().unwrap(); + let mut ar = tar::Archive::new(&bytes[..]); + + let result = ar.unpack(&extract_dir); + assert!(result.is_err()); + + let symlink_path = extract_dir.join("foo"); + assert!(symlink_path + .symlink_metadata() + .unwrap() + .file_type() + .is_symlink()); + + let after_mode = fs::metadata(&target_dir).unwrap().permissions().mode() & 0o7777; + assert_eq!(after_mode, 0o700); +} diff --git a/tests/header/mod.rs b/tests/header/mod.rs index 9008cb55..dbbed991 100644 --- a/tests/header/mod.rs +++ b/tests/header/mod.rs @@ -1,7 +1,7 @@ use std::fs::{self, File}; use std::io::{self, Write}; use std::path::Path; -use std::{iter, mem, thread, time}; +use std::{mem, thread, time}; use tempfile::Builder; @@ -37,24 +37,24 @@ fn goto_ustar() { #[test] fn link_name() { let mut h = Header::new_gnu(); - t!(h.set_link_name("foo")); - assert_eq!(t!(h.link_name()).unwrap().to_str(), Some("foo")); - t!(h.set_link_name("../foo")); - assert_eq!(t!(h.link_name()).unwrap().to_str(), Some("../foo")); - t!(h.set_link_name("foo/bar")); - assert_eq!(t!(h.link_name()).unwrap().to_str(), Some("foo/bar")); - t!(h.set_link_name("foo\\ba")); + h.set_link_name("foo").unwrap(); + assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("foo")); + h.set_link_name("../foo").unwrap(); + assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("../foo")); + h.set_link_name("foo/bar").unwrap(); + assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("foo/bar")); + h.set_link_name("foo\\ba").unwrap(); if cfg!(windows) { - assert_eq!(t!(h.link_name()).unwrap().to_str(), Some("foo/ba")); + assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("foo/ba")); } else { - assert_eq!(t!(h.link_name()).unwrap().to_str(), Some("foo\\ba")); + assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("foo\\ba")); } let name = "foo\\bar\0"; for (slot, val) in h.as_old_mut().linkname.iter_mut().zip(name.as_bytes()) { *slot = *val; } - assert_eq!(t!(h.link_name()).unwrap().to_str(), Some("foo\\bar")); + assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("foo\\bar")); assert!(h.set_link_name("\0").is_err()); } @@ -62,32 +62,32 @@ fn link_name() { #[test] fn mtime() { let h = Header::new_gnu(); - assert_eq!(t!(h.mtime()), 0); + assert_eq!(h.mtime().unwrap(), 0); let h = Header::new_ustar(); - assert_eq!(t!(h.mtime()), 0); + assert_eq!(h.mtime().unwrap(), 0); let h = Header::new_old(); - assert_eq!(t!(h.mtime()), 0); + assert_eq!(h.mtime().unwrap(), 0); } #[test] fn user_and_group_name() { let mut h = Header::new_gnu(); - t!(h.set_username("foo")); - t!(h.set_groupname("bar")); - assert_eq!(t!(h.username()), Some("foo")); - assert_eq!(t!(h.groupname()), Some("bar")); + h.set_username("foo").unwrap(); + h.set_groupname("bar").unwrap(); + assert_eq!(h.username().unwrap(), Some("foo")); + assert_eq!(h.groupname().unwrap(), Some("bar")); h = Header::new_ustar(); - t!(h.set_username("foo")); - t!(h.set_groupname("bar")); - assert_eq!(t!(h.username()), Some("foo")); - assert_eq!(t!(h.groupname()), Some("bar")); + h.set_username("foo").unwrap(); + h.set_groupname("bar").unwrap(); + assert_eq!(h.username().unwrap(), Some("foo")); + assert_eq!(h.groupname().unwrap(), Some("bar")); h = Header::new_old(); - assert_eq!(t!(h.username()), None); - assert_eq!(t!(h.groupname()), None); + assert_eq!(h.username().unwrap(), None); + assert_eq!(h.groupname().unwrap(), None); assert!(h.set_username("foo").is_err()); assert!(h.set_groupname("foo").is_err()); } @@ -95,16 +95,16 @@ fn user_and_group_name() { #[test] fn dev_major_minor() { let mut h = Header::new_gnu(); - t!(h.set_device_major(1)); - t!(h.set_device_minor(2)); - assert_eq!(t!(h.device_major()), Some(1)); - assert_eq!(t!(h.device_minor()), Some(2)); + h.set_device_major(1).unwrap(); + h.set_device_minor(2).unwrap(); + assert_eq!(h.device_major().unwrap(), Some(1)); + assert_eq!(h.device_minor().unwrap(), Some(2)); h = Header::new_ustar(); - t!(h.set_device_major(1)); - t!(h.set_device_minor(2)); - assert_eq!(t!(h.device_major()), Some(1)); - assert_eq!(t!(h.device_minor()), Some(2)); + h.set_device_major(1).unwrap(); + h.set_device_minor(2).unwrap(); + assert_eq!(h.device_major().unwrap(), Some(1)); + assert_eq!(h.device_minor().unwrap(), Some(2)); h.as_ustar_mut().unwrap().dev_minor[0] = 0x7f; h.as_ustar_mut().unwrap().dev_major[0] = 0x7f; @@ -117,8 +117,8 @@ fn dev_major_minor() { assert!(h.device_minor().is_err()); h = Header::new_old(); - assert_eq!(t!(h.device_major()), None); - assert_eq!(t!(h.device_minor()), None); + assert_eq!(h.device_major().unwrap(), None); + assert_eq!(h.device_minor().unwrap(), None); assert!(h.set_device_major(1).is_err()); assert!(h.set_device_minor(1).is_err()); } @@ -126,28 +126,28 @@ fn dev_major_minor() { #[test] fn set_path() { let mut h = Header::new_gnu(); - t!(h.set_path("foo")); - assert_eq!(t!(h.path()).to_str(), Some("foo")); - t!(h.set_path("foo/")); - assert_eq!(t!(h.path()).to_str(), Some("foo/")); - t!(h.set_path("foo/bar")); - assert_eq!(t!(h.path()).to_str(), Some("foo/bar")); - t!(h.set_path("foo\\bar")); + h.set_path("foo").unwrap(); + assert_eq!(h.path().unwrap().to_str(), Some("foo")); + h.set_path("foo/").unwrap(); + assert_eq!(h.path().unwrap().to_str(), Some("foo/")); + h.set_path("foo/bar").unwrap(); + assert_eq!(h.path().unwrap().to_str(), Some("foo/bar")); + h.set_path("foo\\bar").unwrap(); if cfg!(windows) { - assert_eq!(t!(h.path()).to_str(), Some("foo/bar")); + assert_eq!(h.path().unwrap().to_str(), Some("foo/bar")); } else { - assert_eq!(t!(h.path()).to_str(), Some("foo\\bar")); + assert_eq!(h.path().unwrap().to_str(), Some("foo\\bar")); } - // set_path documentation explictly states it removes any ".", signfying the + // set_path documentation explicitly states it removes any ".", signifying the // current directory, from the path. This test ensures that documented - // beavhior occurs - t!(h.set_path("./control")); - assert_eq!(t!(h.path()).to_str(), Some("control")); + // behavior occurs + h.set_path("./control").unwrap(); + assert_eq!(h.path().unwrap().to_str(), Some("control")); - let long_name = iter::repeat("foo").take(100).collect::(); - let medium1 = iter::repeat("foo").take(52).collect::(); - let medium2 = iter::repeat("fo/").take(52).collect::(); + let long_name = "foo".repeat(100); + let medium1 = "foo".repeat(52); + let medium2 = "fo/".repeat(52); assert!(h.set_path(&long_name).is_err()); assert!(h.set_path(&medium1).is_err()); @@ -159,56 +159,56 @@ fn set_path() { assert!(h.set_path("foo/../bar").is_err()); h = Header::new_ustar(); - t!(h.set_path("foo")); - assert_eq!(t!(h.path()).to_str(), Some("foo")); + h.set_path("foo").unwrap(); + assert_eq!(h.path().unwrap().to_str(), Some("foo")); assert!(h.set_path(&long_name).is_err()); assert!(h.set_path(&medium1).is_err()); - t!(h.set_path(&medium2)); - assert_eq!(t!(h.path()).to_str(), Some(&medium2[..])); + h.set_path(&medium2).unwrap(); + assert_eq!(h.path().unwrap().to_str(), Some(&medium2[..])); } #[test] fn set_ustar_path_hard() { let mut h = Header::new_ustar(); - let p = Path::new("a").join(&vec!["a"; 100].join("")); - t!(h.set_path(&p)); - assert_eq!(t!(h.path()), p); + let p = Path::new("a").join(vec!["a"; 100].join("")); + h.set_path(&p).unwrap(); + assert_eq!(h.path().unwrap(), p); } #[test] fn set_metadata_deterministic() { - let td = t!(Builder::new().prefix("tar-rs").tempdir()); + let td = Builder::new().prefix("tar-rs").tempdir().unwrap(); let tmppath = td.path().join("tmpfile"); fn mk_header(path: &Path, readonly: bool) -> Result { - let mut file = t!(File::create(path)); - t!(file.write_all(b"c")); - let mut perms = t!(file.metadata()).permissions(); + let mut file = File::create(path).unwrap(); + file.write_all(b"c").unwrap(); + let mut perms = file.metadata().unwrap().permissions(); perms.set_readonly(readonly); - t!(fs::set_permissions(path, perms)); + fs::set_permissions(path, perms).unwrap(); let mut h = Header::new_ustar(); - h.set_metadata_in_mode(&t!(path.metadata()), HeaderMode::Deterministic); + h.set_metadata_in_mode(&path.metadata().unwrap(), HeaderMode::Deterministic); Ok(h) } // Create "the same" File twice in a row, one second apart, with differing readonly values. - let one = t!(mk_header(tmppath.as_path(), false)); + let one = mk_header(tmppath.as_path(), false).unwrap(); thread::sleep(time::Duration::from_millis(1050)); - let two = t!(mk_header(tmppath.as_path(), true)); + let two = mk_header(tmppath.as_path(), true).unwrap(); // Always expected to match. - assert_eq!(t!(one.size()), t!(two.size())); - assert_eq!(t!(one.path()), t!(two.path())); - assert_eq!(t!(one.mode()), t!(two.mode())); + assert_eq!(one.size().unwrap(), two.size().unwrap()); + assert_eq!(one.path().unwrap(), two.path().unwrap()); + assert_eq!(one.mode().unwrap(), two.mode().unwrap()); // Would not match without `Deterministic`. - assert_eq!(t!(one.mtime()), t!(two.mtime())); - assert_eq!(t!(one.mtime()), 1153704088); + assert_eq!(one.mtime().unwrap(), two.mtime().unwrap()); + assert_eq!(one.mtime().unwrap(), 1153704088); // TODO: No great way to validate that these would not be filled, but // check them anyway. - assert_eq!(t!(one.uid()), t!(two.uid())); - assert_eq!(t!(one.gid()), t!(two.gid())); + assert_eq!(one.uid().unwrap(), two.uid().unwrap()); + assert_eq!(one.gid().unwrap(), two.gid().unwrap()); } #[test] diff --git a/xtask/.gitignore b/xtask/.gitignore new file mode 100644 index 00000000..bb78efa1 --- /dev/null +++ b/xtask/.gitignore @@ -0,0 +1,2 @@ +# We don't have renovate/dependabot set up, though we should +Cargo.lock diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000..06d89479 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "xtask" +version = "0.0.0" +edition = "2021" +publish = false + +# Standalone workspace — prevents cargo from treating this as a member +# of the parent tar-rs workspace. +[workspace] + +[dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +xshell = "0.2" diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..3c0c6262 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,21 @@ +mod revdep_test; + +use anyhow::Result; +use clap::Parser; + +#[derive(Parser)] +#[command(about = "tar-rs development tasks")] +enum Cli { + /// Run reverse dependency tests. + /// + /// Clones known reverse dependencies at pinned revisions, patches them + /// to use our local tar checkout via `cargo --config`, and runs their + /// test suites. This catches regressions that our own tests might miss. + RevdepTest(revdep_test::RevdepTestArgs), +} + +fn main() -> Result<()> { + match Cli::parse() { + Cli::RevdepTest(args) => revdep_test::run(args), + } +} diff --git a/xtask/src/revdep_test.rs b/xtask/src/revdep_test.rs new file mode 100644 index 00000000..680b5e91 --- /dev/null +++ b/xtask/src/revdep_test.rs @@ -0,0 +1,317 @@ +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Context, Result}; +use clap::Args; +use xshell::{cmd, Shell}; + +/// Pinned reverse dependencies for reproducible testing. +/// Update these periodically — prefer tags over bare commit hashes +/// so that pins are easy to audit and understand at a glance. +const REVDEPS: &[RevDep] = &[ + RevDep { + name: "cargo", + repo: "https://github.com/rust-lang/cargo.git", + // cargo 0.94 ships with Rust 1.93 + rev: "0.94.0", + toolchain: None, + }, + RevDep { + name: "cargo-vendor-filterer", + repo: "https://github.com/coreos/cargo-vendor-filterer.git", + rev: "v0.5.18", + toolchain: None, + }, + RevDep { + name: "crates-io", + repo: "https://github.com/rust-lang/crates.io.git", + // crates.io doesn't tag releases; pin to a commit. + rev: "e482ed3da791735f37489cb9e6410a3b768d51f1", + // crates.io pins a specific Rust version via rust-toolchain.toml + // that may not be installed; override to stable. + toolchain: Some("stable"), + }, +]; + +struct RevDep { + name: &'static str, + repo: &'static str, + /// Git revision to check out — may be a tag name or a commit hash. + rev: &'static str, + /// If set, override RUSTUP_TOOLCHAIN for this revdep (e.g. when the + /// project has a rust-toolchain.toml pinning a version we don't have). + toolchain: Option<&'static str>, +} + +#[derive(Args)] +pub(crate) struct RevdepTestArgs { + /// Specific revdeps to test (default: all). Available: cargo, + /// cargo-vendor-filterer, crates-io + targets: Vec, + + /// Self-test mode: inject a compile error into Builder::new and + /// verify that at least one revdep test fails. Validates that the + /// CI pipeline actually catches tar-rs regressions. + #[arg(long)] + self_test: bool, +} + +pub(crate) fn run(args: RevdepTestArgs) -> Result<()> { + let tar_rs_root = project_root()?; + if args.self_test { + run_self_test(&tar_rs_root) + } else { + let revdeps = resolve_targets(&args.targets)?; + run_revdep_tests(&tar_rs_root, &revdeps) + } +} + +fn project_root() -> Result { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR") + .context("CARGO_MANIFEST_DIR not set — run via `cargo xtask`")?; + // xtask is at /xtask, so go up one level + let root = Path::new(&manifest_dir) + .parent() + .context("could not find project root")? + .to_owned(); + Ok(root) +} + +fn resolve_targets(targets: &[String]) -> Result> { + if targets.is_empty() { + return Ok(REVDEPS.iter().collect()); + } + targets + .iter() + .map(|name| { + REVDEPS + .iter() + .find(|r| r.name == name.as_str()) + .with_context(|| { + let available: Vec<_> = REVDEPS.iter().map(|r| r.name).collect(); + format!( + "unknown revdep {name:?}, available: {}", + available.join(", ") + ) + }) + }) + .collect() +} + +fn patch_config_flag(tar_rs_root: &Path) -> String { + let path = tar_rs_root.display(); + format!("patch.crates-io.tar.path=\"{path}\"") +} + +/// Clone a repo at a specific revision (tag or commit hash). +fn clone_at_rev(sh: &Shell, repo: &str, rev: &str, dest: &Path) -> Result<()> { + // `rev^{commit}` dereferences tags to their underlying commit, but + // xshell's cmd! macro doesn't allow literal braces in the template. + // Build the rev-parse argument as a plain string instead. + let rev_deref = format!("{rev}^{{commit}}"); + + if dest.join(".git").is_dir() { + // Resolve what the requested rev points to, so we can compare + // against HEAD regardless of whether rev is a tag or a hash. + let wanted = cmd!(sh, "git -C {dest} rev-parse {rev_deref}") + .ignore_status() + .read()?; + let current = cmd!(sh, "git -C {dest} rev-parse HEAD").read()?; + if current.trim() == wanted.trim() && !wanted.is_empty() { + println!(":: {}: already at {rev}", dest.display()); + return Ok(()); + } + println!( + ":: {}: wrong rev ({}), re-fetching...", + dest.display(), + current.trim() + ); + cmd!(sh, "git -C {dest} fetch origin {rev}").run()?; + cmd!(sh, "git -C {dest} checkout {rev}").run()?; + } else { + println!(":: Cloning {repo} at {rev}..."); + cmd!(sh, "git clone --no-checkout {repo} {dest}").run()?; + cmd!(sh, "git -C {dest} checkout {rev}").run()?; + } + Ok(()) +} + +/// Build a `cargo` command with the `--config patch` flag and optional +/// toolchain override applied. +fn cargo_cmd<'a>(sh: &'a Shell, tar_rs_root: &Path, revdep: &RevDep) -> xshell::Cmd<'a> { + let patch = patch_config_flag(tar_rs_root); + let c = cmd!(sh, "cargo --config {patch}"); + if let Some(tc) = revdep.toolchain { + c.env("RUSTUP_TOOLCHAIN", tc) + } else { + c + } +} + +fn test_cargo(sh: &Shell, tar_rs_root: &Path, revdep: &RevDep) -> Result<()> { + let dest = tar_rs_root.join("target/revdeps/cargo"); + clone_at_rev(sh, revdep.repo, revdep.rev, &dest)?; + let _dir = sh.push_dir(&dest); + + println!(":: Building cargo workspace..."); + cargo_cmd(sh, tar_rs_root, revdep) + .args(["check", "--workspace"]) + .run()?; + + println!(":: Running cargo package tests (exercises tar read+write paths)..."); + // The package:: integration tests exercise tar::Builder (creating .crate + // files) and tar::Archive (unpacking them for validation). + // + // The filter "package::" is a substring match, so it also hits tests from + // other modules that happen to contain "package" in their path (e.g. + // cargo_add::manifest_path_package, cargo_info::*_package, etc.). + // Those are cargo-internal snapshot tests unrelated to tar, and they + // break when the installed Rust version differs from what the pinned + // cargo version expects. Skip them explicitly. + cargo_cmd(sh, tar_rs_root, revdep) + .args([ + "test", + "-p", + "cargo", + "--test", + "testsuite", + "--", + "package::", + "--skip", + "cargo_add::", + "--skip", + "cargo_info::", + "--skip", + "cargo_package::", + "--skip", + "cargo_remove::", + ]) + .run()?; + + Ok(()) +} + +fn test_cargo_vendor_filterer(sh: &Shell, tar_rs_root: &Path, revdep: &RevDep) -> Result<()> { + let dest = tar_rs_root.join("target/revdeps/cargo-vendor-filterer"); + clone_at_rev(sh, revdep.repo, revdep.rev, &dest)?; + let _dir = sh.push_dir(&dest); + + println!(":: Running cargo-vendor-filterer tests (exercises tar write path)..."); + // Skip tar_zstd which requires the external zstd CLI. + cargo_cmd(sh, tar_rs_root, revdep) + .args(["test", "--", "--skip", "tar_zstd"]) + .run()?; + + Ok(()) +} + +fn test_crates_io(sh: &Shell, tar_rs_root: &Path, revdep: &RevDep) -> Result<()> { + let dest = tar_rs_root.join("target/revdeps/crates-io"); + clone_at_rev(sh, revdep.repo, revdep.rev, &dest)?; + let _dir = sh.push_dir(&dest); + + println!(":: Running crates_io_tarball tests (exercises tar Builder round-trip)..."); + // Tests cover Header::new_gnu, append_data, into_inner, size limits, + // symlink/hardlink rejection — no database needed. + cargo_cmd(sh, tar_rs_root, revdep) + .args(["test", "-p", "crates_io_tarball"]) + .run()?; + + Ok(()) +} + +fn run_test_for_revdep(sh: &Shell, tar_rs_root: &Path, revdep: &RevDep) -> Result<()> { + println!(); + println!("========================================"); + println!(" Testing reverse dep: {}", revdep.name); + println!("========================================"); + println!(); + + match revdep.name { + "cargo" => test_cargo(sh, tar_rs_root, revdep), + "cargo-vendor-filterer" => test_cargo_vendor_filterer(sh, tar_rs_root, revdep), + "crates-io" => test_crates_io(sh, tar_rs_root, revdep), + other => bail!("no test function for revdep {other:?}"), + } +} + +fn run_revdep_tests(tar_rs_root: &Path, revdeps: &[&RevDep]) -> Result<()> { + let sh = Shell::new()?; + let revdep_dir = tar_rs_root.join("target/revdeps"); + sh.create_dir(&revdep_dir)?; + + let mut failed = Vec::new(); + for revdep in revdeps { + if let Err(e) = run_test_for_revdep(&sh, tar_rs_root, revdep) { + println!(" FAILED: {}: {e:#}", revdep.name); + failed.push(revdep.name); + } + } + + println!(); + println!("========================================"); + println!(" Reverse dependency testing summary"); + println!("========================================"); + if failed.is_empty() { + println!(" All reverse dependency tests passed."); + Ok(()) + } else { + bail!("revdep tests failed: {}", failed.join(", ")); + } +} + +/// Self-test: inject a compile error into Builder::new, verify that a revdep +/// test fails, then restore. This validates that the CI pipeline actually +/// catches tar-rs regressions rather than passing vacuously. +fn run_self_test(tar_rs_root: &Path) -> Result<()> { + let sh = Shell::new()?; + let builder_rs = tar_rs_root.join("src/builder.rs"); + let revdep_dir = tar_rs_root.join("target/revdeps"); + sh.create_dir(&revdep_dir)?; + + println!(); + println!("========================================"); + println!(" Self-test: verifying the pipeline"); + println!(" detects tar-rs regressions"); + println!("========================================"); + println!(); + + // Inject compile error into Builder::new + println!(":: Injecting compile_error! into Builder::new..."); + let original = sh.read_file(&builder_rs)?; + let broken = original.replace( + "pub fn new(obj: W) -> Builder {", + "pub fn new(obj: W) -> Builder { compile_error!(\"revdep self-test: intentional breakage\");", + ); + if broken == original { + bail!("failed to inject compile_error — Builder::new signature not found"); + } + sh.write_file(&builder_rs, &broken)?; + + // Use cargo-vendor-filterer as the fast canary (small, quick to build) + let cvf = REVDEPS + .iter() + .find(|r| r.name == "cargo-vendor-filterer") + .unwrap(); + + println!(":: Running cargo-vendor-filterer (expecting failure)..."); + let result = run_test_for_revdep(&sh, tar_rs_root, cvf); + + // Restore original file + println!(":: Restoring builder.rs..."); + sh.write_file(&builder_rs, &original)?; + + match result { + Ok(()) => { + bail!( + "self-test FAILED: revdep tests passed despite injected compile error!\n\ + The CI pipeline is not actually exercising tar-rs code paths." + ); + } + Err(_) => { + println!(); + println!(":: Good — revdep tests failed as expected."); + println!(":: Self-test passed: pipeline correctly detects tar-rs breakage."); + Ok(()) + } + } +}