├── .gitignore ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ ├── archive.rs │ └── tar.rs ├── tests ├── archives │ ├── biguid_gnu.tar │ ├── sparse-large.tar │ ├── sparse-n.sh │ ├── empty_filename.tar │ ├── file_times.tar │ ├── spaces.tar │ ├── duplicate_dirs.tar │ ├── 7z_long_path.tar │ ├── simple_missing_last_header.tar │ ├── link.tar │ ├── simple.tar │ ├── directory.tar │ ├── sparse-1.tar │ ├── pax_size.tar │ ├── biguid_pax.tar │ ├── xattrs.tar │ ├── sparse.tar │ ├── pax2.tar │ ├── reading_files.tar │ └── pax.tar ├── header │ └── mod.rs └── entry.rs ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── examples ├── write.rs ├── list.rs ├── extract_file.rs └── raw_list.rs ├── src ├── error.rs ├── lib.rs ├── entry_type.rs ├── pax.rs └── archive.rs ├── Cargo.toml ├── LICENSE-MIT ├── README.md └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | coverage 5 | -------------------------------------------------------------------------------- /tests/archives/biguid_gnu.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexcrichton/tar-rs/HEAD/tests/archives/biguid_gnu.tar -------------------------------------------------------------------------------- /tests/archives/sparse-large.tar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexcrichton/tar-rs/HEAD/tests/archives/sparse-large.tar -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "08:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /tests/archives/sparse-n.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eu 3 | T="$(mktemp)" 4 | trap 'rm "$T"' EXIT 5 | for i in $(seq 0 "$1"); do 6 | truncate -s "${i}M" a.big 7 | echo "${i}MB through" >> a.big 8 | done 9 | tar -cf sparse-"$1".tar --sparse a.big 10 | -------------------------------------------------------------------------------- /examples/write.rs: -------------------------------------------------------------------------------- 1 | extern crate tar; 2 | 3 | use std::fs::File; 4 | use tar::Builder; 5 | 6 | fn main() { 7 | let file = File::create("foo.tar").unwrap(); 8 | let mut a = Builder::new(file); 9 | 10 | a.append_path("README.md").unwrap(); 11 | a.append_file("lib.rs", &mut File::open("src/lib.rs").unwrap()) 12 | .unwrap(); 13 | } 14 | -------------------------------------------------------------------------------- /examples/list.rs: -------------------------------------------------------------------------------- 1 | //! An example of listing the file names of entries in an archive. 2 | //! 3 | //! Takes a tarball on stdin and prints out all of the entries inside. 4 | 5 | extern crate tar; 6 | 7 | use std::io::stdin; 8 | 9 | use tar::Archive; 10 | 11 | fn main() { 12 | let mut ar = Archive::new(stdin()); 13 | for file in ar.entries().unwrap() { 14 | let f = file.unwrap(); 15 | println!("{}", f.path().unwrap().display()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/archives/empty_filename.tar: -------------------------------------------------------------------------------- 1 | ustar 0000755000175000017500000000000012440021574010647 5ustar mvd42530647 5 -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tar-fuzz" 3 | version = "0.0.0" 4 | publish = false 5 | edition = "2018" 6 | 7 | [package.metadata] 8 | cargo-fuzz = true 9 | 10 | [dependencies] 11 | arbitrary = { version = "1.3.2", features = ["derive"] } 12 | cap-std = "3.4.0" 13 | libfuzzer-sys = "0.4" 14 | tempfile = "3.3" 15 | 16 | [dependencies.tar] 17 | path = ".." 18 | 19 | [[bin]] 20 | name = "archive" 21 | path = "fuzz_targets/archive.rs" 22 | test = false 23 | doc = false 24 | bench = false 25 | 26 | [[bin]] 27 | name = "tar" 28 | path = "fuzz_targets/tar.rs" 29 | test = false 30 | doc = false 31 | bench = false 32 | -------------------------------------------------------------------------------- /examples/extract_file.rs: -------------------------------------------------------------------------------- 1 | //! An example of extracting a file in an archive. 2 | //! 3 | //! Takes a tarball on standard input, looks for an entry with a listed file 4 | //! name as the first argument provided, and then prints the contents of that 5 | //! file to stdout. 6 | 7 | extern crate tar; 8 | 9 | use std::env::args_os; 10 | use std::io::{copy, stdin, stdout}; 11 | use std::path::Path; 12 | 13 | use tar::Archive; 14 | 15 | fn main() { 16 | let first_arg = args_os().nth(1).unwrap(); 17 | let filename = Path::new(&first_arg); 18 | let mut ar = Archive::new(stdin()); 19 | for file in ar.entries().unwrap() { 20 | let mut f = file.unwrap(); 21 | if f.path().unwrap() == filename { 22 | copy(&mut f, &mut stdout()).unwrap(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::error; 3 | use std::fmt; 4 | use std::io::{self, Error}; 5 | 6 | #[derive(Debug)] 7 | pub struct TarError { 8 | desc: Cow<'static, str>, 9 | io: io::Error, 10 | } 11 | 12 | impl TarError { 13 | pub fn new(desc: impl Into>, err: Error) -> TarError { 14 | TarError { 15 | desc: desc.into(), 16 | io: err, 17 | } 18 | } 19 | } 20 | 21 | impl error::Error for TarError { 22 | fn description(&self) -> &str { 23 | &self.desc 24 | } 25 | 26 | fn source(&self) -> Option<&(dyn error::Error + 'static)> { 27 | Some(&self.io) 28 | } 29 | } 30 | 31 | impl fmt::Display for TarError { 32 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 33 | self.desc.fmt(f) 34 | } 35 | } 36 | 37 | impl From for Error { 38 | fn from(t: TarError) -> Error { 39 | Error::new(t.io.kind(), t) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tar" 3 | version = "0.4.44" 4 | authors = ["Alex Crichton "] 5 | homepage = "https://github.com/alexcrichton/tar-rs" 6 | repository = "https://github.com/alexcrichton/tar-rs" 7 | documentation = "https://docs.rs/tar" 8 | license = "MIT OR Apache-2.0" 9 | keywords = ["tar", "tarfile", "encoding"] 10 | readme = "README.md" 11 | edition = "2021" 12 | rust-version = "1.63" 13 | exclude = ["tests/archives/*"] 14 | 15 | description = """ 16 | A Rust implementation of a TAR file reader and writer. This library does not 17 | currently handle compression, but it is abstract over all I/O readers and 18 | writers. Additionally, great lengths are taken to ensure that the entire 19 | contents are never required to be entirely resident in memory all at once. 20 | """ 21 | 22 | [dependencies] 23 | filetime = "0.2.8" 24 | 25 | [dev-dependencies] 26 | tempfile = "3" 27 | 28 | [target."cfg(unix)".dependencies] 29 | xattr = { version = "1.1.3", optional = true } 30 | libc = "0.2" 31 | 32 | [features] 33 | default = ["xattr"] 34 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) The tar-rs Project Contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /tests/archives/file_times.tar: -------------------------------------------------------------------------------- 1 | a000644 000765 000024 00000000000 07346545000 012100 0ustar00HyeonKimstaff000000 000000 -------------------------------------------------------------------------------- /examples/raw_list.rs: -------------------------------------------------------------------------------- 1 | //! An example of listing raw entries in an archive. 2 | //! 3 | //! Takes a tarball on stdin and prints out all of the entries inside. 4 | 5 | extern crate tar; 6 | 7 | use std::io::stdin; 8 | 9 | use tar::Archive; 10 | 11 | fn main() { 12 | let mut ar = Archive::new(stdin()); 13 | for (i, file) in ar.entries().unwrap().raw(true).enumerate() { 14 | println!("-------------------------- Entry {}", i); 15 | let mut f = file.unwrap(); 16 | println!("path: {}", f.path().unwrap().display()); 17 | println!("size: {}", f.header().size().unwrap()); 18 | println!("entry size: {}", f.header().entry_size().unwrap()); 19 | println!("link name: {:?}", f.link_name().unwrap()); 20 | println!("file type: {:#x}", f.header().entry_type().as_byte()); 21 | println!("mode: {:#o}", f.header().mode().unwrap()); 22 | println!("uid: {}", f.header().uid().unwrap()); 23 | println!("gid: {}", f.header().gid().unwrap()); 24 | println!("mtime: {}", f.header().mtime().unwrap()); 25 | println!("username: {:?}", f.header().username().unwrap()); 26 | println!("groupname: {:?}", f.header().groupname().unwrap()); 27 | 28 | if f.header().as_ustar().is_some() { 29 | println!("kind: UStar"); 30 | } else if f.header().as_gnu().is_some() { 31 | println!("kind: GNU"); 32 | } else { 33 | println!("kind: normal"); 34 | } 35 | 36 | if let Ok(Some(extensions)) = f.pax_extensions() { 37 | println!("pax extensions:"); 38 | for e in extensions { 39 | let e = e.unwrap(); 40 | println!( 41 | "\t{:?} = {:?}", 42 | String::from_utf8_lossy(e.key_bytes()), 43 | String::from_utf8_lossy(e.value_bytes()) 44 | ); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A library for reading and writing TAR archives 2 | //! 3 | //! This library provides utilities necessary to manage [TAR archives][1] 4 | //! abstracted over a reader or writer. Great strides are taken to ensure that 5 | //! an archive is never required to be fully resident in memory, and all objects 6 | //! provide largely a streaming interface to read bytes from. 7 | //! 8 | //! [1]: http://en.wikipedia.org/wiki/Tar_%28computing%29 9 | 10 | // More docs about the detailed tar format can also be found here: 11 | // http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current 12 | 13 | // NB: some of the coding patterns and idioms here may seem a little strange. 14 | // This is currently attempting to expose a super generic interface while 15 | // also not forcing clients to codegen the entire crate each time they use 16 | // it. To that end lots of work is done to ensure that concrete 17 | // implementations are all found in this crate and the generic functions are 18 | // all just super thin wrappers (e.g. easy to codegen). 19 | 20 | #![doc(html_root_url = "https://docs.rs/tar/0.4")] 21 | #![deny(missing_docs)] 22 | #![cfg_attr(test, deny(warnings))] 23 | 24 | use std::io::{Error, ErrorKind}; 25 | 26 | pub use crate::archive::{Archive, Entries}; 27 | pub use crate::builder::{Builder, EntryWriter}; 28 | pub use crate::entry::{Entry, Unpacked}; 29 | pub use crate::entry_type::EntryType; 30 | pub use crate::header::GnuExtSparseHeader; 31 | #[cfg(all(any(unix, windows), not(target_arch = "wasm32")))] 32 | pub use crate::header::DETERMINISTIC_TIMESTAMP; 33 | pub use crate::header::{GnuHeader, GnuSparseHeader, Header, HeaderMode, OldHeader, UstarHeader}; 34 | pub use crate::pax::{PaxExtension, PaxExtensions}; 35 | 36 | mod archive; 37 | mod builder; 38 | mod entry; 39 | mod entry_type; 40 | mod error; 41 | mod header; 42 | mod pax; 43 | 44 | fn other(msg: &str) -> Error { 45 | Error::new(ErrorKind::Other, msg) 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tar-rs 2 | 3 | [Documentation](https://docs.rs/tar) 4 | 5 | A tar archive reading/writing library for Rust. 6 | 7 | ```toml 8 | # Cargo.toml 9 | [dependencies] 10 | tar = "0.4" 11 | ``` 12 | 13 | ## Reading an archive 14 | 15 | ```rust,no_run 16 | extern crate tar; 17 | 18 | use std::io::prelude::*; 19 | use std::fs::File; 20 | use tar::Archive; 21 | 22 | fn main() { 23 | let file = File::open("foo.tar").unwrap(); 24 | let mut a = Archive::new(file); 25 | 26 | for file in a.entries().unwrap() { 27 | // Make sure there wasn't an I/O error 28 | let mut file = file.unwrap(); 29 | 30 | // Inspect metadata about the file 31 | println!("{:?}", file.header().path().unwrap()); 32 | println!("{}", file.header().size().unwrap()); 33 | 34 | // files implement the Read trait 35 | let mut s = String::new(); 36 | file.read_to_string(&mut s).unwrap(); 37 | println!("{}", s); 38 | } 39 | } 40 | 41 | ``` 42 | 43 | ## Writing an archive 44 | 45 | ```rust,no_run 46 | extern crate tar; 47 | 48 | use std::io::prelude::*; 49 | use std::fs::File; 50 | use tar::Builder; 51 | 52 | fn main() { 53 | let file = File::create("foo.tar").unwrap(); 54 | let mut a = Builder::new(file); 55 | 56 | a.append_path("file1.txt").unwrap(); 57 | a.append_file("file2.txt", &mut File::open("file3.txt").unwrap()).unwrap(); 58 | } 59 | ``` 60 | 61 | # License 62 | 63 | This project is licensed under either of 64 | 65 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 66 | http://www.apache.org/licenses/LICENSE-2.0) 67 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or 68 | http://opensource.org/licenses/MIT) 69 | 70 | at your option. 71 | 72 | ### Contribution 73 | 74 | Unless you explicitly state otherwise, any contribution intentionally submitted 75 | for inclusion in this project by you, as defined in the Apache-2.0 license, 76 | shall be dual licensed as above, without any additional terms or conditions. 77 | -------------------------------------------------------------------------------- /tests/archives/spaces.tar: -------------------------------------------------------------------------------- 1 | a100777 0 0 2 12440016664 4253 0a 2 | -------------------------------------------------------------------------------- /tests/archives/duplicate_dirs.tar: -------------------------------------------------------------------------------- 1 | some_dir/000755 000766 000024 00000000000 12527333503 015306 5ustar00cadencemarseillestaff000000 000000 some_dir/000755 000766 000024 00000000000 12527333503 015306 5ustar00cadencemarseillestaff000000 000000 -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | name: Test 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | build: [stable, beta, nightly, macos, windows] 11 | include: 12 | - build: stable 13 | os: ubuntu-latest 14 | rust: stable 15 | - build: beta 16 | os: ubuntu-latest 17 | rust: beta 18 | - build: nightly 19 | os: ubuntu-latest 20 | rust: nightly 21 | - build: macos 22 | os: macos-latest 23 | rust: stable 24 | - build: windows 25 | os: windows-latest 26 | rust: stable 27 | steps: 28 | - uses: actions/checkout@master 29 | - name: Install Rust 30 | run: rustup update ${{ matrix.rust }} --no-self-update && rustup default ${{ matrix.rust }} 31 | shell: bash 32 | - run: cargo test 33 | - run: cargo test --no-default-features 34 | - name: Run cargo test with root 35 | run: sudo -E $(which cargo) test 36 | if: ${{ matrix.os == 'ubuntu-latest' }} 37 | 38 | wasm: 39 | name: Wasm 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@master 43 | - run: rustup target add wasm32-unknown-emscripten 44 | - run: cargo build --target=wasm32-unknown-emscripten 45 | 46 | rustfmt: 47 | name: Rustfmt 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@master 51 | - name: Install Rust 52 | run: rustup update stable && rustup default stable && rustup component add rustfmt 53 | - run: cargo fmt -- --check 54 | 55 | publish_docs: 56 | name: Publish Documentation 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@master 60 | - name: Install Rust 61 | run: rustup update stable && rustup default stable 62 | - name: Build documentation 63 | run: cargo doc --no-deps --all-features 64 | - name: Publish documentation 65 | run: | 66 | cd target/doc 67 | git init 68 | git add . 69 | git -c user.name='ci' -c user.email='ci' commit -m init 70 | git push -f -q https://git:${{ secrets.github_token }}@github.com/${{ github.repository }} HEAD:gh-pages 71 | if: github.event_name == 'push' && github.event.ref == 'refs/heads/master' 72 | -------------------------------------------------------------------------------- /tests/archives/7z_long_path.tar: -------------------------------------------------------------------------------- 1 | ././@LongLink0040777000000000000000000000015013661252300010077 Lustar00this_is_a_folder_with_a_name_that_just_keeps_going_on_and_on_and_on_it_is_a_very_very_long_folder_name/this_is_a_folder_with_a_name_that_just_keeps_going_on_and_on_and_on_it_is_a_very_very_long_folder_n0040777000000000000000000000000013661252300032242 5ustar00././@LongLink0100777000000000000000000000016013661252300010075 Lustar00this_is_a_folder_with_a_name_that_just_keeps_going_on_and_on_and_on_it_is_a_very_very_long_folder_name/test.txtthis_is_a_folder_with_a_name_that_just_keeps_going_on_and_on_and_on_it_is_a_very_very_long_folder_n0100777000000000000000000000000013661252300032232 0ustar00 -------------------------------------------------------------------------------- /fuzz/fuzz_targets/archive.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![no_main] 16 | 17 | use arbitrary::{Arbitrary, Unstructured}; 18 | use cap_std::fs::Dir; 19 | use cap_std::ambient_authority; 20 | use libfuzzer_sys::fuzz_target; 21 | use std::io::{Cursor, Write}; 22 | use tar::{Archive, Builder, EntryType, Header}; 23 | use tempfile::tempdir; 24 | 25 | // Define ArchiveEntry for arbitrary crate 26 | #[derive(Debug, Arbitrary)] 27 | struct ArchiveEntry { 28 | path: String, 29 | entry_type: u8, 30 | content: Vec, 31 | } 32 | 33 | // Define FuzzInput for arbitrary crate 34 | #[derive(Debug, Arbitrary)] 35 | struct FuzzInput { 36 | entries: Vec, 37 | } 38 | 39 | fuzz_target!(|data: &[u8]| { 40 | // Prepare FuzzInput with Arbitrary 41 | let mut unstructured = Unstructured::new(data); 42 | let input: FuzzInput = match FuzzInput::arbitrary(&mut unstructured) { 43 | Ok(val) => val, 44 | Err(_) => return, 45 | }; 46 | 47 | // Create a sandbox directory with cap_std 48 | let temp_dir = match tempdir() { 49 | Ok(dir) => dir, 50 | Err(_) => return, 51 | }; 52 | let sandbox_dir = match Dir::open_ambient_dir(temp_dir.path(), ambient_authority()) { 53 | Ok(dir) => dir, 54 | Err(_) => return, 55 | }; 56 | let temp_file_path = "archive_file.tar"; 57 | let mut builder = Builder::new(Vec::new()); 58 | 59 | // Iterate through the archive entries to build a tar structure 60 | for entry in &input.entries { 61 | let mut header = Header::new_gnu(); 62 | 63 | // Ensure content size is reasonable to avoid potential overflow issues 64 | let file_size = entry.content.len() as u64; 65 | if file_size > u32::MAX as u64 { 66 | continue; 67 | } 68 | header.set_size(file_size); 69 | 70 | // Determine the entry type from fuzzed data 71 | let entry_type = match entry.entry_type % 5 { 72 | 0 => EntryType::Regular, 73 | 1 => EntryType::Directory, 74 | 2 => EntryType::Symlink, 75 | 3 => EntryType::hard_link(), 76 | _ => EntryType::character_special(), 77 | }; 78 | header.set_entry_type(entry_type); 79 | 80 | // Process entry types using cap_std sandbox 81 | match entry_type { 82 | EntryType::Directory => { 83 | if let Err(_) = sandbox_dir.create_dir_all(&entry.path) { 84 | continue; 85 | } 86 | if builder.append_dir(&entry.path, &entry.path).is_err() { 87 | continue; 88 | } 89 | } 90 | EntryType::Regular => { 91 | let mut cursor = Cursor::new(entry.content.clone()); 92 | if builder.append_data(&mut header, entry.path.as_str(), &mut cursor).is_err() { 93 | continue; 94 | } 95 | } 96 | _ => { 97 | // Handle other types with appropriate mock content or skip unsupported 98 | let mut cursor = Cursor::new(entry.content.clone()); 99 | if builder.append_data(&mut header, entry.path.as_str(), &mut cursor).is_err() { 100 | continue; 101 | } 102 | } 103 | } 104 | } 105 | 106 | // Write the builder content to the temporary tar file within the sandbox 107 | if let Ok(mut temp_file) = sandbox_dir.create(temp_file_path) { 108 | if temp_file.write_all(&builder.into_inner().unwrap_or_default()).is_ok() { 109 | let mut archive = Archive::new(temp_file); 110 | if let Ok(entries) = archive.entries() { 111 | for entry in entries { 112 | if entry.is_err() { 113 | return; 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | // Cleanup temp directory and sandbox directory 121 | drop(sandbox_dir); 122 | drop(temp_dir); 123 | }); 124 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/tar.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #![no_main] 16 | 17 | use arbitrary::{Arbitrary, Unstructured}; 18 | use libfuzzer_sys::fuzz_target; 19 | use std::io::{Cursor, Read, Seek, Write}; 20 | use tar::{Archive, Builder, EntryType, Header}; 21 | use tempfile::{tempdir, NamedTempFile}; 22 | 23 | // Define FuzzInput for arbitrary crate 24 | #[derive(Debug)] 25 | struct FuzzInput { 26 | data: Vec, 27 | file_name: String, 28 | link_path: String, 29 | target_path: String, 30 | entry_type: u8, 31 | metadata_size: u64, 32 | } 33 | 34 | // Implement Arbitrary for FuzzInput 35 | impl<'a> Arbitrary<'a> for FuzzInput { 36 | fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { 37 | Ok(FuzzInput { 38 | data: u.arbitrary()?, 39 | file_name: u.arbitrary::<&str>()?.to_string(), 40 | link_path: u.arbitrary::<&str>()?.to_string(), 41 | target_path: u.arbitrary::<&str>()?.to_string(), 42 | entry_type: u.arbitrary()?, 43 | metadata_size: u.int_in_range(0..=1000)?, 44 | }) 45 | } 46 | } 47 | 48 | fuzz_target!(|data: &[u8]| { 49 | // Prepare FuzzInput by Arbitrary crate 50 | let mut unstructured = Unstructured::new(data); 51 | let input: FuzzInput = match FuzzInput::arbitrary(&mut unstructured) { 52 | Ok(val) => val, 53 | Err(_) => return, 54 | }; 55 | 56 | // Setup temporary directory and initialize builder 57 | let temp_dir = match tempdir() { 58 | Ok(dir) => dir, 59 | Err(_) => return, 60 | }; 61 | let archive_data = Cursor::new(&input.data); 62 | let mut builder = Builder::new(Cursor::new(Vec::new())); 63 | let mut header = Header::new_gnu(); 64 | 65 | // Set random header metadata 66 | header.set_size(input.metadata_size.min(input.data.len() as u64)); 67 | header.set_cksum(); 68 | let entry_type = match input.entry_type % 5 { 69 | 0 => EntryType::Regular, 70 | 1 => EntryType::Directory, 71 | 2 => EntryType::Symlink, 72 | 3 => EntryType::Link, 73 | _ => EntryType::Fifo, 74 | }; 75 | header.set_entry_type(entry_type); 76 | 77 | // Append data 78 | let _ = builder.append_data(&mut header, &input.file_name, archive_data); 79 | if let Ok(mut temp_file) = NamedTempFile::new() { 80 | let _ = temp_file.write_all(&input.data); 81 | let _ = builder.append_file("fuzzed/file2", temp_file.as_file_mut()).ok(); 82 | } 83 | 84 | #[cfg(unix)] 85 | let _ = builder.append_link(&mut header, &input.link_path, &input.target_path).ok(); 86 | let _ = builder.finish(); 87 | 88 | // Fuzzing Archive and Entry logic 89 | let mut archive = Archive::new(Cursor::new(&input.data)); 90 | if let Ok(mut entries) = archive.entries() { 91 | while let Some(Ok(mut entry)) = entries.next() { 92 | let _ = entry.path().map(|p| p.to_owned()); 93 | let _ = entry.link_name().map(|l| l.map(|ln| ln.to_owned())); 94 | let _ = entry.size(); 95 | let _ = entry.header(); 96 | let _ = entry.raw_header_position(); 97 | let _ = entry.raw_file_position(); 98 | 99 | // Randomly choose entry actions based on entry type 100 | match entry.header().entry_type() { 101 | EntryType::Regular => { /* Do nothing */ } 102 | EntryType::Directory | EntryType::Symlink | EntryType::Link => { 103 | let _ = entry.unpack_in(temp_dir.path()).ok(); 104 | } 105 | EntryType::Fifo => { /* Do nothing */ } 106 | _ => { /* Do nothing */ } 107 | } 108 | 109 | // Randomly read contents and adjust permissions and attributes 110 | let mut buffer = Vec::new(); 111 | let _ = entry.read_to_end(&mut buffer).ok(); 112 | entry.set_mask(0o755); 113 | entry.set_unpack_xattrs(true); 114 | entry.set_preserve_permissions(true); 115 | entry.set_preserve_mtime(true); 116 | 117 | // Fuzz unpack to randomized destination path 118 | let dst_path = temp_dir.path().join(&input.file_name); 119 | let _ = entry.unpack(&dst_path).ok(); 120 | let _ = entry.unpack_in(temp_dir.path()).ok(); 121 | 122 | // Fuzz PaxExtensions 123 | if let Ok(Some(pax_extensions)) = entry.pax_extensions() { 124 | for ext in pax_extensions { 125 | let _ = ext.ok(); 126 | } 127 | } 128 | 129 | // Randomized file search with tar entry position 130 | if entry.size() > 0 { 131 | let mut data_cursor = Cursor::new(&input.data); 132 | let _ = data_cursor.seek(std::io::SeekFrom::Start(entry.raw_file_position())).ok(); 133 | let _ = data_cursor.read(&mut buffer).ok(); 134 | } 135 | } 136 | } 137 | }); 138 | -------------------------------------------------------------------------------- /src/entry_type.rs: -------------------------------------------------------------------------------- 1 | // See https://en.wikipedia.org/wiki/Tar_%28computing%29#UStar_format 2 | /// Indicate the type of content described by a header. 3 | /// 4 | /// This is returned by [`crate::Header::entry_type()`] and should be used to 5 | /// distinguish between types of content. 6 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 7 | pub enum EntryType { 8 | /// Regular file 9 | Regular, 10 | /// Hard link 11 | Link, 12 | /// Symbolic link 13 | Symlink, 14 | /// Character device 15 | Char, 16 | /// Block device 17 | Block, 18 | /// Directory 19 | Directory, 20 | /// Named pipe (fifo) 21 | Fifo, 22 | /// Implementation-defined 'high-performance' type, treated as regular file 23 | Continuous, 24 | /// GNU extension - long file name 25 | GNULongName, 26 | /// GNU extension - long link name (link target) 27 | GNULongLink, 28 | /// GNU extension - sparse file 29 | GNUSparse, 30 | /// Global extended header 31 | XGlobalHeader, 32 | /// Extended Header 33 | XHeader, 34 | /// Hints that destructuring should not be exhaustive. 35 | /// 36 | /// This enum may grow additional variants, so this makes sure clients 37 | /// don't count on exhaustive matching. (Otherwise, adding a new variant 38 | /// could break existing code.) 39 | #[doc(hidden)] 40 | __Nonexhaustive(u8), 41 | } 42 | 43 | impl EntryType { 44 | /// Creates a new entry type from a raw byte. 45 | /// 46 | /// Note that the other named constructors of entry type may be more 47 | /// appropriate to create a file type from. 48 | pub fn new(byte: u8) -> EntryType { 49 | match byte { 50 | b'\x00' | b'0' => EntryType::Regular, 51 | b'1' => EntryType::Link, 52 | b'2' => EntryType::Symlink, 53 | b'3' => EntryType::Char, 54 | b'4' => EntryType::Block, 55 | b'5' => EntryType::Directory, 56 | b'6' => EntryType::Fifo, 57 | b'7' => EntryType::Continuous, 58 | b'x' => EntryType::XHeader, 59 | b'g' => EntryType::XGlobalHeader, 60 | b'L' => EntryType::GNULongName, 61 | b'K' => EntryType::GNULongLink, 62 | b'S' => EntryType::GNUSparse, 63 | b => EntryType::__Nonexhaustive(b), 64 | } 65 | } 66 | 67 | /// Returns the raw underlying byte that this entry type represents. 68 | pub fn as_byte(&self) -> u8 { 69 | match *self { 70 | EntryType::Regular => b'0', 71 | EntryType::Link => b'1', 72 | EntryType::Symlink => b'2', 73 | EntryType::Char => b'3', 74 | EntryType::Block => b'4', 75 | EntryType::Directory => b'5', 76 | EntryType::Fifo => b'6', 77 | EntryType::Continuous => b'7', 78 | EntryType::XHeader => b'x', 79 | EntryType::XGlobalHeader => b'g', 80 | EntryType::GNULongName => b'L', 81 | EntryType::GNULongLink => b'K', 82 | EntryType::GNUSparse => b'S', 83 | EntryType::__Nonexhaustive(b) => b, 84 | } 85 | } 86 | 87 | /// Creates a new entry type representing a regular file. 88 | pub fn file() -> EntryType { 89 | EntryType::Regular 90 | } 91 | 92 | /// Creates a new entry type representing a hard link. 93 | pub fn hard_link() -> EntryType { 94 | EntryType::Link 95 | } 96 | 97 | /// Creates a new entry type representing a symlink. 98 | pub fn symlink() -> EntryType { 99 | EntryType::Symlink 100 | } 101 | 102 | /// Creates a new entry type representing a character special device. 103 | pub fn character_special() -> EntryType { 104 | EntryType::Char 105 | } 106 | 107 | /// Creates a new entry type representing a block special device. 108 | pub fn block_special() -> EntryType { 109 | EntryType::Block 110 | } 111 | 112 | /// Creates a new entry type representing a directory. 113 | pub fn dir() -> EntryType { 114 | EntryType::Directory 115 | } 116 | 117 | /// Creates a new entry type representing a FIFO. 118 | pub fn fifo() -> EntryType { 119 | EntryType::Fifo 120 | } 121 | 122 | /// Creates a new entry type representing a contiguous file. 123 | pub fn contiguous() -> EntryType { 124 | EntryType::Continuous 125 | } 126 | 127 | /// Returns whether this type represents a regular file. 128 | pub fn is_file(&self) -> bool { 129 | self == &EntryType::Regular 130 | } 131 | 132 | /// Returns whether this type represents a hard link. 133 | pub fn is_hard_link(&self) -> bool { 134 | self == &EntryType::Link 135 | } 136 | 137 | /// Returns whether this type represents a symlink. 138 | pub fn is_symlink(&self) -> bool { 139 | self == &EntryType::Symlink 140 | } 141 | 142 | /// Returns whether this type represents a character special device. 143 | pub fn is_character_special(&self) -> bool { 144 | self == &EntryType::Char 145 | } 146 | 147 | /// Returns whether this type represents a block special device. 148 | pub fn is_block_special(&self) -> bool { 149 | self == &EntryType::Block 150 | } 151 | 152 | /// Returns whether this type represents a directory. 153 | pub fn is_dir(&self) -> bool { 154 | self == &EntryType::Directory 155 | } 156 | 157 | /// Returns whether this type represents a FIFO. 158 | pub fn is_fifo(&self) -> bool { 159 | self == &EntryType::Fifo 160 | } 161 | 162 | /// Returns whether this type represents a contiguous file. 163 | pub fn is_contiguous(&self) -> bool { 164 | self == &EntryType::Continuous 165 | } 166 | 167 | /// Returns whether this type represents a GNU long name header. 168 | pub fn is_gnu_longname(&self) -> bool { 169 | self == &EntryType::GNULongName 170 | } 171 | 172 | /// Returns whether this type represents a GNU sparse header. 173 | pub fn is_gnu_sparse(&self) -> bool { 174 | self == &EntryType::GNUSparse 175 | } 176 | 177 | /// Returns whether this type represents a GNU long link header. 178 | pub fn is_gnu_longlink(&self) -> bool { 179 | self == &EntryType::GNULongLink 180 | } 181 | 182 | /// Returns whether this type represents PAX global extensions, that 183 | /// should affect all following entries. For more, see [PAX]. 184 | /// 185 | /// [PAX]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html 186 | pub fn is_pax_global_extensions(&self) -> bool { 187 | self == &EntryType::XGlobalHeader 188 | } 189 | 190 | /// Returns whether this type represents PAX local extensions; these 191 | /// only affect the current entry. For more, see [PAX]. 192 | /// 193 | /// [PAX]: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html 194 | pub fn is_pax_local_extensions(&self) -> bool { 195 | self == &EntryType::XHeader 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/pax.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | use std::io; 3 | use std::io::Write; 4 | use std::slice; 5 | use std::str; 6 | 7 | use crate::other; 8 | 9 | // Keywords for PAX extended header records. 10 | pub const PAX_NONE: &str = ""; // Indicates that no PAX key is suitable 11 | pub const PAX_PATH: &str = "path"; 12 | pub const PAX_LINKPATH: &str = "linkpath"; 13 | pub const PAX_SIZE: &str = "size"; 14 | pub const PAX_UID: &str = "uid"; 15 | pub const PAX_GID: &str = "gid"; 16 | pub const PAX_UNAME: &str = "uname"; 17 | pub const PAX_GNAME: &str = "gname"; 18 | pub const PAX_MTIME: &str = "mtime"; 19 | pub const PAX_ATIME: &str = "atime"; 20 | pub const PAX_CTIME: &str = "ctime"; // Removed from later revision of PAX spec, but was valid 21 | pub const PAX_CHARSET: &str = "charset"; // Currently unused 22 | pub const PAX_COMMENT: &str = "comment"; // Currently unused 23 | 24 | pub const PAX_SCHILYXATTR: &str = "SCHILY.xattr."; 25 | 26 | // Keywords for GNU sparse files in a PAX extended header. 27 | pub const PAX_GNUSPARSE: &str = "GNU.sparse."; 28 | pub const PAX_GNUSPARSENUMBLOCKS: &str = "GNU.sparse.numblocks"; 29 | pub const PAX_GNUSPARSEOFFSET: &str = "GNU.sparse.offset"; 30 | pub const PAX_GNUSPARSENUMBYTES: &str = "GNU.sparse.numbytes"; 31 | pub const PAX_GNUSPARSEMAP: &str = "GNU.sparse.map"; 32 | pub const PAX_GNUSPARSENAME: &str = "GNU.sparse.name"; 33 | pub const PAX_GNUSPARSEMAJOR: &str = "GNU.sparse.major"; 34 | pub const PAX_GNUSPARSEMINOR: &str = "GNU.sparse.minor"; 35 | pub const PAX_GNUSPARSESIZE: &str = "GNU.sparse.size"; 36 | pub const PAX_GNUSPARSEREALSIZE: &str = "GNU.sparse.realsize"; 37 | 38 | /// An iterator over the pax extensions in an archive entry. 39 | /// 40 | /// This iterator yields structures which can themselves be parsed into 41 | /// key/value pairs. 42 | pub struct PaxExtensions<'entry> { 43 | data: slice::Split<'entry, u8, fn(&u8) -> bool>, 44 | } 45 | 46 | impl<'entry> PaxExtensions<'entry> { 47 | /// Create new pax extensions iterator from the given entry data. 48 | pub fn new(a: &'entry [u8]) -> Self { 49 | fn is_newline(a: &u8) -> bool { 50 | *a == b'\n' 51 | } 52 | PaxExtensions { 53 | data: a.split(is_newline), 54 | } 55 | } 56 | } 57 | 58 | /// A key/value pair corresponding to a pax extension. 59 | pub struct PaxExtension<'entry> { 60 | key: &'entry [u8], 61 | value: &'entry [u8], 62 | } 63 | 64 | pub fn pax_extensions_value(a: &[u8], key: &str) -> Option { 65 | for extension in PaxExtensions::new(a) { 66 | let current_extension = match extension { 67 | Ok(ext) => ext, 68 | Err(_) => return None, 69 | }; 70 | if current_extension.key() != Ok(key) { 71 | continue; 72 | } 73 | 74 | let value = match current_extension.value() { 75 | Ok(value) => value, 76 | Err(_) => return None, 77 | }; 78 | let result = match value.parse::() { 79 | Ok(result) => result, 80 | Err(_) => return None, 81 | }; 82 | return Some(result); 83 | } 84 | None 85 | } 86 | 87 | impl<'entry> Iterator for PaxExtensions<'entry> { 88 | type Item = io::Result>; 89 | 90 | fn next(&mut self) -> Option>> { 91 | let line = match self.data.next() { 92 | Some([]) => return None, 93 | Some(line) => line, 94 | None => return None, 95 | }; 96 | 97 | Some( 98 | line.iter() 99 | .position(|b| *b == b' ') 100 | .and_then(|i| { 101 | str::from_utf8(&line[..i]) 102 | .ok() 103 | .and_then(|len| len.parse::().ok().map(|j| (i + 1, j))) 104 | }) 105 | .and_then(|(kvstart, reported_len)| { 106 | if line.len() + 1 == reported_len { 107 | line[kvstart..] 108 | .iter() 109 | .position(|b| *b == b'=') 110 | .map(|equals| (kvstart, equals)) 111 | } else { 112 | None 113 | } 114 | }) 115 | .map(|(kvstart, equals)| PaxExtension { 116 | key: &line[kvstart..kvstart + equals], 117 | value: &line[kvstart + equals + 1..], 118 | }) 119 | .ok_or_else(|| other("malformed pax extension")), 120 | ) 121 | } 122 | } 123 | 124 | impl<'entry> PaxExtension<'entry> { 125 | /// Returns the key for this key/value pair parsed as a string. 126 | /// 127 | /// May fail if the key isn't actually utf-8. 128 | pub fn key(&self) -> Result<&'entry str, str::Utf8Error> { 129 | str::from_utf8(self.key) 130 | } 131 | 132 | /// Returns the underlying raw bytes for the key of this key/value pair. 133 | pub fn key_bytes(&self) -> &'entry [u8] { 134 | self.key 135 | } 136 | 137 | /// Returns the value for this key/value pair parsed as a string. 138 | /// 139 | /// May fail if the value isn't actually utf-8. 140 | pub fn value(&self) -> Result<&'entry str, str::Utf8Error> { 141 | str::from_utf8(self.value) 142 | } 143 | 144 | /// Returns the underlying raw bytes for this value of this key/value pair. 145 | pub fn value_bytes(&self) -> &'entry [u8] { 146 | self.value 147 | } 148 | } 149 | 150 | /// Extension trait for `Builder` to append PAX extended headers. 151 | impl crate::Builder { 152 | /// Append PAX extended headers to the archive. 153 | /// 154 | /// Takes in an iterator over the list of headers to add to convert it into a header set formatted. 155 | /// 156 | /// Returns io::Error if an error occurs, else it returns () 157 | pub fn append_pax_extensions<'key, 'value>( 158 | &mut self, 159 | headers: impl IntoIterator, 160 | ) -> Result<(), io::Error> { 161 | // Store the headers formatted before write 162 | let mut data: Vec = Vec::new(); 163 | 164 | // For each key in headers, convert into a sized space and add it to data. 165 | // This will then be written in the file 166 | for (key, value) in headers { 167 | let mut len_len = 1; 168 | let mut max_len = 10; 169 | let rest_len = 3 + key.len() + value.len(); 170 | while rest_len + len_len >= max_len { 171 | len_len += 1; 172 | max_len *= 10; 173 | } 174 | let len = rest_len + len_len; 175 | write!(&mut data, "{} {}=", len, key)?; 176 | data.extend_from_slice(value); 177 | data.push(b'\n'); 178 | } 179 | 180 | // Ignore the header append if it's empty. 181 | if data.is_empty() { 182 | return Ok(()); 183 | } 184 | 185 | // Create a header of type XHeader, set the size to the length of the 186 | // data, set the entry type to XHeader, and set the checksum 187 | // then append the header and the data to the archive. 188 | let mut header = crate::Header::new_ustar(); 189 | let data_as_bytes: &[u8] = &data; 190 | header.set_size(data_as_bytes.len() as u64); 191 | header.set_entry_type(crate::EntryType::XHeader); 192 | header.set_cksum(); 193 | self.append(&header, data_as_bytes) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /tests/archives/simple_missing_last_header.tar: -------------------------------------------------------------------------------- 1 | a0000664000175000017500000000000012361661662007666 0ustar alexalexb0000664000175000017500000000000012361661662007667 0ustar alexalexc0000664000175000017500000000000012361661662007670 0ustar alexalex -------------------------------------------------------------------------------- /tests/archives/link.tar: -------------------------------------------------------------------------------- 1 | lnk0000777000175000017500000000000012624625331010546 2fileustar devdevfile0000644000175000017500000000001612624625317010043 0ustar devdevfile_contents 2 | -------------------------------------------------------------------------------- /tests/archives/simple.tar: -------------------------------------------------------------------------------- 1 | a0000664000175000017500000000000012361661662007666 0ustar alexalexb0000664000175000017500000000000012361661662007667 0ustar alexalexc0000664000175000017500000000000012361661662007670 0ustar alexalex -------------------------------------------------------------------------------- /tests/archives/directory.tar: -------------------------------------------------------------------------------- 1 | a/0000755000175000017500000000000012440021574010647 5ustar mvdnesmvdnesa/b/0000755000175000017500000000000012440021525011064 5ustar mvdnesmvdnesa/c0000644000175000017500000000000212440021631010776 0ustar mvdnesmvdnesc 2 | -------------------------------------------------------------------------------- /tests/archives/sparse-1.tar: -------------------------------------------------------------------------------- 1 | a.big0000664000175000017500000001001414012201656017723 Sustar fauxfaux000000000000000001000000004000000000000000140000400001400000000000000040000140MB through 2 | 1MB through 3 | -------------------------------------------------------------------------------- /tests/archives/pax_size.tar: -------------------------------------------------------------------------------- 1 | ./PaxHeaders.31978/test0000644000000000000000000000014313665276445011711 xustar0030 mtime=1591049509.784738529 2 | 30 atime=1591049509.784738529 3 | 30 ctime=1591049509.784738529 4 | 9 size=4 5 | test0000664000175000017510000000000013665276445013215 0ustar00irisliuirisliu00000000000000 -------------------------------------------------------------------------------- /tests/archives/biguid_pax.tar: -------------------------------------------------------------------------------- 1 | ./PaxHeaders.241169/test.txt0000644000000000000000000000017614455221404012567 xustar0018 uid=4294967294 2 | 18 gid=4294967294 3 | 30 mtime=1689592580.626944296 4 | 30 atime=1689592580.626944296 5 | 30 ctime=1689592580.630944276 6 | ./test.txt0000644000000000000000000000001614455221404011117 0ustar0000000000000000Hello, world! 7 | -------------------------------------------------------------------------------- /tests/archives/xattrs.tar: -------------------------------------------------------------------------------- 1 | ./PaxHeaders.8071/a0000644000000000000000000000013112727267131011041 xustar0030 mtime=1465740889.394991526 2 | 29 atime=1465740884.12838135 3 | 30 ctime=1465740889.394991526 4 | a/0000755000175000001440000000000012727267131011477 5ustar00muteusers00000000000000a/PaxHeaders.8071/b0000644000000000000000000000017512727267131011135 xustar0030 mtime=1465740889.394991526 5 | 30 atime=1465740889.394991526 6 | 30 ctime=1465740898.211565394 7 | 35 SCHILY.xattr.user.pax.flags=epm 8 | a/b0000644000175000001440000000000012727267131011631 0ustar00muteusers00000000000000 -------------------------------------------------------------------------------- /tests/archives/sparse.tar: -------------------------------------------------------------------------------- 1 | sparse_begin.txt0000644000175000001440000000100012710737150020023 Sustar pcusers0000000000000000001000000000176400000000000000000017640test 2 | sparse_end.txt0000644000175000001440000000065112710737305015465 Sustar pcusers000000170000000000065100000017651test_end 3 | sparse_ext.txt0000644000175000001440000000500512710742433023641 Sustar pcusers00000010000000000010000000003000000000001000000000500000000000100000000070000000000010000000013000500000110000000000010000000013000000000000005text 4 | text 5 | text 6 | text 7 | text 8 | text 9 | sparse.txt0000644000175000001440000000200012710737456020710 Sustar pcusers00000010000000000010000000002700000000001000000000400000000000000000000040000hello 10 | world 11 | -------------------------------------------------------------------------------- /tests/header/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{self, File}; 2 | use std::io::{self, Write}; 3 | use std::path::Path; 4 | use std::{mem, thread, time}; 5 | 6 | use tempfile::Builder; 7 | 8 | use tar::{GnuHeader, Header, HeaderMode}; 9 | 10 | #[test] 11 | fn default_gnu() { 12 | let mut h = Header::new_gnu(); 13 | assert!(h.as_gnu().is_some()); 14 | assert!(h.as_gnu_mut().is_some()); 15 | assert!(h.as_ustar().is_none()); 16 | assert!(h.as_ustar_mut().is_none()); 17 | } 18 | 19 | #[test] 20 | fn goto_old() { 21 | let mut h = Header::new_old(); 22 | assert!(h.as_gnu().is_none()); 23 | assert!(h.as_gnu_mut().is_none()); 24 | assert!(h.as_ustar().is_none()); 25 | assert!(h.as_ustar_mut().is_none()); 26 | } 27 | 28 | #[test] 29 | fn goto_ustar() { 30 | let mut h = Header::new_ustar(); 31 | assert!(h.as_gnu().is_none()); 32 | assert!(h.as_gnu_mut().is_none()); 33 | assert!(h.as_ustar().is_some()); 34 | assert!(h.as_ustar_mut().is_some()); 35 | } 36 | 37 | #[test] 38 | fn link_name() { 39 | let mut h = Header::new_gnu(); 40 | h.set_link_name("foo").unwrap(); 41 | assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("foo")); 42 | h.set_link_name("../foo").unwrap(); 43 | assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("../foo")); 44 | h.set_link_name("foo/bar").unwrap(); 45 | assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("foo/bar")); 46 | h.set_link_name("foo\\ba").unwrap(); 47 | if cfg!(windows) { 48 | assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("foo/ba")); 49 | } else { 50 | assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("foo\\ba")); 51 | } 52 | 53 | let name = "foo\\bar\0"; 54 | for (slot, val) in h.as_old_mut().linkname.iter_mut().zip(name.as_bytes()) { 55 | *slot = *val; 56 | } 57 | assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("foo\\bar")); 58 | 59 | assert!(h.set_link_name("\0").is_err()); 60 | } 61 | 62 | #[test] 63 | fn mtime() { 64 | let h = Header::new_gnu(); 65 | assert_eq!(h.mtime().unwrap(), 0); 66 | 67 | let h = Header::new_ustar(); 68 | assert_eq!(h.mtime().unwrap(), 0); 69 | 70 | let h = Header::new_old(); 71 | assert_eq!(h.mtime().unwrap(), 0); 72 | } 73 | 74 | #[test] 75 | fn user_and_group_name() { 76 | let mut h = Header::new_gnu(); 77 | h.set_username("foo").unwrap(); 78 | h.set_groupname("bar").unwrap(); 79 | assert_eq!(h.username().unwrap(), Some("foo")); 80 | assert_eq!(h.groupname().unwrap(), Some("bar")); 81 | 82 | h = Header::new_ustar(); 83 | h.set_username("foo").unwrap(); 84 | h.set_groupname("bar").unwrap(); 85 | assert_eq!(h.username().unwrap(), Some("foo")); 86 | assert_eq!(h.groupname().unwrap(), Some("bar")); 87 | 88 | h = Header::new_old(); 89 | assert_eq!(h.username().unwrap(), None); 90 | assert_eq!(h.groupname().unwrap(), None); 91 | assert!(h.set_username("foo").is_err()); 92 | assert!(h.set_groupname("foo").is_err()); 93 | } 94 | 95 | #[test] 96 | fn dev_major_minor() { 97 | let mut h = Header::new_gnu(); 98 | h.set_device_major(1).unwrap(); 99 | h.set_device_minor(2).unwrap(); 100 | assert_eq!(h.device_major().unwrap(), Some(1)); 101 | assert_eq!(h.device_minor().unwrap(), Some(2)); 102 | 103 | h = Header::new_ustar(); 104 | h.set_device_major(1).unwrap(); 105 | h.set_device_minor(2).unwrap(); 106 | assert_eq!(h.device_major().unwrap(), Some(1)); 107 | assert_eq!(h.device_minor().unwrap(), Some(2)); 108 | 109 | h.as_ustar_mut().unwrap().dev_minor[0] = 0x7f; 110 | h.as_ustar_mut().unwrap().dev_major[0] = 0x7f; 111 | assert!(h.device_major().is_err()); 112 | assert!(h.device_minor().is_err()); 113 | 114 | h.as_ustar_mut().unwrap().dev_minor[0] = b'g'; 115 | h.as_ustar_mut().unwrap().dev_major[0] = b'h'; 116 | assert!(h.device_major().is_err()); 117 | assert!(h.device_minor().is_err()); 118 | 119 | h = Header::new_old(); 120 | assert_eq!(h.device_major().unwrap(), None); 121 | assert_eq!(h.device_minor().unwrap(), None); 122 | assert!(h.set_device_major(1).is_err()); 123 | assert!(h.set_device_minor(1).is_err()); 124 | } 125 | 126 | #[test] 127 | fn set_path() { 128 | let mut h = Header::new_gnu(); 129 | h.set_path("foo").unwrap(); 130 | assert_eq!(h.path().unwrap().to_str(), Some("foo")); 131 | h.set_path("foo/").unwrap(); 132 | assert_eq!(h.path().unwrap().to_str(), Some("foo/")); 133 | h.set_path("foo/bar").unwrap(); 134 | assert_eq!(h.path().unwrap().to_str(), Some("foo/bar")); 135 | h.set_path("foo\\bar").unwrap(); 136 | if cfg!(windows) { 137 | assert_eq!(h.path().unwrap().to_str(), Some("foo/bar")); 138 | } else { 139 | assert_eq!(h.path().unwrap().to_str(), Some("foo\\bar")); 140 | } 141 | 142 | // set_path documentation explicitly states it removes any ".", signifying the 143 | // current directory, from the path. This test ensures that documented 144 | // behavior occurs 145 | h.set_path("./control").unwrap(); 146 | assert_eq!(h.path().unwrap().to_str(), Some("control")); 147 | 148 | let long_name = "foo".repeat(100); 149 | let medium1 = "foo".repeat(52); 150 | let medium2 = "fo/".repeat(52); 151 | 152 | assert!(h.set_path(&long_name).is_err()); 153 | assert!(h.set_path(&medium1).is_err()); 154 | assert!(h.set_path(&medium2).is_err()); 155 | assert!(h.set_path("\0").is_err()); 156 | 157 | assert!(h.set_path("..").is_err()); 158 | assert!(h.set_path("foo/..").is_err()); 159 | assert!(h.set_path("foo/../bar").is_err()); 160 | 161 | h = Header::new_ustar(); 162 | h.set_path("foo").unwrap(); 163 | assert_eq!(h.path().unwrap().to_str(), Some("foo")); 164 | 165 | assert!(h.set_path(&long_name).is_err()); 166 | assert!(h.set_path(&medium1).is_err()); 167 | h.set_path(&medium2).unwrap(); 168 | assert_eq!(h.path().unwrap().to_str(), Some(&medium2[..])); 169 | } 170 | 171 | #[test] 172 | fn set_ustar_path_hard() { 173 | let mut h = Header::new_ustar(); 174 | let p = Path::new("a").join(vec!["a"; 100].join("")); 175 | h.set_path(&p).unwrap(); 176 | assert_eq!(h.path().unwrap(), p); 177 | } 178 | 179 | #[test] 180 | fn set_metadata_deterministic() { 181 | let td = Builder::new().prefix("tar-rs").tempdir().unwrap(); 182 | let tmppath = td.path().join("tmpfile"); 183 | 184 | fn mk_header(path: &Path, readonly: bool) -> Result { 185 | let mut file = File::create(path).unwrap(); 186 | file.write_all(b"c").unwrap(); 187 | let mut perms = file.metadata().unwrap().permissions(); 188 | perms.set_readonly(readonly); 189 | fs::set_permissions(path, perms).unwrap(); 190 | let mut h = Header::new_ustar(); 191 | h.set_metadata_in_mode(&path.metadata().unwrap(), HeaderMode::Deterministic); 192 | Ok(h) 193 | } 194 | 195 | // Create "the same" File twice in a row, one second apart, with differing readonly values. 196 | let one = mk_header(tmppath.as_path(), false).unwrap(); 197 | thread::sleep(time::Duration::from_millis(1050)); 198 | let two = mk_header(tmppath.as_path(), true).unwrap(); 199 | 200 | // Always expected to match. 201 | assert_eq!(one.size().unwrap(), two.size().unwrap()); 202 | assert_eq!(one.path().unwrap(), two.path().unwrap()); 203 | assert_eq!(one.mode().unwrap(), two.mode().unwrap()); 204 | 205 | // Would not match without `Deterministic`. 206 | assert_eq!(one.mtime().unwrap(), two.mtime().unwrap()); 207 | assert_eq!(one.mtime().unwrap(), 1153704088); 208 | // TODO: No great way to validate that these would not be filled, but 209 | // check them anyway. 210 | assert_eq!(one.uid().unwrap(), two.uid().unwrap()); 211 | assert_eq!(one.gid().unwrap(), two.gid().unwrap()); 212 | } 213 | 214 | #[test] 215 | fn extended_numeric_format() { 216 | let mut h: GnuHeader = unsafe { mem::zeroed() }; 217 | h.as_header_mut().set_size(42); 218 | assert_eq!(h.size, [48, 48, 48, 48, 48, 48, 48, 48, 48, 53, 50, 0]); 219 | h.as_header_mut().set_size(8589934593); 220 | assert_eq!(h.size, [0x80, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0, 1]); 221 | h.as_header_mut().set_size(44); 222 | assert_eq!(h.size, [48, 48, 48, 48, 48, 48, 48, 48, 48, 53, 52, 0]); 223 | h.size = [0x80, 0, 0, 0, 0, 0, 0, 0x02, 0, 0, 0, 0]; 224 | assert_eq!(h.as_header().entry_size().unwrap(), 0x0200000000); 225 | h.size = [48, 48, 48, 48, 48, 48, 48, 48, 48, 53, 51, 0]; 226 | assert_eq!(h.as_header().entry_size().unwrap(), 43); 227 | 228 | h.as_header_mut().set_gid(42); 229 | assert_eq!(h.gid, [48, 48, 48, 48, 48, 53, 50, 0]); 230 | assert_eq!(h.as_header().gid().unwrap(), 42); 231 | h.as_header_mut().set_gid(0x7fffffffffffffff); 232 | assert_eq!(h.gid, [0xff; 8]); 233 | assert_eq!(h.as_header().gid().unwrap(), 0x7fffffffffffffff); 234 | h.uid = [0x80, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78]; 235 | assert_eq!(h.as_header().uid().unwrap(), 0x12345678); 236 | 237 | h.mtime = [ 238 | 0x80, 0, 0, 0, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 239 | ]; 240 | assert_eq!(h.as_header().mtime().unwrap(), 0x0123456789abcdef); 241 | 242 | h.realsize = [0x80, 0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde]; 243 | assert_eq!(h.real_size().unwrap(), 0x00123456789abcde); 244 | h.sparse[0].offset = [0x80, 0, 0, 0, 0, 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd]; 245 | assert_eq!(h.sparse[0].offset().unwrap(), 0x000123456789abcd); 246 | h.sparse[0].numbytes = [0x80, 0, 0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc]; 247 | assert_eq!(h.sparse[0].length().unwrap(), 0x0000123456789abc); 248 | } 249 | 250 | #[test] 251 | fn byte_slice_conversion() { 252 | let h = Header::new_gnu(); 253 | let b: &[u8] = h.as_bytes(); 254 | let b_conv: &[u8] = Header::from_byte_slice(h.as_bytes()).as_bytes(); 255 | assert_eq!(b, b_conv); 256 | } 257 | -------------------------------------------------------------------------------- /tests/archives/pax2.tar: -------------------------------------------------------------------------------- 1 | aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaa0000644000000000000000000000043614071074000030402 xustar00202 path=aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/ 2 | 28 mtime=1625585664.5056427 3 | 28 atime=1625585665.4756427 4 | 28 ctime=1625585664.5056427 5 | aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaa0000755000175000017500000000000014071074000034243 5ustar00nullponullpo00000000000000aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaa0000644000000000000000000000045514071074000030403 xustar00217 path=aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/ccccccccccccccc 6 | 28 mtime=1625585664.5056427 7 | 28 atime=1625585702.9056427 8 | 28 ctime=1625585850.8956427 9 | aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaa0000644000175000017500000000002014071074000034235 0ustar00nullponullpo00000000000000ccccccccccccccc 10 | aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaa0000644000000000000000000000045514071073773030423 xustar00217 path=aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/bbbbbbbbbbbbbbb 11 | 28 mtime=1625585659.6556427 12 | 28 atime=1625585702.9056427 13 | 28 ctime=1625585659.6556427 14 | aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaa0000644000175000017500000000002014071073773034255 0ustar00nullponullpo00000000000000bbbbbbbbbbbbbbb 15 | ./PaxHeaders.1116970/bbbbbbbbbbbbbbb0000644000000000000000000000046114071074261014027 xustar00221 linkpath=aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/bbbbbbbbbbbbbbb 16 | 28 mtime=1625585841.1456427 17 | 28 atime=1625585850.8956427 18 | 28 ctime=1625585841.1456427 19 | bbbbbbbbbbbbbbb0000777000175000017500000000000014071074261037153 2aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaustar00nullponullpo00000000000000./PaxHeaders.1116970/ccccccccccccccc0000644000000000000000000000046114071074000014035 xustar00221 linkpath=aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/ccccccccccccccc 20 | 28 mtime=1625585664.5056427 21 | 28 atime=1625585878.7956427 22 | 28 ctime=1625585850.8956427 23 | ccccccccccccccc0000644000175000017500000000000014071074000037151 1aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaaaaaaaaaaaa/aaaaustar00nullponullpo00000000000000 -------------------------------------------------------------------------------- /tests/archives/reading_files.tar: -------------------------------------------------------------------------------- 1 | a0000664000175000017500000000002612361754471007677 0ustar alexalexa 2 | a 3 | a 4 | a 5 | a 6 | a 7 | a 8 | a 9 | a 10 | a 11 | a 12 | b0000664000175000017500000000002612361754476007705 0ustar alexalexb 13 | b 14 | b 15 | b 16 | b 17 | b 18 | b 19 | b 20 | b 21 | b 22 | b 23 | -------------------------------------------------------------------------------- /tests/archives/pax.tar: -------------------------------------------------------------------------------- 1 | ./PaxHeaders.4428/Cargo.toml0000644000000000000000000000013112647240064012625 xustar0030 mtime=1453146164.953123768 2 | 29 atime=1453251915.24892486 3 | 30 ctime=1453146164.953123768 4 | Cargo.toml0000664000175000017500000000137012647240064012762 0ustar00alexalex00000000000000[package] 5 | 6 | name = "tar" 7 | version = "0.3.3" 8 | authors = ["Alex Crichton "] 9 | homepage = "https://github.com/alexcrichton/tar-rs" 10 | repository = "https://github.com/alexcrichton/tar-rs" 11 | documentation = "http://alexcrichton.com/tar-rs" 12 | license = "MIT/Apache-2.0" 13 | keywords = ["tar", "tarfile", "encoding"] 14 | readme = "README.md" 15 | 16 | description = """ 17 | A Rust implementation of a TAR file reader and writer. This library does not 18 | currently handle compression, but it is abstract over all I/O readers and 19 | writers. Additionally, great lengths are taken to ensure that the entire 20 | contents are never required to be entirely resident in memory all at once. 21 | """ 22 | 23 | [dependencies] 24 | libc = "0.2" 25 | filetime = "0.1.5" 26 | winapi = "0.2" 27 | 28 | [dev-dependencies] 29 | tempdir = "0.3" 30 | src/PaxHeaders.4428/lib.rs0000644000000000000000000000013212647616024012447 xustar0030 mtime=1453267988.890050911 31 | 30 atime=1453268000.730238913 32 | 30 ctime=1453267988.898051038 33 | src/lib.rs0000664000175000017500000000452612647616024012746 0ustar00alexalex00000000000000//! A library for reading and writing TAR archives 34 | //! 35 | //! This library provides utilities necessary to manage TAR archives [1] 36 | //! abstracted over a reader or writer. Great strides are taken to ensure that 37 | //! an archive is never required to be fully resident in memory, all objects 38 | //! provide largely a streaming interface to read bytes from. 39 | //! 40 | //! [1]: http://en.wikipedia.org/wiki/Tar_%28computing%29 41 | 42 | // More docs about the detailed tar format can also be found here: 43 | // http://www.freebsd.org/cgi/man.cgi?query=tar&sektion=5&manpath=FreeBSD+8-current 44 | 45 | // NB: some of the coding patterns and idioms here may seem a little strange. 46 | // This is currently attempting to expose a super generic interface while 47 | // also not forcing clients to codegen the entire crate each time they use 48 | // it. To that end lots of work is done to ensure that concrete 49 | // implementations are all found in this crate and the generic functions are 50 | // all just super thin wrappers (e.g. easy to codegen). 51 | 52 | #![doc(html_root_url = "http://alexcrichton.com/tar-rs")] 53 | #![deny(missing_docs)] 54 | #![cfg_attr(test, deny(warnings))] 55 | 56 | extern crate libc; 57 | extern crate winapi; 58 | extern crate filetime; 59 | 60 | use std::io::{Error, ErrorKind}; 61 | use std::ops::{Deref, DerefMut}; 62 | 63 | pub use header::{Header, UstarHeader, GnuHeader, GnuSparseHeader}; 64 | pub use entry_type::EntryType; 65 | pub use entry::Entry; 66 | pub use archive::{Archive, Entries}; 67 | pub use builder::Builder; 68 | pub use pax::{PaxExtensions, PaxExtension}; 69 | 70 | mod archive; 71 | mod builder; 72 | mod entry; 73 | mod entry_type; 74 | mod error; 75 | mod header; 76 | mod pax; 77 | 78 | // FIXME(rust-lang/rust#26403): 79 | // Right now there's a bug when a DST struct's last field has more 80 | // alignment than the rest of a structure, causing invalid pointers to be 81 | // created when it's casted around at runtime. To work around this we force 82 | // our DST struct to instead have a forcibly higher alignment via a 83 | // synthesized u64 (hopefully the largest alignment we'll run into in 84 | // practice), and this should hopefully ensure that the pointers all work 85 | // out. 86 | struct AlignHigher(u64, R); 87 | 88 | impl Deref for AlignHigher { 89 | type Target = R; 90 | fn deref(&self) -> &R { &self.1 } 91 | } 92 | impl DerefMut for AlignHigher { 93 | fn deref_mut(&mut self) -> &mut R { &mut self.1 } 94 | } 95 | 96 | fn other(msg: &str) -> Error { 97 | Error::new(ErrorKind::Other, msg) 98 | } 99 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /tests/entry.rs: -------------------------------------------------------------------------------- 1 | extern crate tar; 2 | extern crate tempfile; 3 | 4 | use std::fs::create_dir; 5 | use std::fs::File; 6 | use std::io::Read; 7 | 8 | use tempfile::Builder; 9 | 10 | #[test] 11 | fn absolute_symlink() { 12 | let mut ar = tar::Builder::new(Vec::new()); 13 | 14 | let mut header = tar::Header::new_gnu(); 15 | header.set_size(0); 16 | header.set_entry_type(tar::EntryType::Symlink); 17 | header.set_path("foo").unwrap(); 18 | header.set_link_name("/bar").unwrap(); 19 | header.set_cksum(); 20 | ar.append(&header, &[][..]).unwrap(); 21 | 22 | let bytes = ar.into_inner().unwrap(); 23 | let mut ar = tar::Archive::new(&bytes[..]); 24 | 25 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 26 | ar.unpack(td.path()).unwrap(); 27 | 28 | td.path().join("foo").symlink_metadata().unwrap(); 29 | 30 | let mut ar = tar::Archive::new(&bytes[..]); 31 | let mut entries = ar.entries().unwrap(); 32 | let entry = entries.next().unwrap().unwrap(); 33 | assert_eq!(&*entry.link_name_bytes().unwrap(), b"/bar"); 34 | } 35 | 36 | #[test] 37 | fn absolute_hardlink() { 38 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 39 | let mut ar = tar::Builder::new(Vec::new()); 40 | 41 | let mut header = tar::Header::new_gnu(); 42 | header.set_size(0); 43 | header.set_entry_type(tar::EntryType::Regular); 44 | header.set_path("foo").unwrap(); 45 | header.set_cksum(); 46 | ar.append(&header, &[][..]).unwrap(); 47 | 48 | let mut header = tar::Header::new_gnu(); 49 | header.set_size(0); 50 | header.set_entry_type(tar::EntryType::Link); 51 | header.set_path("bar").unwrap(); 52 | // This absolute path under tempdir will be created at unpack time 53 | header.set_link_name(td.path().join("foo")).unwrap(); 54 | header.set_cksum(); 55 | ar.append(&header, &[][..]).unwrap(); 56 | 57 | let bytes = ar.into_inner().unwrap(); 58 | let mut ar = tar::Archive::new(&bytes[..]); 59 | 60 | ar.unpack(td.path()).unwrap(); 61 | td.path().join("foo").metadata().unwrap(); 62 | td.path().join("bar").metadata().unwrap(); 63 | } 64 | 65 | #[test] 66 | fn relative_hardlink() { 67 | let mut ar = tar::Builder::new(Vec::new()); 68 | 69 | let mut header = tar::Header::new_gnu(); 70 | header.set_size(0); 71 | header.set_entry_type(tar::EntryType::Regular); 72 | header.set_path("foo").unwrap(); 73 | header.set_cksum(); 74 | ar.append(&header, &[][..]).unwrap(); 75 | 76 | let mut header = tar::Header::new_gnu(); 77 | header.set_size(0); 78 | header.set_entry_type(tar::EntryType::Link); 79 | header.set_path("bar").unwrap(); 80 | header.set_link_name("foo").unwrap(); 81 | header.set_cksum(); 82 | ar.append(&header, &[][..]).unwrap(); 83 | 84 | let bytes = ar.into_inner().unwrap(); 85 | let mut ar = tar::Archive::new(&bytes[..]); 86 | 87 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 88 | ar.unpack(td.path()).unwrap(); 89 | td.path().join("foo").metadata().unwrap(); 90 | td.path().join("bar").metadata().unwrap(); 91 | } 92 | 93 | #[test] 94 | fn absolute_link_deref_error() { 95 | let mut ar = tar::Builder::new(Vec::new()); 96 | 97 | let mut header = tar::Header::new_gnu(); 98 | header.set_size(0); 99 | header.set_entry_type(tar::EntryType::Symlink); 100 | header.set_path("foo").unwrap(); 101 | header.set_link_name("/").unwrap(); 102 | header.set_cksum(); 103 | ar.append(&header, &[][..]).unwrap(); 104 | 105 | let mut header = tar::Header::new_gnu(); 106 | header.set_size(0); 107 | header.set_entry_type(tar::EntryType::Regular); 108 | header.set_path("foo/bar").unwrap(); 109 | header.set_cksum(); 110 | ar.append(&header, &[][..]).unwrap(); 111 | 112 | let bytes = ar.into_inner().unwrap(); 113 | let mut ar = tar::Archive::new(&bytes[..]); 114 | 115 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 116 | assert!(ar.unpack(td.path()).is_err()); 117 | td.path().join("foo").symlink_metadata().unwrap(); 118 | assert!(File::open(td.path().join("foo").join("bar")).is_err()); 119 | } 120 | 121 | #[test] 122 | fn relative_link_deref_error() { 123 | let mut ar = tar::Builder::new(Vec::new()); 124 | 125 | let mut header = tar::Header::new_gnu(); 126 | header.set_size(0); 127 | header.set_entry_type(tar::EntryType::Symlink); 128 | header.set_path("foo").unwrap(); 129 | header.set_link_name("../../../../").unwrap(); 130 | header.set_cksum(); 131 | ar.append(&header, &[][..]).unwrap(); 132 | 133 | let mut header = tar::Header::new_gnu(); 134 | header.set_size(0); 135 | header.set_entry_type(tar::EntryType::Regular); 136 | header.set_path("foo/bar").unwrap(); 137 | header.set_cksum(); 138 | ar.append(&header, &[][..]).unwrap(); 139 | 140 | let bytes = ar.into_inner().unwrap(); 141 | let mut ar = tar::Archive::new(&bytes[..]); 142 | 143 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 144 | assert!(ar.unpack(td.path()).is_err()); 145 | td.path().join("foo").symlink_metadata().unwrap(); 146 | assert!(File::open(td.path().join("foo").join("bar")).is_err()); 147 | } 148 | 149 | #[test] 150 | #[cfg(unix)] 151 | fn directory_maintains_permissions() { 152 | use ::std::os::unix::fs::PermissionsExt; 153 | 154 | let mut ar = tar::Builder::new(Vec::new()); 155 | 156 | let mut header = tar::Header::new_gnu(); 157 | header.set_size(0); 158 | header.set_entry_type(tar::EntryType::Directory); 159 | header.set_path("foo").unwrap(); 160 | header.set_mode(0o777); 161 | header.set_cksum(); 162 | ar.append(&header, &[][..]).unwrap(); 163 | 164 | let bytes = ar.into_inner().unwrap(); 165 | let mut ar = tar::Archive::new(&bytes[..]); 166 | 167 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 168 | ar.unpack(td.path()).unwrap(); 169 | let f = File::open(td.path().join("foo")).unwrap(); 170 | let md = f.metadata().unwrap(); 171 | assert!(md.is_dir()); 172 | assert_eq!(md.permissions().mode(), 0o40777); 173 | } 174 | 175 | #[test] 176 | #[cfg(unix)] 177 | fn set_entry_mask() { 178 | use ::std::os::unix::fs::PermissionsExt; 179 | 180 | let mut ar = tar::Builder::new(Vec::new()); 181 | 182 | let mut header = tar::Header::new_gnu(); 183 | header.set_size(0); 184 | header.set_entry_type(tar::EntryType::Regular); 185 | header.set_path("foo").unwrap(); 186 | header.set_mode(0o777); 187 | header.set_cksum(); 188 | ar.append(&header, &[][..]).unwrap(); 189 | 190 | let bytes = ar.into_inner().unwrap(); 191 | let mut ar = tar::Archive::new(&bytes[..]); 192 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 193 | let foo_path = td.path().join("foo"); 194 | 195 | let mut entries = ar.entries().unwrap(); 196 | let mut foo = entries.next().unwrap().unwrap(); 197 | foo.set_mask(0o027); 198 | foo.unpack(&foo_path).unwrap(); 199 | 200 | let f = File::open(foo_path).unwrap(); 201 | let md = f.metadata().unwrap(); 202 | assert!(md.is_file()); 203 | assert_eq!(md.permissions().mode(), 0o100750); 204 | } 205 | 206 | #[test] 207 | #[cfg(not(windows))] // dangling symlinks have weird permissions 208 | fn modify_link_just_created() { 209 | let mut ar = tar::Builder::new(Vec::new()); 210 | 211 | let mut header = tar::Header::new_gnu(); 212 | header.set_size(0); 213 | header.set_entry_type(tar::EntryType::Symlink); 214 | header.set_path("foo").unwrap(); 215 | header.set_link_name("bar").unwrap(); 216 | header.set_cksum(); 217 | ar.append(&header, &[][..]).unwrap(); 218 | 219 | let mut header = tar::Header::new_gnu(); 220 | header.set_size(0); 221 | header.set_entry_type(tar::EntryType::Regular); 222 | header.set_path("bar/foo").unwrap(); 223 | header.set_cksum(); 224 | ar.append(&header, &[][..]).unwrap(); 225 | 226 | let mut header = tar::Header::new_gnu(); 227 | header.set_size(0); 228 | header.set_entry_type(tar::EntryType::Regular); 229 | header.set_path("foo/bar").unwrap(); 230 | header.set_cksum(); 231 | ar.append(&header, &[][..]).unwrap(); 232 | 233 | let bytes = ar.into_inner().unwrap(); 234 | let mut ar = tar::Archive::new(&bytes[..]); 235 | 236 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 237 | ar.unpack(td.path()).unwrap(); 238 | 239 | File::open(td.path().join("bar/foo")).unwrap(); 240 | File::open(td.path().join("bar/bar")).unwrap(); 241 | File::open(td.path().join("foo/foo")).unwrap(); 242 | File::open(td.path().join("foo/bar")).unwrap(); 243 | } 244 | 245 | #[test] 246 | #[cfg(not(windows))] // dangling symlinks have weird permissions 247 | fn modify_outside_with_relative_symlink() { 248 | let mut ar = tar::Builder::new(Vec::new()); 249 | 250 | let mut header = tar::Header::new_gnu(); 251 | header.set_size(0); 252 | header.set_entry_type(tar::EntryType::Symlink); 253 | header.set_path("symlink").unwrap(); 254 | header.set_link_name("..").unwrap(); 255 | header.set_cksum(); 256 | ar.append(&header, &[][..]).unwrap(); 257 | 258 | let mut header = tar::Header::new_gnu(); 259 | header.set_size(0); 260 | header.set_entry_type(tar::EntryType::Regular); 261 | header.set_path("symlink/foo/bar").unwrap(); 262 | header.set_cksum(); 263 | ar.append(&header, &[][..]).unwrap(); 264 | 265 | let bytes = ar.into_inner().unwrap(); 266 | let mut ar = tar::Archive::new(&bytes[..]); 267 | 268 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 269 | let tar_dir = td.path().join("tar"); 270 | create_dir(&tar_dir).unwrap(); 271 | assert!(ar.unpack(tar_dir).is_err()); 272 | assert!(!td.path().join("foo").exists()); 273 | } 274 | 275 | #[test] 276 | fn parent_paths_error() { 277 | let mut ar = tar::Builder::new(Vec::new()); 278 | 279 | let mut header = tar::Header::new_gnu(); 280 | header.set_size(0); 281 | header.set_entry_type(tar::EntryType::Symlink); 282 | header.set_path("foo").unwrap(); 283 | header.set_link_name("..").unwrap(); 284 | header.set_cksum(); 285 | ar.append(&header, &[][..]).unwrap(); 286 | 287 | let mut header = tar::Header::new_gnu(); 288 | header.set_size(0); 289 | header.set_entry_type(tar::EntryType::Regular); 290 | header.set_path("foo/bar").unwrap(); 291 | header.set_cksum(); 292 | ar.append(&header, &[][..]).unwrap(); 293 | 294 | let bytes = ar.into_inner().unwrap(); 295 | let mut ar = tar::Archive::new(&bytes[..]); 296 | 297 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 298 | assert!(ar.unpack(td.path()).is_err()); 299 | td.path().join("foo").symlink_metadata().unwrap(); 300 | assert!(File::open(td.path().join("foo").join("bar")).is_err()); 301 | } 302 | 303 | #[test] 304 | #[cfg(unix)] 305 | fn good_parent_paths_ok() { 306 | use std::path::PathBuf; 307 | let mut ar = tar::Builder::new(Vec::new()); 308 | 309 | let mut header = tar::Header::new_gnu(); 310 | header.set_size(0); 311 | header.set_entry_type(tar::EntryType::Symlink); 312 | header.set_path(PathBuf::from("foo").join("bar")).unwrap(); 313 | header 314 | .set_link_name(PathBuf::from("..").join("bar")) 315 | .unwrap(); 316 | header.set_cksum(); 317 | ar.append(&header, &[][..]).unwrap(); 318 | 319 | let mut header = tar::Header::new_gnu(); 320 | header.set_size(0); 321 | header.set_entry_type(tar::EntryType::Regular); 322 | header.set_path("bar").unwrap(); 323 | header.set_cksum(); 324 | ar.append(&header, &[][..]).unwrap(); 325 | 326 | let bytes = ar.into_inner().unwrap(); 327 | let mut ar = tar::Archive::new(&bytes[..]); 328 | 329 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 330 | ar.unpack(td.path()).unwrap(); 331 | td.path().join("foo").join("bar").read_link().unwrap(); 332 | let dst = td.path().join("foo").join("bar").canonicalize().unwrap(); 333 | File::open(dst).unwrap(); 334 | } 335 | 336 | #[test] 337 | fn modify_hard_link_just_created() { 338 | let mut ar = tar::Builder::new(Vec::new()); 339 | 340 | let mut header = tar::Header::new_gnu(); 341 | header.set_size(0); 342 | header.set_entry_type(tar::EntryType::Link); 343 | header.set_path("foo").unwrap(); 344 | header.set_link_name("../test").unwrap(); 345 | header.set_cksum(); 346 | ar.append(&header, &[][..]).unwrap(); 347 | 348 | let mut header = tar::Header::new_gnu(); 349 | header.set_size(1); 350 | header.set_entry_type(tar::EntryType::Regular); 351 | header.set_path("foo").unwrap(); 352 | header.set_cksum(); 353 | ar.append(&header, &b"x"[..]).unwrap(); 354 | 355 | let bytes = ar.into_inner().unwrap(); 356 | let mut ar = tar::Archive::new(&bytes[..]); 357 | 358 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 359 | 360 | let test = td.path().join("test"); 361 | File::create(&test).unwrap(); 362 | 363 | let dir = td.path().join("dir"); 364 | assert!(ar.unpack(&dir).is_err()); 365 | 366 | let mut contents = Vec::new(); 367 | File::open(&test) 368 | .unwrap() 369 | .read_to_end(&mut contents) 370 | .unwrap(); 371 | assert_eq!(contents.len(), 0); 372 | } 373 | 374 | #[test] 375 | fn modify_symlink_just_created() { 376 | let mut ar = tar::Builder::new(Vec::new()); 377 | 378 | let mut header = tar::Header::new_gnu(); 379 | header.set_size(0); 380 | header.set_entry_type(tar::EntryType::Symlink); 381 | header.set_path("foo").unwrap(); 382 | header.set_link_name("../test").unwrap(); 383 | header.set_cksum(); 384 | ar.append(&header, &[][..]).unwrap(); 385 | 386 | let mut header = tar::Header::new_gnu(); 387 | header.set_size(1); 388 | header.set_entry_type(tar::EntryType::Regular); 389 | header.set_path("foo").unwrap(); 390 | header.set_cksum(); 391 | ar.append(&header, &b"x"[..]).unwrap(); 392 | 393 | let bytes = ar.into_inner().unwrap(); 394 | let mut ar = tar::Archive::new(&bytes[..]); 395 | 396 | let td = Builder::new().prefix("tar").tempdir().unwrap(); 397 | 398 | let test = td.path().join("test"); 399 | File::create(&test).unwrap(); 400 | 401 | let dir = td.path().join("dir"); 402 | ar.unpack(&dir).unwrap(); 403 | 404 | let mut contents = Vec::new(); 405 | File::open(&test) 406 | .unwrap() 407 | .read_to_end(&mut contents) 408 | .unwrap(); 409 | assert_eq!(contents.len(), 0); 410 | } 411 | -------------------------------------------------------------------------------- /src/archive.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{Cell, RefCell}; 2 | use std::cmp; 3 | use std::convert::TryFrom; 4 | use std::fs; 5 | use std::io::prelude::*; 6 | use std::io::{self, SeekFrom}; 7 | use std::marker; 8 | use std::path::Path; 9 | 10 | use crate::entry::{EntryFields, EntryIo}; 11 | use crate::error::TarError; 12 | use crate::header::BLOCK_SIZE; 13 | use crate::other; 14 | use crate::pax::*; 15 | use crate::{Entry, GnuExtSparseHeader, GnuSparseHeader, Header}; 16 | 17 | /// A top-level representation of an archive file. 18 | /// 19 | /// This archive can have an entry added to it and it can be iterated over. 20 | pub struct Archive { 21 | inner: ArchiveInner, 22 | } 23 | 24 | pub struct ArchiveInner { 25 | pos: Cell, 26 | mask: u32, 27 | unpack_xattrs: bool, 28 | preserve_permissions: bool, 29 | preserve_ownerships: bool, 30 | preserve_mtime: bool, 31 | overwrite: bool, 32 | ignore_zeros: bool, 33 | obj: RefCell, 34 | } 35 | 36 | /// An iterator over the entries of an archive. 37 | pub struct Entries<'a, R: 'a + Read> { 38 | fields: EntriesFields<'a>, 39 | _ignored: marker::PhantomData<&'a Archive>, 40 | } 41 | 42 | trait SeekRead: Read + Seek {} 43 | impl SeekRead for R {} 44 | 45 | struct EntriesFields<'a> { 46 | archive: &'a Archive, 47 | seekable_archive: Option<&'a Archive>, 48 | next: u64, 49 | done: bool, 50 | raw: bool, 51 | } 52 | 53 | impl Archive { 54 | /// Create a new archive with the underlying object as the reader. 55 | pub fn new(obj: R) -> Archive { 56 | Archive { 57 | inner: ArchiveInner { 58 | mask: u32::MIN, 59 | unpack_xattrs: false, 60 | preserve_permissions: false, 61 | preserve_ownerships: false, 62 | preserve_mtime: true, 63 | overwrite: true, 64 | ignore_zeros: false, 65 | obj: RefCell::new(obj), 66 | pos: Cell::new(0), 67 | }, 68 | } 69 | } 70 | 71 | /// Unwrap this archive, returning the underlying object. 72 | pub fn into_inner(self) -> R { 73 | self.inner.obj.into_inner() 74 | } 75 | 76 | /// Construct an iterator over the entries in this archive. 77 | /// 78 | /// Note that care must be taken to consider each entry within an archive in 79 | /// sequence. If entries are processed out of sequence (from what the 80 | /// iterator returns), then the contents read for each entry may be 81 | /// corrupted. 82 | pub fn entries(&mut self) -> io::Result> { 83 | let me: &mut Archive = self; 84 | me._entries(None).map(|fields| Entries { 85 | fields, 86 | _ignored: marker::PhantomData, 87 | }) 88 | } 89 | 90 | /// Unpacks the contents tarball into the specified `dst`. 91 | /// 92 | /// This function will iterate over the entire contents of this tarball, 93 | /// extracting each file in turn to the location specified by the entry's 94 | /// path name. 95 | /// 96 | /// This operation is relatively sensitive in that it will not write files 97 | /// outside of the path specified by `dst`. Files in the archive which have 98 | /// a '..' in their path are skipped during the unpacking process. 99 | /// 100 | /// # Examples 101 | /// 102 | /// ```no_run 103 | /// use std::fs::File; 104 | /// use tar::Archive; 105 | /// 106 | /// let mut ar = Archive::new(File::open("foo.tar").unwrap()); 107 | /// ar.unpack("foo").unwrap(); 108 | /// ``` 109 | pub fn unpack>(&mut self, dst: P) -> io::Result<()> { 110 | let me: &mut Archive = self; 111 | me._unpack(dst.as_ref()) 112 | } 113 | 114 | /// Set the mask of the permission bits when unpacking this entry. 115 | /// 116 | /// The mask will be inverted when applying against a mode, similar to how 117 | /// `umask` works on Unix. In logical notation it looks like: 118 | /// 119 | /// ```text 120 | /// new_mode = old_mode & (~mask) 121 | /// ``` 122 | /// 123 | /// The mask is 0 by default and is currently only implemented on Unix. 124 | pub fn set_mask(&mut self, mask: u32) { 125 | self.inner.mask = mask; 126 | } 127 | 128 | /// Indicate whether extended file attributes (xattrs on Unix) are preserved 129 | /// when unpacking this archive. 130 | /// 131 | /// This flag is disabled by default and is currently only implemented on 132 | /// Unix using xattr support. This may eventually be implemented for 133 | /// Windows, however, if other archive implementations are found which do 134 | /// this as well. 135 | pub fn set_unpack_xattrs(&mut self, unpack_xattrs: bool) { 136 | self.inner.unpack_xattrs = unpack_xattrs; 137 | } 138 | 139 | /// Indicate whether extended permissions (like suid on Unix) are preserved 140 | /// when unpacking this entry. 141 | /// 142 | /// This flag is disabled by default and is currently only implemented on 143 | /// Unix. 144 | pub fn set_preserve_permissions(&mut self, preserve: bool) { 145 | self.inner.preserve_permissions = preserve; 146 | } 147 | 148 | /// Indicate whether numeric ownership ids (like uid and gid on Unix) 149 | /// are preserved when unpacking this entry. 150 | /// 151 | /// This flag is disabled by default and is currently only implemented on 152 | /// Unix. 153 | pub fn set_preserve_ownerships(&mut self, preserve: bool) { 154 | self.inner.preserve_ownerships = preserve; 155 | } 156 | 157 | /// Indicate whether files and symlinks should be overwritten on extraction. 158 | pub fn set_overwrite(&mut self, overwrite: bool) { 159 | self.inner.overwrite = overwrite; 160 | } 161 | 162 | /// Indicate whether access time information is preserved when unpacking 163 | /// this entry. 164 | /// 165 | /// This flag is enabled by default. 166 | pub fn set_preserve_mtime(&mut self, preserve: bool) { 167 | self.inner.preserve_mtime = preserve; 168 | } 169 | 170 | /// Ignore zeroed headers, which would otherwise indicate to the archive that it has no more 171 | /// entries. 172 | /// 173 | /// This can be used in case multiple tar archives have been concatenated together. 174 | pub fn set_ignore_zeros(&mut self, ignore_zeros: bool) { 175 | self.inner.ignore_zeros = ignore_zeros; 176 | } 177 | } 178 | 179 | impl Archive { 180 | /// Construct an iterator over the entries in this archive for a seekable 181 | /// reader. Seek will be used to efficiently skip over file contents. 182 | /// 183 | /// Note that care must be taken to consider each entry within an archive in 184 | /// sequence. If entries are processed out of sequence (from what the 185 | /// iterator returns), then the contents read for each entry may be 186 | /// corrupted. 187 | pub fn entries_with_seek(&mut self) -> io::Result> { 188 | let me: &Archive = self; 189 | let me_seekable: &Archive = self; 190 | me._entries(Some(me_seekable)).map(|fields| Entries { 191 | fields, 192 | _ignored: marker::PhantomData, 193 | }) 194 | } 195 | } 196 | 197 | impl Archive { 198 | fn _entries<'a>( 199 | &'a self, 200 | seekable_archive: Option<&'a Archive>, 201 | ) -> io::Result> { 202 | if self.inner.pos.get() != 0 { 203 | return Err(other( 204 | "cannot call entries unless archive is at \ 205 | position 0", 206 | )); 207 | } 208 | Ok(EntriesFields { 209 | archive: self, 210 | seekable_archive, 211 | done: false, 212 | next: 0, 213 | raw: false, 214 | }) 215 | } 216 | 217 | fn _unpack(&mut self, dst: &Path) -> io::Result<()> { 218 | if dst.symlink_metadata().is_err() { 219 | fs::create_dir_all(dst) 220 | .map_err(|e| TarError::new(format!("failed to create `{}`", dst.display()), e))?; 221 | } 222 | 223 | // Canonicalizing the dst directory will prepend the path with '\\?\' 224 | // on windows which will allow windows APIs to treat the path as an 225 | // extended-length path with a 32,767 character limit. Otherwise all 226 | // unpacked paths over 260 characters will fail on creation with a 227 | // NotFound exception. 228 | let dst = &dst.canonicalize().unwrap_or(dst.to_path_buf()); 229 | 230 | // Delay any directory entries until the end (they will be created if needed by 231 | // descendants), to ensure that directory permissions do not interfere with descendant 232 | // extraction. 233 | let mut directories = Vec::new(); 234 | for entry in self._entries(None)? { 235 | let mut file = entry.map_err(|e| TarError::new("failed to iterate over archive", e))?; 236 | if file.header().entry_type() == crate::EntryType::Directory { 237 | directories.push(file); 238 | } else { 239 | file.unpack_in(dst)?; 240 | } 241 | } 242 | 243 | // Apply the directories. 244 | // 245 | // Note: the order of application is important to permissions. That is, we must traverse 246 | // the filesystem graph in topological ordering or else we risk not being able to create 247 | // child directories within those of more restrictive permissions. See [0] for details. 248 | // 249 | // [0]: 250 | directories.sort_by(|a, b| b.path_bytes().cmp(&a.path_bytes())); 251 | for mut dir in directories { 252 | dir.unpack_in(dst)?; 253 | } 254 | 255 | Ok(()) 256 | } 257 | } 258 | 259 | impl<'a, R: Read> Entries<'a, R> { 260 | /// Indicates whether this iterator will return raw entries or not. 261 | /// 262 | /// If the raw list of entries is returned, then no preprocessing happens 263 | /// on account of this library, for example taking into account GNU long name 264 | /// or long link archive members. Raw iteration is disabled by default. 265 | pub fn raw(self, raw: bool) -> Entries<'a, R> { 266 | Entries { 267 | fields: EntriesFields { raw, ..self.fields }, 268 | _ignored: marker::PhantomData, 269 | } 270 | } 271 | } 272 | impl<'a, R: Read> Iterator for Entries<'a, R> { 273 | type Item = io::Result>; 274 | 275 | fn next(&mut self) -> Option>> { 276 | self.fields 277 | .next() 278 | .map(|result| result.map(|e| EntryFields::from(e).into_entry())) 279 | } 280 | } 281 | 282 | impl<'a> EntriesFields<'a> { 283 | fn next_entry_raw( 284 | &mut self, 285 | pax_extensions: Option<&[u8]>, 286 | ) -> io::Result>> { 287 | let mut header = Header::new_old(); 288 | let mut header_pos = self.next; 289 | loop { 290 | // Seek to the start of the next header in the archive 291 | let delta = self.next - self.archive.inner.pos.get(); 292 | self.skip(delta)?; 293 | 294 | // EOF is an indicator that we are at the end of the archive. 295 | if !try_read_all(&mut &self.archive.inner, header.as_mut_bytes())? { 296 | return Ok(None); 297 | } 298 | 299 | // If a header is not all zeros, we have another valid header. 300 | // Otherwise, check if we are ignoring zeros and continue, or break as if this is the 301 | // end of the archive. 302 | if !header.as_bytes().iter().all(|i| *i == 0) { 303 | self.next += BLOCK_SIZE; 304 | break; 305 | } 306 | 307 | if !self.archive.inner.ignore_zeros { 308 | return Ok(None); 309 | } 310 | self.next += BLOCK_SIZE; 311 | header_pos = self.next; 312 | } 313 | 314 | // Make sure the checksum is ok 315 | let sum = header.as_bytes()[..148] 316 | .iter() 317 | .chain(&header.as_bytes()[156..]) 318 | .fold(0, |a, b| a + (*b as u32)) 319 | + 8 * 32; 320 | let cksum = header.cksum()?; 321 | if sum != cksum { 322 | return Err(other("archive header checksum mismatch")); 323 | } 324 | 325 | let mut pax_size: Option = None; 326 | if let Some(pax_extensions_ref) = &pax_extensions { 327 | pax_size = pax_extensions_value(pax_extensions_ref, PAX_SIZE); 328 | 329 | if let Some(pax_uid) = pax_extensions_value(pax_extensions_ref, PAX_UID) { 330 | header.set_uid(pax_uid); 331 | } 332 | 333 | if let Some(pax_gid) = pax_extensions_value(pax_extensions_ref, PAX_GID) { 334 | header.set_gid(pax_gid); 335 | } 336 | } 337 | 338 | let file_pos = self.next; 339 | let mut size = header.entry_size()?; 340 | if size == 0 { 341 | if let Some(pax_size) = pax_size { 342 | size = pax_size; 343 | } 344 | } 345 | let ret = EntryFields { 346 | size, 347 | header_pos, 348 | file_pos, 349 | data: vec![EntryIo::Data((&self.archive.inner).take(size))], 350 | header, 351 | long_pathname: None, 352 | long_linkname: None, 353 | pax_extensions: None, 354 | mask: self.archive.inner.mask, 355 | unpack_xattrs: self.archive.inner.unpack_xattrs, 356 | preserve_permissions: self.archive.inner.preserve_permissions, 357 | preserve_mtime: self.archive.inner.preserve_mtime, 358 | overwrite: self.archive.inner.overwrite, 359 | preserve_ownerships: self.archive.inner.preserve_ownerships, 360 | }; 361 | 362 | // Store where the next entry is, rounding up by 512 bytes (the size of 363 | // a header); 364 | let size = size 365 | .checked_add(BLOCK_SIZE - 1) 366 | .ok_or_else(|| other("size overflow"))?; 367 | self.next = self 368 | .next 369 | .checked_add(size & !(BLOCK_SIZE - 1)) 370 | .ok_or_else(|| other("size overflow"))?; 371 | 372 | Ok(Some(ret.into_entry())) 373 | } 374 | 375 | fn next_entry(&mut self) -> io::Result>> { 376 | if self.raw { 377 | return self.next_entry_raw(None); 378 | } 379 | 380 | let mut gnu_longname = None; 381 | let mut gnu_longlink = None; 382 | let mut pax_extensions = None; 383 | let mut processed = 0; 384 | loop { 385 | processed += 1; 386 | let entry = match self.next_entry_raw(pax_extensions.as_deref())? { 387 | Some(entry) => entry, 388 | None if processed > 1 => { 389 | return Err(other( 390 | "members found describing a future member \ 391 | but no future member found", 392 | )); 393 | } 394 | None => return Ok(None), 395 | }; 396 | 397 | let is_recognized_header = 398 | entry.header().as_gnu().is_some() || entry.header().as_ustar().is_some(); 399 | 400 | if is_recognized_header && entry.header().entry_type().is_gnu_longname() { 401 | if gnu_longname.is_some() { 402 | return Err(other( 403 | "two long name entries describing \ 404 | the same member", 405 | )); 406 | } 407 | gnu_longname = Some(EntryFields::from(entry).read_all()?); 408 | continue; 409 | } 410 | 411 | if is_recognized_header && entry.header().entry_type().is_gnu_longlink() { 412 | if gnu_longlink.is_some() { 413 | return Err(other( 414 | "two long name entries describing \ 415 | the same member", 416 | )); 417 | } 418 | gnu_longlink = Some(EntryFields::from(entry).read_all()?); 419 | continue; 420 | } 421 | 422 | if is_recognized_header && entry.header().entry_type().is_pax_local_extensions() { 423 | if pax_extensions.is_some() { 424 | return Err(other( 425 | "two pax extensions entries describing \ 426 | the same member", 427 | )); 428 | } 429 | pax_extensions = Some(EntryFields::from(entry).read_all()?); 430 | continue; 431 | } 432 | 433 | let mut fields = EntryFields::from(entry); 434 | fields.long_pathname = gnu_longname; 435 | fields.long_linkname = gnu_longlink; 436 | fields.pax_extensions = pax_extensions; 437 | self.parse_sparse_header(&mut fields)?; 438 | return Ok(Some(fields.into_entry())); 439 | } 440 | } 441 | 442 | fn parse_sparse_header(&mut self, entry: &mut EntryFields<'a>) -> io::Result<()> { 443 | if !entry.header.entry_type().is_gnu_sparse() { 444 | return Ok(()); 445 | } 446 | let gnu = match entry.header.as_gnu() { 447 | Some(gnu) => gnu, 448 | None => return Err(other("sparse entry type listed but not GNU header")), 449 | }; 450 | 451 | // Sparse files are represented internally as a list of blocks that are 452 | // read. Blocks are either a bunch of 0's or they're data from the 453 | // underlying archive. 454 | // 455 | // Blocks of a sparse file are described by the `GnuSparseHeader` 456 | // structure, some of which are contained in `GnuHeader` but some of 457 | // which may also be contained after the first header in further 458 | // headers. 459 | // 460 | // We read off all the blocks here and use the `add_block` function to 461 | // incrementally add them to the list of I/O block (in `entry.data`). 462 | // The `add_block` function also validates that each chunk comes after 463 | // the previous, we don't overrun the end of the file, and each block is 464 | // aligned to a 512-byte boundary in the archive itself. 465 | // 466 | // At the end we verify that the sparse file size (`Header::size`) is 467 | // the same as the current offset (described by the list of blocks) as 468 | // well as the amount of data read equals the size of the entry 469 | // (`Header::entry_size`). 470 | entry.data.truncate(0); 471 | 472 | let mut cur = 0; 473 | let mut remaining = entry.size; 474 | { 475 | let data = &mut entry.data; 476 | let reader = &self.archive.inner; 477 | let size = entry.size; 478 | let mut add_block = |block: &GnuSparseHeader| -> io::Result<_> { 479 | if block.is_empty() { 480 | return Ok(()); 481 | } 482 | let off = block.offset()?; 483 | let len = block.length()?; 484 | if len != 0 && (size - remaining) % BLOCK_SIZE != 0 { 485 | return Err(other( 486 | "previous block in sparse file was not \ 487 | aligned to 512-byte boundary", 488 | )); 489 | } else if off < cur { 490 | return Err(other( 491 | "out of order or overlapping sparse \ 492 | blocks", 493 | )); 494 | } else if cur < off { 495 | let block = io::repeat(0).take(off - cur); 496 | data.push(EntryIo::Pad(block)); 497 | } 498 | cur = off 499 | .checked_add(len) 500 | .ok_or_else(|| other("more bytes listed in sparse file than u64 can hold"))?; 501 | remaining = remaining.checked_sub(len).ok_or_else(|| { 502 | other( 503 | "sparse file consumed more data than the header \ 504 | listed", 505 | ) 506 | })?; 507 | data.push(EntryIo::Data(reader.take(len))); 508 | Ok(()) 509 | }; 510 | for block in gnu.sparse.iter() { 511 | add_block(block)? 512 | } 513 | if gnu.is_extended() { 514 | let mut ext = GnuExtSparseHeader::new(); 515 | ext.isextended[0] = 1; 516 | while ext.is_extended() { 517 | if !try_read_all(&mut &self.archive.inner, ext.as_mut_bytes())? { 518 | return Err(other("failed to read extension")); 519 | } 520 | 521 | self.next += BLOCK_SIZE; 522 | for block in ext.sparse.iter() { 523 | add_block(block)?; 524 | } 525 | } 526 | } 527 | } 528 | if cur != gnu.real_size()? { 529 | return Err(other( 530 | "mismatch in sparse file chunks and \ 531 | size in header", 532 | )); 533 | } 534 | entry.size = cur; 535 | if remaining > 0 { 536 | return Err(other( 537 | "mismatch in sparse file chunks and \ 538 | entry size in header", 539 | )); 540 | } 541 | Ok(()) 542 | } 543 | 544 | fn skip(&mut self, mut amt: u64) -> io::Result<()> { 545 | if let Some(seekable_archive) = self.seekable_archive { 546 | let pos = io::SeekFrom::Current( 547 | i64::try_from(amt).map_err(|_| other("seek position out of bounds"))?, 548 | ); 549 | (&seekable_archive.inner).seek(pos)?; 550 | } else { 551 | let mut buf = [0u8; 4096 * 8]; 552 | while amt > 0 { 553 | let n = cmp::min(amt, buf.len() as u64); 554 | let n = (&self.archive.inner).read(&mut buf[..n as usize])?; 555 | if n == 0 { 556 | return Err(other("unexpected EOF during skip")); 557 | } 558 | amt -= n as u64; 559 | } 560 | } 561 | Ok(()) 562 | } 563 | } 564 | 565 | impl<'a> Iterator for EntriesFields<'a> { 566 | type Item = io::Result>; 567 | 568 | fn next(&mut self) -> Option>> { 569 | if self.done { 570 | None 571 | } else { 572 | match self.next_entry() { 573 | Ok(Some(e)) => Some(Ok(e)), 574 | Ok(None) => { 575 | self.done = true; 576 | None 577 | } 578 | Err(e) => { 579 | self.done = true; 580 | Some(Err(e)) 581 | } 582 | } 583 | } 584 | } 585 | } 586 | 587 | impl Read for &ArchiveInner { 588 | fn read(&mut self, into: &mut [u8]) -> io::Result { 589 | let i = self.obj.borrow_mut().read(into)?; 590 | self.pos.set(self.pos.get() + i as u64); 591 | Ok(i) 592 | } 593 | } 594 | 595 | impl Seek for &ArchiveInner { 596 | fn seek(&mut self, pos: SeekFrom) -> io::Result { 597 | let pos = self.obj.borrow_mut().seek(pos)?; 598 | self.pos.set(pos); 599 | Ok(pos) 600 | } 601 | } 602 | 603 | /// Try to fill the buffer from the reader. 604 | /// 605 | /// If the reader reaches its end before filling the buffer at all, returns `false`. 606 | /// Otherwise returns `true`. 607 | fn try_read_all(r: &mut R, buf: &mut [u8]) -> io::Result { 608 | let mut read = 0; 609 | while read < buf.len() { 610 | match r.read(&mut buf[read..])? { 611 | 0 => { 612 | if read == 0 { 613 | return Ok(false); 614 | } 615 | 616 | return Err(other("failed to read entire block")); 617 | } 618 | n => read += n, 619 | } 620 | } 621 | Ok(true) 622 | } 623 | --------------------------------------------------------------------------------