Files
cli/vendor/tar/tests/all.rs

2025 lines
68 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extern crate filetime;
extern crate tar;
extern crate tempfile;
#[cfg(all(unix, feature = "xattr"))]
extern crate xattr;
use std::fs::{self, File};
use std::io::prelude::*;
use std::io::{self, BufWriter, Cursor};
use std::iter::repeat;
use std::path::{Path, PathBuf};
use filetime::FileTime;
use rand::rngs::SmallRng;
use rand::{Rng, SeedableRng};
use tar::{Archive, Builder, Entries, Entry, EntryType, Header, HeaderMode};
use tempfile::{Builder as TempBuilder, TempDir};
/// A reader wrapper that returns partial results from `read()` to exercise
/// parsers that might assume `read()` fills the entire buffer.
///
/// Each call returns between 1 and buf.len() bytes, biased toward small
/// reads by taking the minimum of two uniform samples. This gives roughly
/// quadratic density toward 1, so small reads (1-10 bytes) occur frequently
/// while large reads still happen. Uses a deterministic seeded RNG so
/// tests remain reproducible.
struct RandomReader<R> {
inner: R,
rng: SmallRng,
}
impl<R> RandomReader<R> {
fn new(inner: R) -> Self {
RandomReader {
inner,
rng: SmallRng::seed_from_u64(0),
}
}
}
impl<R: Read> Read for RandomReader<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if buf.is_empty() {
return self.inner.read(buf);
}
// Take the min of two uniform samples to bias toward small reads.
let a = self.rng.gen_range(1..=buf.len());
let b = self.rng.gen_range(1..=buf.len());
self.inner.read(&mut buf[..a.min(b)])
}
}
/// Convenience: wrap a byte slice in a RandomReader<Cursor<_>>.
///
/// The RNG is seeded from a hash of the data, so different archives
/// exercise different read-size sequences while remaining deterministic.
fn random_cursor_reader<D: AsRef<[u8]>>(data: D) -> RandomReader<Cursor<D>> {
RandomReader::new(Cursor::new(data))
}
macro_rules! tar {
($e:expr) => {
&include_bytes!(concat!("archives/", $e))[..]
};
}
mod header;
/// test that we can concatenate the simple.tar archive and extract the same entries twice when we
/// use the ignore_zeros option.
#[test]
fn simple_concat() {
let bytes = tar!("simple.tar");
let mut archive_bytes = Vec::new();
archive_bytes.extend(bytes);
let original_names: Vec<String> =
decode_names(&mut Archive::new(random_cursor_reader(&archive_bytes)));
let expected: Vec<&str> = original_names.iter().map(|n| n.as_str()).collect();
// concat two archives (with null in-between);
archive_bytes.extend(bytes);
// test now that when we read the archive, it stops processing at the first zero header.
let actual = decode_names(&mut Archive::new(random_cursor_reader(&archive_bytes)));
assert_eq!(expected, actual);
// extend expected by itself.
let expected: Vec<&str> = {
let mut o = Vec::new();
o.extend(&expected);
o.extend(&expected);
o
};
let mut ar = Archive::new(random_cursor_reader(&archive_bytes));
ar.set_ignore_zeros(true);
let actual = decode_names(&mut ar);
assert_eq!(expected, actual);
fn decode_names<R>(ar: &mut Archive<R>) -> Vec<String>
where
R: Read,
{
let mut names = Vec::new();
for entry in ar.entries().unwrap() {
let e = entry.unwrap();
names.push(::std::str::from_utf8(&e.path_bytes()).unwrap().to_string());
}
names
}
}
#[test]
fn header_impls() {
let mut ar = Archive::new(random_cursor_reader(tar!("simple.tar")));
let hn = Header::new_old();
let hnb = hn.as_bytes();
for file in ar.entries().unwrap() {
let file = file.unwrap();
let h1 = file.header();
let h1b = h1.as_bytes();
let h2 = h1.clone();
let h2b = h2.as_bytes();
assert!(h1b[..] == h2b[..] && h2b[..] != hnb[..])
}
}
#[test]
fn header_impls_missing_last_header() {
let mut ar = Archive::new(random_cursor_reader(tar!("simple_missing_last_header.tar")));
let hn = Header::new_old();
let hnb = hn.as_bytes();
for file in ar.entries().unwrap() {
let file = file.unwrap();
let h1 = file.header();
let h1b = h1.as_bytes();
let h2 = h1.clone();
let h2b = h2.as_bytes();
assert!(h1b[..] == h2b[..] && h2b[..] != hnb[..])
}
}
#[test]
fn reading_files() {
let rdr = random_cursor_reader(tar!("reading_files.tar"));
let mut ar = Archive::new(rdr);
let mut entries = ar.entries().unwrap();
let mut a = entries.next().unwrap().unwrap();
assert_eq!(&*a.header().path_bytes(), b"a");
let mut s = String::new();
a.read_to_string(&mut s).unwrap();
assert_eq!(s, "a\na\na\na\na\na\na\na\na\na\na\n");
let mut b = entries.next().unwrap().unwrap();
assert_eq!(&*b.header().path_bytes(), b"b");
s.truncate(0);
b.read_to_string(&mut s).unwrap();
assert_eq!(s, "b\nb\nb\nb\nb\nb\nb\nb\nb\nb\nb\n");
assert!(entries.next().is_none());
}
#[test]
fn writing_files() {
let mut ar = Builder::new(Vec::new());
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let path = td.path().join("test");
File::create(&path).unwrap().write_all(b"test").unwrap();
ar.append_file("test2", &mut File::open(&path).unwrap())
.unwrap();
let data = ar.into_inner().unwrap();
let mut ar = Archive::new(Cursor::new(data));
let mut entries = ar.entries().unwrap();
let mut f = entries.next().unwrap().unwrap();
assert_eq!(&*f.header().path_bytes(), b"test2");
assert_eq!(f.header().size().unwrap(), 4);
let mut s = String::new();
f.read_to_string(&mut s).unwrap();
assert_eq!(s, "test");
assert!(entries.next().is_none());
}
#[test]
fn large_filename() {
let mut ar = Builder::new(Vec::new());
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let path = td.path().join("test");
File::create(&path).unwrap().write_all(b"test").unwrap();
let filename = "abcd/".repeat(50);
let mut header = Header::new_ustar();
header.set_path(&filename).unwrap();
header.set_metadata(&fs::metadata(&path).unwrap());
header.set_cksum();
ar.append(&header, &b"test"[..]).unwrap();
let too_long = "abcd".repeat(200);
ar.append_file(&too_long, &mut File::open(&path).unwrap())
.unwrap();
ar.append_data(&mut header, &too_long, &b"test"[..])
.unwrap();
let rd = Cursor::new(ar.into_inner().unwrap());
let mut ar = Archive::new(rd);
let mut entries = ar.entries().unwrap();
// The short entry added with `append`
let mut f = entries.next().unwrap().unwrap();
assert_eq!(&*f.header().path_bytes(), filename.as_bytes());
assert_eq!(f.header().size().unwrap(), 4);
let mut s = String::new();
f.read_to_string(&mut s).unwrap();
assert_eq!(s, "test");
// The long entry added with `append_file`
let mut f = entries.next().unwrap().unwrap();
assert_eq!(&*f.path_bytes(), too_long.as_bytes());
assert_eq!(f.header().size().unwrap(), 4);
let mut s = String::new();
f.read_to_string(&mut s).unwrap();
assert_eq!(s, "test");
// The long entry added with `append_data`
let mut f = entries.next().unwrap().unwrap();
assert!(f.header().path_bytes().len() < too_long.len());
assert_eq!(&*f.path_bytes(), too_long.as_bytes());
assert_eq!(f.header().size().unwrap(), 4);
let mut s = String::new();
f.read_to_string(&mut s).unwrap();
assert_eq!(s, "test");
assert!(entries.next().is_none());
}
// This test checks very particular scenario where a path component starting
// with ".." of a long path gets split at 100-byte mark so that ".." part goes
// into header and gets interpreted as parent dir (and rejected) .
#[test]
fn large_filename_with_dot_dot_at_100_byte_mark() {
let mut ar = Builder::new(Vec::new());
let mut header = Header::new_gnu();
header.set_mode(0o644);
header.set_size(4);
let mut long_name_with_dot_dot = "tdir/".repeat(19);
long_name_with_dot_dot.push_str("tt/..file");
ar.append_data(&mut header, &long_name_with_dot_dot, b"test".as_slice())
.unwrap();
let rd = Cursor::new(ar.into_inner().unwrap());
let mut ar = Archive::new(rd);
let mut entries = ar.entries().unwrap();
let mut f = entries.next().unwrap().unwrap();
assert_eq!(&*f.path_bytes(), long_name_with_dot_dot.as_bytes());
assert_eq!(f.header().size().unwrap(), 4);
let mut s = String::new();
f.read_to_string(&mut s).unwrap();
assert_eq!(s, "test");
assert!(entries.next().is_none());
}
fn reading_entries_common<R: Read>(mut entries: Entries<R>) {
let mut a = entries.next().unwrap().unwrap();
assert_eq!(&*a.header().path_bytes(), b"a");
let mut s = String::new();
a.read_to_string(&mut s).unwrap();
assert_eq!(s, "a\na\na\na\na\na\na\na\na\na\na\n");
s.truncate(0);
a.read_to_string(&mut s).unwrap();
assert_eq!(s, "");
let mut b = entries.next().unwrap().unwrap();
assert_eq!(&*b.header().path_bytes(), b"b");
s.truncate(0);
b.read_to_string(&mut s).unwrap();
assert_eq!(s, "b\nb\nb\nb\nb\nb\nb\nb\nb\nb\nb\n");
assert!(entries.next().is_none());
}
#[test]
fn reading_entries() {
let rdr = random_cursor_reader(tar!("reading_files.tar"));
let mut ar = Archive::new(rdr);
reading_entries_common(ar.entries().unwrap());
}
#[test]
fn reading_entries_with_seek() {
let rdr = Cursor::new(tar!("reading_files.tar"));
let mut ar = Archive::new(rdr);
reading_entries_common(ar.entries_with_seek().unwrap());
}
struct LoggingReader<R> {
inner: R,
read_bytes: u64,
}
impl<R> LoggingReader<R> {
fn new(reader: R) -> LoggingReader<R> {
LoggingReader {
inner: reader,
read_bytes: 0,
}
}
}
impl<T: Read> Read for LoggingReader<T> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.inner.read(buf).inspect(|&i| {
self.read_bytes += i as u64;
})
}
}
impl<T: Seek> Seek for LoggingReader<T> {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
self.inner.seek(pos)
}
}
#[test]
fn skipping_entries_with_seek() {
let mut reader = LoggingReader::new(Cursor::new(tar!("reading_files.tar")));
let mut ar_reader = Archive::new(&mut reader);
let files: Vec<_> = ar_reader
.entries()
.unwrap()
.map(|entry| entry.unwrap().path().unwrap().to_path_buf())
.collect();
let mut seekable_reader = LoggingReader::new(Cursor::new(tar!("reading_files.tar")));
let mut ar_seekable_reader = Archive::new(&mut seekable_reader);
let files_seekable: Vec<_> = ar_seekable_reader
.entries_with_seek()
.unwrap()
.map(|entry| entry.unwrap().path().unwrap().to_path_buf())
.collect();
assert!(files == files_seekable);
assert!(seekable_reader.read_bytes < reader.read_bytes);
}
fn check_dirtree(td: &TempDir) {
let dir_a = td.path().join("a");
let dir_b = td.path().join("a/b");
let file_c = td.path().join("a/c");
assert!(fs::metadata(&dir_a).map(|m| m.is_dir()).unwrap_or(false));
assert!(fs::metadata(&dir_b).map(|m| m.is_dir()).unwrap_or(false));
assert!(fs::metadata(&file_c).map(|m| m.is_file()).unwrap_or(false));
}
#[test]
fn extracting_directories() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let rdr = random_cursor_reader(tar!("directory.tar"));
let mut ar = Archive::new(rdr);
ar.unpack(td.path()).unwrap();
check_dirtree(&td);
}
#[test]
fn extracting_duplicate_file_fail() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let path_present = td.path().join("a");
File::create(path_present).unwrap();
let rdr = random_cursor_reader(tar!("reading_files.tar"));
let mut ar = Archive::new(rdr);
ar.set_overwrite(false);
if let Err(err) = ar.unpack(td.path()) {
if err.kind() == std::io::ErrorKind::AlreadyExists {
// as expected with overwrite false
return;
}
panic!("unexpected error: {:?}", err);
}
panic!(
"unpack() should have returned an error of kind {:?}, returned Ok",
std::io::ErrorKind::AlreadyExists
)
}
#[test]
fn extracting_duplicate_file_succeed() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let path_present = td.path().join("a");
File::create(path_present).unwrap();
let rdr = random_cursor_reader(tar!("reading_files.tar"));
let mut ar = Archive::new(rdr);
ar.set_overwrite(true);
ar.unpack(td.path()).unwrap();
}
#[test]
#[cfg(unix)]
fn extracting_duplicate_link_fail() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let path_present = td.path().join("lnk");
std::os::unix::fs::symlink("file", path_present).unwrap();
let rdr = random_cursor_reader(tar!("link.tar"));
let mut ar = Archive::new(rdr);
ar.set_overwrite(false);
if let Err(err) = ar.unpack(td.path()) {
if err.kind() == std::io::ErrorKind::AlreadyExists {
// as expected with overwrite false
return;
}
panic!("unexpected error: {:?}", err);
}
panic!(
"unpack() should have returned an error of kind {:?}, returned Ok",
std::io::ErrorKind::AlreadyExists
)
}
#[test]
#[cfg(unix)]
fn extracting_duplicate_link_succeed() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let path_present = td.path().join("lnk");
std::os::unix::fs::symlink("file", path_present).unwrap();
let rdr = random_cursor_reader(tar!("link.tar"));
let mut ar = Archive::new(rdr);
ar.set_overwrite(true);
ar.unpack(td.path()).unwrap();
}
#[test]
#[cfg(all(unix, feature = "xattr"))]
fn xattrs() {
// If /tmp is a tmpfs, xattr will fail
// The xattr crate's unit tests also use /var/tmp for this reason
let td = TempBuilder::new()
.prefix("tar-rs")
.tempdir_in("/var/tmp")
.unwrap();
let rdr = random_cursor_reader(tar!("xattrs.tar"));
let mut ar = Archive::new(rdr);
ar.set_unpack_xattrs(true);
ar.unpack(td.path()).unwrap();
let val = xattr::get(td.path().join("a/b"), "user.pax.flags").unwrap();
assert_eq!(val.unwrap(), "epm".as_bytes());
}
#[test]
#[cfg(all(unix, feature = "xattr"))]
fn no_xattrs() {
// If /tmp is a tmpfs, xattr will fail
// The xattr crate's unit tests also use /var/tmp for this reason
let td = TempBuilder::new()
.prefix("tar-rs")
.tempdir_in("/var/tmp")
.unwrap();
let rdr = random_cursor_reader(tar!("xattrs.tar"));
let mut ar = Archive::new(rdr);
ar.set_unpack_xattrs(false);
ar.unpack(td.path()).unwrap();
assert_eq!(
xattr::get(td.path().join("a/b"), "user.pax.flags").unwrap(),
None
);
}
#[test]
fn writing_and_extracting_directories() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut ar = Builder::new(Vec::new());
let tmppath = td.path().join("tmpfile");
File::create(&tmppath).unwrap().write_all(b"c").unwrap();
ar.append_dir("a", ".").unwrap();
ar.append_dir("a/b", ".").unwrap();
ar.append_file("a/c", &mut File::open(&tmppath).unwrap())
.unwrap();
ar.finish().unwrap();
let rdr = Cursor::new(ar.into_inner().unwrap());
let mut ar = Archive::new(rdr);
ar.unpack(td.path()).unwrap();
check_dirtree(&td);
}
#[test]
fn writing_and_extracting_directories_complex_permissions() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
// Archive with complex permissions which would fail to unpack if one attempted to do so
// without reordering of entries.
let mut ar = Builder::new(Vec::new());
let tmppath = td.path().join("tmpfile");
File::create(&tmppath).unwrap().write_all(b"c").unwrap();
// Root dir with very stringent permissions
let data: &[u8] = &[];
let mut header = Header::new_gnu();
header.set_mode(0o555);
header.set_entry_type(EntryType::Directory);
header.set_path("a").unwrap();
header.set_size(0);
header.set_cksum();
ar.append(&header, data).unwrap();
// Nested dir
header.set_mode(0o777);
header.set_entry_type(EntryType::Directory);
header.set_path("a/b").unwrap();
header.set_cksum();
ar.append(&header, data).unwrap();
// Nested file.
ar.append_file("a/c", &mut File::open(&tmppath).unwrap())
.unwrap();
ar.finish().unwrap();
let rdr = Cursor::new(ar.into_inner().unwrap());
let mut ar = Archive::new(rdr);
ar.unpack(td.path()).unwrap();
check_dirtree(&td);
}
#[test]
fn writing_directories_recursively() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let base_dir = td.path().join("base");
fs::create_dir(&base_dir).unwrap();
File::create(base_dir.join("file1"))
.unwrap()
.write_all(b"file1")
.unwrap();
let sub_dir = base_dir.join("sub");
fs::create_dir(&sub_dir).unwrap();
File::create(sub_dir.join("file2"))
.unwrap()
.write_all(b"file2")
.unwrap();
let mut ar = Builder::new(Vec::new());
ar.append_dir_all("foobar", base_dir).unwrap();
let data = ar.into_inner().unwrap();
let mut ar = Archive::new(Cursor::new(data));
ar.unpack(td.path()).unwrap();
let base_dir = td.path().join("foobar");
assert!(fs::metadata(&base_dir).map(|m| m.is_dir()).unwrap_or(false));
let file1_path = base_dir.join("file1");
assert!(fs::metadata(&file1_path)
.map(|m| m.is_file())
.unwrap_or(false));
let sub_dir = base_dir.join("sub");
assert!(fs::metadata(&sub_dir).map(|m| m.is_dir()).unwrap_or(false));
let file2_path = sub_dir.join("file2");
assert!(fs::metadata(&file2_path)
.map(|m| m.is_file())
.unwrap_or(false));
}
#[test]
fn append_dir_all_blank_dest() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let base_dir = td.path().join("base");
fs::create_dir(&base_dir).unwrap();
File::create(base_dir.join("file1"))
.unwrap()
.write_all(b"file1")
.unwrap();
let sub_dir = base_dir.join("sub");
fs::create_dir(&sub_dir).unwrap();
File::create(sub_dir.join("file2"))
.unwrap()
.write_all(b"file2")
.unwrap();
let mut ar = Builder::new(Vec::new());
ar.append_dir_all("", base_dir).unwrap();
let data = ar.into_inner().unwrap();
let mut ar = Archive::new(Cursor::new(data));
ar.unpack(td.path()).unwrap();
let base_dir = td.path();
assert!(fs::metadata(base_dir).map(|m| m.is_dir()).unwrap_or(false));
let file1_path = base_dir.join("file1");
assert!(fs::metadata(&file1_path)
.map(|m| m.is_file())
.unwrap_or(false));
let sub_dir = base_dir.join("sub");
assert!(fs::metadata(&sub_dir).map(|m| m.is_dir()).unwrap_or(false));
let file2_path = sub_dir.join("file2");
assert!(fs::metadata(&file2_path)
.map(|m| m.is_file())
.unwrap_or(false));
}
#[test]
fn append_dir_all_does_not_work_on_non_directory() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let path = td.path().join("test");
File::create(&path).unwrap().write_all(b"test").unwrap();
let mut ar = Builder::new(Vec::new());
let result = ar.append_dir_all("test", path);
assert!(result.is_err());
}
#[test]
fn extracting_duplicate_dirs() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let rdr = random_cursor_reader(tar!("duplicate_dirs.tar"));
let mut ar = Archive::new(rdr);
ar.unpack(td.path()).unwrap();
let some_dir = td.path().join("some_dir");
assert!(fs::metadata(&some_dir).map(|m| m.is_dir()).unwrap_or(false));
}
#[test]
fn unpack_old_style_bsd_dir() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut ar = Builder::new(Vec::new());
let mut header = Header::new_old();
header.set_entry_type(EntryType::Regular);
header.set_path("testdir/").unwrap();
header.set_size(0);
header.set_cksum();
ar.append(&header, &mut io::empty()).unwrap();
// Extracting
let rdr = Cursor::new(ar.into_inner().unwrap());
let mut ar = Archive::new(rdr);
ar.unpack(td.path()).unwrap();
// Iterating
let rdr = Cursor::new(ar.into_inner().into_inner());
let mut ar = Archive::new(rdr);
assert!(ar.entries().unwrap().all(|fr| fr.is_ok()));
assert!(td.path().join("testdir").is_dir());
}
#[test]
fn handling_incorrect_file_size() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut ar = Builder::new(Vec::new());
let path = td.path().join("tmpfile");
File::create(&path).unwrap();
let mut file = File::open(&path).unwrap();
let mut header = Header::new_old();
header.set_path("somepath").unwrap();
header.set_metadata(&file.metadata().unwrap());
header.set_size(2048); // past the end of file null blocks
header.set_cksum();
ar.append(&header, &mut file).unwrap();
// Extracting
let rdr = Cursor::new(ar.into_inner().unwrap());
let mut ar = Archive::new(rdr);
assert!(ar.unpack(td.path()).is_err());
// Iterating
let rdr = Cursor::new(ar.into_inner().into_inner());
let mut ar = Archive::new(rdr);
assert!(ar.entries().unwrap().any(|fr| fr.is_err()));
}
#[test]
fn extracting_malicious_tarball() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut evil_tar = Vec::new();
{
let mut a = Builder::new(&mut evil_tar);
let mut append = |path: &str| {
let mut header = Header::new_gnu();
assert!(header.set_path(path).is_err(), "was ok: {:?}", path);
{
let h = header.as_gnu_mut().unwrap();
for (a, b) in h.name.iter_mut().zip(path.as_bytes()) {
*a = *b;
}
}
header.set_size(1);
header.set_cksum();
a.append(&header, io::repeat(1).take(1)).unwrap();
};
append("/tmp/abs_evil.txt");
// std parse `//` as UNC path, see rust-lang/rust#100833
append(
#[cfg(not(windows))]
"//tmp/abs_evil2.txt",
#[cfg(windows)]
"C://tmp/abs_evil2.txt",
);
append("///tmp/abs_evil3.txt");
append("/./tmp/abs_evil4.txt");
append(
#[cfg(not(windows))]
"//./tmp/abs_evil5.txt",
#[cfg(windows)]
"C://./tmp/abs_evil5.txt",
);
append("///./tmp/abs_evil6.txt");
append("/../tmp/rel_evil.txt");
append("../rel_evil2.txt");
append("./../rel_evil3.txt");
append("some/../../rel_evil4.txt");
append("");
append("././//./..");
append("..");
append("/////////..");
append("/////////");
}
let mut ar = Archive::new(&evil_tar[..]);
ar.unpack(td.path()).unwrap();
assert!(fs::metadata("/tmp/abs_evil.txt").is_err());
assert!(fs::metadata("/tmp/abs_evil.txt2").is_err());
assert!(fs::metadata("/tmp/abs_evil.txt3").is_err());
assert!(fs::metadata("/tmp/abs_evil.txt4").is_err());
assert!(fs::metadata("/tmp/abs_evil.txt5").is_err());
assert!(fs::metadata("/tmp/abs_evil.txt6").is_err());
assert!(fs::metadata("/tmp/rel_evil.txt").is_err());
assert!(fs::metadata("/tmp/rel_evil.txt").is_err());
assert!(fs::metadata(td.path().join("../tmp/rel_evil.txt")).is_err());
assert!(fs::metadata(td.path().join("../rel_evil2.txt")).is_err());
assert!(fs::metadata(td.path().join("../rel_evil3.txt")).is_err());
assert!(fs::metadata(td.path().join("../rel_evil4.txt")).is_err());
// The `some` subdirectory should not be created because the only
// filename that references this has '..'.
assert!(fs::metadata(td.path().join("some")).is_err());
// The `tmp` subdirectory should be created and within this
// subdirectory, there should be files named `abs_evil.txt` through
// `abs_evil6.txt`.
assert!(fs::metadata(td.path().join("tmp"))
.map(|m| m.is_dir())
.unwrap_or(false));
assert!(fs::metadata(td.path().join("tmp/abs_evil.txt"))
.map(|m| m.is_file())
.unwrap_or(false));
assert!(fs::metadata(td.path().join("tmp/abs_evil2.txt"))
.map(|m| m.is_file())
.unwrap_or(false));
assert!(fs::metadata(td.path().join("tmp/abs_evil3.txt"))
.map(|m| m.is_file())
.unwrap_or(false));
assert!(fs::metadata(td.path().join("tmp/abs_evil4.txt"))
.map(|m| m.is_file())
.unwrap_or(false));
assert!(fs::metadata(td.path().join("tmp/abs_evil5.txt"))
.map(|m| m.is_file())
.unwrap_or(false));
assert!(fs::metadata(td.path().join("tmp/abs_evil6.txt"))
.map(|m| m.is_file())
.unwrap_or(false));
}
#[test]
fn octal_spaces() {
let rdr = random_cursor_reader(tar!("spaces.tar"));
let mut ar = Archive::new(rdr);
let entry = ar.entries().unwrap().next().unwrap().unwrap();
assert_eq!(entry.header().mode().unwrap() & 0o777, 0o777);
assert_eq!(entry.header().uid().unwrap(), 0);
assert_eq!(entry.header().gid().unwrap(), 0);
assert_eq!(entry.header().size().unwrap(), 2);
assert_eq!(entry.header().mtime().unwrap(), 0o12440016664);
assert_eq!(entry.header().cksum().unwrap(), 0o4253);
}
#[test]
fn extracting_malformed_tar_null_blocks() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut ar = Builder::new(Vec::new());
let path1 = td.path().join("tmpfile1");
let path2 = td.path().join("tmpfile2");
File::create(&path1).unwrap();
File::create(&path2).unwrap();
ar.append_file("tmpfile1", &mut File::open(&path1).unwrap())
.unwrap();
let mut data = ar.into_inner().unwrap();
let amt = data.len();
data.truncate(amt - 512);
let mut ar = Builder::new(data);
ar.append_file("tmpfile2", &mut File::open(&path2).unwrap())
.unwrap();
ar.finish().unwrap();
let data = ar.into_inner().unwrap();
let mut ar = Archive::new(&data[..]);
assert!(ar.unpack(td.path()).is_ok());
}
#[test]
fn empty_filename() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let rdr = random_cursor_reader(tar!("empty_filename.tar"));
let mut ar = Archive::new(rdr);
assert!(ar.unpack(td.path()).is_ok());
}
#[test]
fn file_times() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let rdr = random_cursor_reader(tar!("file_times.tar"));
let mut ar = Archive::new(rdr);
ar.unpack(td.path()).unwrap();
let meta = fs::metadata(td.path().join("a")).unwrap();
let mtime = FileTime::from_last_modification_time(&meta);
let atime = FileTime::from_last_access_time(&meta);
assert_eq!(mtime.unix_seconds(), 1000000000);
assert_eq!(mtime.nanoseconds(), 0);
assert_eq!(atime.unix_seconds(), 1000000000);
assert_eq!(atime.nanoseconds(), 0);
}
#[test]
fn zero_file_times() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut ar = Builder::new(Vec::new());
ar.mode(HeaderMode::Deterministic);
let path = td.path().join("tmpfile");
File::create(&path).unwrap();
ar.append_path_with_name(&path, "a").unwrap();
let data = ar.into_inner().unwrap();
let mut ar = Archive::new(&data[..]);
assert!(ar.unpack(td.path()).is_ok());
let meta = fs::metadata(td.path().join("a")).unwrap();
let mtime = FileTime::from_last_modification_time(&meta);
let atime = FileTime::from_last_access_time(&meta);
assert!(mtime.unix_seconds() != 0);
assert!(atime.unix_seconds() != 0);
}
#[test]
fn backslash_treated_well() {
// Insert a file into an archive with a backslash
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut ar = Builder::new(Vec::<u8>::new());
ar.append_dir("foo\\bar", td.path()).unwrap();
let mut ar = Archive::new(Cursor::new(ar.into_inner().unwrap()));
let f = ar.entries().unwrap().next().unwrap().unwrap();
if cfg!(unix) {
assert_eq!(f.header().path().unwrap().to_str(), Some("foo\\bar"));
} else {
assert_eq!(f.header().path().unwrap().to_str(), Some("foo/bar"));
}
// Unpack an archive with a backslash in the name
let mut ar = Builder::new(Vec::<u8>::new());
let mut header = Header::new_gnu();
header.set_metadata(&fs::metadata(td.path()).unwrap());
header.set_size(0);
for (a, b) in header.as_old_mut().name.iter_mut().zip(b"foo\\bar\x00") {
*a = *b;
}
header.set_cksum();
ar.append(&header, &mut io::empty()).unwrap();
let data = ar.into_inner().unwrap();
let mut ar = Archive::new(&data[..]);
let f = ar.entries().unwrap().next().unwrap().unwrap();
assert_eq!(f.header().path().unwrap().to_str(), Some("foo\\bar"));
let mut ar = Archive::new(&data[..]);
ar.unpack(td.path()).unwrap();
assert!(fs::metadata(td.path().join("foo\\bar")).is_ok());
}
#[test]
#[cfg(unix)]
fn set_mask() {
use std::os::unix::fs::PermissionsExt;
let mut ar = tar::Builder::new(Vec::new());
let mut header = tar::Header::new_gnu();
header.set_size(0);
header.set_entry_type(tar::EntryType::Regular);
header.set_path("foo").unwrap();
header.set_mode(0o777);
header.set_cksum();
ar.append(&header, &[][..]).unwrap();
let mut header = tar::Header::new_gnu();
header.set_size(0);
header.set_entry_type(tar::EntryType::Regular);
header.set_path("bar").unwrap();
header.set_mode(0o421);
header.set_cksum();
ar.append(&header, &[][..]).unwrap();
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let bytes = ar.into_inner().unwrap();
let mut ar = tar::Archive::new(&bytes[..]);
ar.set_mask(0o211);
ar.unpack(td.path()).unwrap();
let md = fs::metadata(td.path().join("foo")).unwrap();
assert_eq!(md.permissions().mode(), 0o100566);
let md = fs::metadata(td.path().join("bar")).unwrap();
assert_eq!(md.permissions().mode(), 0o100420);
}
#[cfg(unix)]
#[test]
fn nul_bytes_in_path() {
use std::ffi::OsStr;
use std::os::unix::prelude::*;
let nul_path = OsStr::from_bytes(b"foo\0");
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut ar = Builder::new(Vec::<u8>::new());
let err = ar.append_dir(nul_path, td.path()).unwrap_err();
assert!(err.to_string().contains("contains a nul byte"));
}
#[test]
fn links() {
let mut ar = Archive::new(random_cursor_reader(tar!("link.tar")));
let mut entries = ar.entries().unwrap();
let link = entries.next().unwrap().unwrap();
assert_eq!(
link.header().link_name().unwrap().as_deref(),
Some(Path::new("file"))
);
let other = entries.next().unwrap().unwrap();
assert!(other.header().link_name().unwrap().is_none());
}
#[test]
#[cfg(unix)] // making symlinks on windows is hard
fn unpack_links() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut ar = Archive::new(random_cursor_reader(tar!("link.tar")));
ar.unpack(td.path()).unwrap();
let md = fs::symlink_metadata(td.path().join("lnk")).unwrap();
assert!(md.file_type().is_symlink());
let mtime = FileTime::from_last_modification_time(&md);
assert_eq!(mtime.unix_seconds(), 1448291033);
assert_eq!(
&*fs::read_link(td.path().join("lnk")).unwrap(),
Path::new("file")
);
File::open(td.path().join("lnk")).unwrap();
}
#[test]
fn pax_size() {
let mut ar = Archive::new(random_cursor_reader(tar!("pax_size.tar")));
let mut entries = ar.entries().unwrap();
let mut entry = entries.next().unwrap().unwrap();
let mut attributes = entry.pax_extensions().unwrap().unwrap();
let _first = attributes.next().unwrap().unwrap();
let _second = attributes.next().unwrap().unwrap();
let _third = attributes.next().unwrap().unwrap();
let fourth = attributes.next().unwrap().unwrap();
assert!(attributes.next().is_none());
assert_eq!(fourth.key(), Ok("size"));
assert_eq!(fourth.value(), Ok("4"));
assert_eq!(entry.header().size().unwrap(), 0);
assert_eq!(entry.size(), 4);
}
#[test]
fn pax_simple() {
let mut ar = Archive::new(random_cursor_reader(tar!("pax.tar")));
let mut entries = ar.entries().unwrap();
let mut first = entries.next().unwrap().unwrap();
let mut attributes = first.pax_extensions().unwrap().unwrap();
let first = attributes.next().unwrap().unwrap();
let second = attributes.next().unwrap().unwrap();
let third = attributes.next().unwrap().unwrap();
assert!(attributes.next().is_none());
assert_eq!(first.key(), Ok("mtime"));
assert_eq!(first.value(), Ok("1453146164.953123768"));
assert_eq!(second.key(), Ok("atime"));
assert_eq!(second.value(), Ok("1453251915.24892486"));
assert_eq!(third.key(), Ok("ctime"));
assert_eq!(third.value(), Ok("1453146164.953123768"));
}
#[test]
fn pax_simple_write() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let pax_path = td.path().join("pax.tar");
let file: File = File::create(&pax_path).unwrap();
let mut ar: Builder<BufWriter<File>> = Builder::new(BufWriter::new(file));
let pax_extensions = [
("arbitrary_pax_key", b"arbitrary_pax_value".as_slice()),
("SCHILY.xattr.security.selinux", b"foo_t"),
];
ar.append_pax_extensions(pax_extensions).unwrap();
ar.append_file("test2", &mut File::open(&pax_path).unwrap())
.unwrap();
ar.finish().unwrap();
drop(ar);
let mut archive_opened = Archive::new(File::open(pax_path).unwrap());
let mut entries = archive_opened.entries().unwrap();
let mut f: Entry<File> = entries.next().unwrap().unwrap();
let pax_headers = f.pax_extensions().unwrap();
assert!(pax_headers.is_some(), "pax_headers is None");
let mut pax_headers = pax_headers.unwrap();
let pax_arbitrary = pax_headers.next().unwrap().unwrap();
assert_eq!(pax_arbitrary.key(), Ok("arbitrary_pax_key"));
assert_eq!(pax_arbitrary.value(), Ok("arbitrary_pax_value"));
let xattr = pax_headers.next().unwrap().unwrap();
assert_eq!(xattr.key().unwrap(), pax_extensions[1].0);
assert_eq!(xattr.value_bytes(), pax_extensions[1].1);
assert!(entries.next().is_none());
}
#[test]
fn pax_path() {
let mut ar = Archive::new(random_cursor_reader(tar!("pax2.tar")));
let mut entries = ar.entries().unwrap();
let first = entries.next().unwrap().unwrap();
assert!(first.path().unwrap().ends_with("aaaaaaaaaaaaaaa"));
}
#[test]
fn pax_linkpath() {
let mut ar = Archive::new(random_cursor_reader(tar!("pax2.tar")));
let mut links = ar.entries().unwrap().skip(3).take(2);
let long_symlink = links.next().unwrap().unwrap();
let link_name = long_symlink.link_name().unwrap().unwrap();
assert!(link_name.to_str().unwrap().len() > 99);
assert!(link_name.ends_with("bbbbbbbbbbbbbbb"));
let long_hardlink = links.next().unwrap().unwrap();
let link_name = long_hardlink.link_name().unwrap().unwrap();
assert!(link_name.to_str().unwrap().len() > 99);
assert!(link_name.ends_with("ccccccccccccccc"));
}
#[test]
fn long_name_trailing_nul() {
let mut b = Builder::new(Vec::<u8>::new());
let mut h = Header::new_gnu();
h.set_path("././@LongLink").unwrap();
h.set_size(4);
h.set_entry_type(EntryType::new(b'L'));
h.set_cksum();
b.append(&h, "foo\0".as_bytes()).unwrap();
let mut h = Header::new_gnu();
h.set_path("bar").unwrap();
h.set_size(6);
h.set_entry_type(EntryType::file());
h.set_cksum();
b.append(&h, "foobar".as_bytes()).unwrap();
let contents = b.into_inner().unwrap();
let mut a = Archive::new(&contents[..]);
let e = a.entries().unwrap().next().unwrap().unwrap();
assert_eq!(&*e.path_bytes(), b"foo");
}
#[test]
fn long_linkname_trailing_nul() {
let mut b = Builder::new(Vec::<u8>::new());
let mut h = Header::new_gnu();
h.set_path("././@LongLink").unwrap();
h.set_size(4);
h.set_entry_type(EntryType::new(b'K'));
h.set_cksum();
b.append(&h, "foo\0".as_bytes()).unwrap();
let mut h = Header::new_gnu();
h.set_path("bar").unwrap();
h.set_size(6);
h.set_entry_type(EntryType::file());
h.set_cksum();
b.append(&h, "foobar".as_bytes()).unwrap();
let contents = b.into_inner().unwrap();
let mut a = Archive::new(&contents[..]);
let e = a.entries().unwrap().next().unwrap().unwrap();
assert_eq!(&*e.link_name_bytes().unwrap(), b"foo");
}
#[test]
fn long_linkname_gnu() {
for t in [tar::EntryType::Symlink, tar::EntryType::Link] {
let mut b = Builder::new(Vec::<u8>::new());
let mut h = Header::new_gnu();
h.set_entry_type(t);
h.set_size(0);
let path = "usr/lib/.build-id/05/159ed904e45ff5100f7acd3d3b99fa7e27e34f";
let target = "../../../../usr/lib64/qt5/plugins/wayland-graphics-integration-server/libqt-wayland-compositor-xcomposite-egl.so";
b.append_link(&mut h, path, target).unwrap();
let contents = b.into_inner().unwrap();
let mut a = Archive::new(&contents[..]);
let e = &a.entries().unwrap().next().unwrap().unwrap();
assert_eq!(e.header().entry_type(), t);
assert_eq!(e.path().unwrap().to_str().unwrap(), path);
assert_eq!(e.link_name().unwrap().unwrap().to_str().unwrap(), target);
}
}
#[test]
fn linkname_literal() {
for t in [tar::EntryType::Symlink, tar::EntryType::Link] {
let mut b = Builder::new(Vec::<u8>::new());
let mut h = Header::new_gnu();
h.set_entry_type(t);
h.set_size(0);
let path = "usr/lib/systemd/systemd-sysv-install";
let target = "../../..//sbin/chkconfig";
h.set_link_name_literal(target).unwrap();
b.append_data(&mut h, path, std::io::empty()).unwrap();
let contents = b.into_inner().unwrap();
let mut a = Archive::new(&contents[..]);
let e = &a.entries().unwrap().next().unwrap().unwrap();
assert_eq!(e.header().entry_type(), t);
assert_eq!(e.path().unwrap().to_str().unwrap(), path);
assert_eq!(e.link_name().unwrap().unwrap().to_str().unwrap(), target);
}
}
#[test]
fn append_writer() {
let mut b = Builder::new(Cursor::new(Vec::new()));
let mut h = Header::new_gnu();
h.set_uid(42);
let mut writer = b.append_writer(&mut h, "file1").unwrap();
writer.write_all(b"foo").unwrap();
writer.write_all(b"barbaz").unwrap();
writer.finish().unwrap();
let mut h = Header::new_gnu();
h.set_uid(43);
let long_path: PathBuf = repeat("abcd").take(50).collect();
let mut writer = b.append_writer(&mut h, &long_path).unwrap();
let long_data = repeat(b'x').take(513).collect::<Vec<u8>>();
writer.write_all(&long_data).unwrap();
writer.finish().unwrap();
let contents = b.into_inner().unwrap().into_inner();
let mut ar = Archive::new(&contents[..]);
let mut entries = ar.entries().unwrap();
let e = &mut entries.next().unwrap().unwrap();
assert_eq!(e.header().uid().unwrap(), 42);
assert_eq!(&*e.path_bytes(), b"file1");
let mut r = Vec::new();
e.read_to_end(&mut r).unwrap();
assert_eq!(&r[..], b"foobarbaz");
let e = &mut entries.next().unwrap().unwrap();
assert_eq!(e.header().uid().unwrap(), 43);
assert_eq!(e.path().unwrap(), long_path.as_path());
let mut r = Vec::new();
e.read_to_end(&mut r).unwrap();
assert_eq!(r.len(), 513);
assert!(r.iter().all(|b| *b == b'x'));
}
#[test]
fn encoded_long_name_has_trailing_nul() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let path = td.path().join("foo");
File::create(&path).unwrap().write_all(b"test").unwrap();
let mut b = Builder::new(Vec::<u8>::new());
let long = "abcd".repeat(200);
b.append_file(&long, &mut File::open(&path).unwrap())
.unwrap();
let contents = b.into_inner().unwrap();
let mut a = Archive::new(&contents[..]);
let mut e = a.entries().unwrap().raw(true).next().unwrap().unwrap();
let mut name = Vec::new();
e.read_to_end(&mut name).unwrap();
assert_eq!(name[name.len() - 1], 0);
let header_name = &e.header().as_gnu().unwrap().name;
assert!(header_name.starts_with(b"././@LongLink\x00"));
}
#[test]
fn reading_sparse() {
let rdr = random_cursor_reader(tar!("sparse.tar"));
let mut ar = Archive::new(rdr);
let mut entries = ar.entries().unwrap();
let mut a = entries.next().unwrap().unwrap();
let mut s = String::new();
assert_eq!(&*a.header().path_bytes(), b"sparse_begin.txt");
a.read_to_string(&mut s).unwrap();
assert_eq!(&s[..5], "test\n");
assert!(s[5..].chars().all(|x| x == '\u{0}'));
let mut a = entries.next().unwrap().unwrap();
let mut s = String::new();
assert_eq!(&*a.header().path_bytes(), b"sparse_end.txt");
a.read_to_string(&mut s).unwrap();
assert!(s[..s.len() - 9].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[s.len() - 9..], "test_end\n");
let mut a = entries.next().unwrap().unwrap();
let mut s = String::new();
assert_eq!(&*a.header().path_bytes(), b"sparse_ext.txt");
a.read_to_string(&mut s).unwrap();
assert!(s[..0x1000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x1000..0x1000 + 5], "text\n");
assert!(s[0x1000 + 5..0x3000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x3000..0x3000 + 5], "text\n");
assert!(s[0x3000 + 5..0x5000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x5000..0x5000 + 5], "text\n");
assert!(s[0x5000 + 5..0x7000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x7000..0x7000 + 5], "text\n");
assert!(s[0x7000 + 5..0x9000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x9000..0x9000 + 5], "text\n");
assert!(s[0x9000 + 5..0xb000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0xb000..0xb000 + 5], "text\n");
let mut a = entries.next().unwrap().unwrap();
let mut s = String::new();
assert_eq!(&*a.header().path_bytes(), b"sparse.txt");
a.read_to_string(&mut s).unwrap();
assert!(s[..0x1000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x1000..0x1000 + 6], "hello\n");
assert!(s[0x1000 + 6..0x2fa0].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x2fa0..0x2fa0 + 6], "world\n");
assert!(s[0x2fa0 + 6..0x4000].chars().all(|x| x == '\u{0}'));
assert!(entries.next().is_none());
}
#[test]
fn extract_sparse() {
let rdr = random_cursor_reader(tar!("sparse.tar"));
let mut ar = Archive::new(rdr);
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
ar.unpack(td.path()).unwrap();
let mut s = String::new();
File::open(td.path().join("sparse_begin.txt"))
.unwrap()
.read_to_string(&mut s)
.unwrap();
assert_eq!(&s[..5], "test\n");
assert!(s[5..].chars().all(|x| x == '\u{0}'));
s.truncate(0);
File::open(td.path().join("sparse_end.txt"))
.unwrap()
.read_to_string(&mut s)
.unwrap();
assert!(s[..s.len() - 9].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[s.len() - 9..], "test_end\n");
s.truncate(0);
File::open(td.path().join("sparse_ext.txt"))
.unwrap()
.read_to_string(&mut s)
.unwrap();
assert!(s[..0x1000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x1000..0x1000 + 5], "text\n");
assert!(s[0x1000 + 5..0x3000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x3000..0x3000 + 5], "text\n");
assert!(s[0x3000 + 5..0x5000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x5000..0x5000 + 5], "text\n");
assert!(s[0x5000 + 5..0x7000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x7000..0x7000 + 5], "text\n");
assert!(s[0x7000 + 5..0x9000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x9000..0x9000 + 5], "text\n");
assert!(s[0x9000 + 5..0xb000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0xb000..0xb000 + 5], "text\n");
s.truncate(0);
File::open(td.path().join("sparse.txt"))
.unwrap()
.read_to_string(&mut s)
.unwrap();
assert!(s[..0x1000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x1000..0x1000 + 6], "hello\n");
assert!(s[0x1000 + 6..0x2fa0].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x2fa0..0x2fa0 + 6], "world\n");
assert!(s[0x2fa0 + 6..0x4000].chars().all(|x| x == '\u{0}'));
}
#[test]
fn large_sparse() {
let rdr = random_cursor_reader(tar!("sparse-large.tar"));
let mut ar = Archive::new(rdr);
let mut entries = ar.entries().unwrap();
// Only check the header info without extracting, as the file is very large,
// and not all filesystems support sparse files.
let a = entries.next().unwrap().unwrap();
let h = a.header().as_gnu().unwrap();
assert_eq!(h.real_size().unwrap(), 12626929280);
}
#[test]
fn sparse_with_trailing() {
let rdr = random_cursor_reader(tar!("sparse-1.tar"));
let mut ar = Archive::new(rdr);
let mut entries = ar.entries().unwrap();
let mut a = entries.next().unwrap().unwrap();
let mut s = String::new();
a.read_to_string(&mut s).unwrap();
assert_eq!(0x100_00c, s.len());
assert_eq!(&s[..0xc], "0MB through\n");
assert!(s[0xc..0x100_000].chars().all(|x| x == '\u{0}'));
assert_eq!(&s[0x100_000..], "1MB through\n");
}
#[test]
#[allow(clippy::option_map_unit_fn)]
fn writing_sparse() {
let mut ar = Builder::new(Vec::new());
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut files = Vec::new();
let mut append_file = |name: &str, chunks: &[(u64, u64)]| {
let path = td.path().join(name);
let mut file = File::create(&path).unwrap();
file.set_len(
chunks
.iter()
.map(|&(off, len)| off + len)
.max()
.unwrap_or(0),
)
.unwrap();
for (i, &(off, len)) in chunks.iter().enumerate() {
file.seek(io::SeekFrom::Start(off)).unwrap();
let mut data = vec![i as u8 + b'a'; len as usize];
data.first_mut().map(|x| *x = b'[');
data.last_mut().map(|x| *x = b']');
file.write_all(&data).unwrap();
}
ar.append_path_with_name(&path, path.file_name().unwrap())
.unwrap();
files.push(path);
};
append_file("empty", &[]);
append_file("full_sparse", &[(0x20_000, 0)]);
append_file("_x", &[(0x20_000, 0x1_000)]);
append_file("x_", &[(0, 0x1_000), (0x20_000, 0)]);
append_file("_x_x", &[(0x20_000, 0x1_000), (0x40_000, 0x1_000)]);
append_file("x_x_", &[(0, 0x1_000), (0x20_000, 0x1_000), (0x40_000, 0)]);
append_file("uneven", &[(0x20_333, 0x555), (0x40_777, 0x999)]);
ar.finish().unwrap();
let data = ar.into_inner().unwrap();
// Without sparse support, the size of the tarball exceed 1MiB.
#[cfg(target_os = "linux")]
assert!(data.len() <= 37 * 1024); // ext4 (defaults to 4k block size)
#[cfg(target_os = "freebsd")]
assert!(data.len() <= 273 * 1024); // UFS (defaults to 32k block size, last block isn't a hole)
let mut ar = Archive::new(&data[..]);
let mut entries = ar.entries().unwrap();
for path in files {
let mut f = entries.next().unwrap().unwrap();
let mut s = String::new();
f.read_to_string(&mut s).unwrap();
let expected = fs::read_to_string(&path).unwrap();
assert!(s == expected, "path: {path:?}");
}
assert!(entries.next().is_none());
}
#[test]
fn path_separators() {
let mut ar = Builder::new(Vec::new());
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let path = td.path().join("test");
File::create(&path).unwrap().write_all(b"test").unwrap();
let short_path: PathBuf = repeat("abcd").take(2).collect();
let long_path: PathBuf = repeat("abcd").take(50).collect();
// Make sure UStar headers normalize to Unix path separators
let mut header = Header::new_ustar();
header.set_path(&short_path).unwrap();
assert_eq!(header.path().unwrap(), short_path);
assert!(!header.path_bytes().contains(&b'\\'));
header.set_path(&long_path).unwrap();
assert_eq!(header.path().unwrap(), long_path);
assert!(!header.path_bytes().contains(&b'\\'));
// Make sure GNU headers normalize to Unix path separators,
// including the `@LongLink` fallback used by `append_file`.
ar.append_file(&short_path, &mut File::open(&path).unwrap())
.unwrap();
ar.append_file(&long_path, &mut File::open(&path).unwrap())
.unwrap();
let rd = Cursor::new(ar.into_inner().unwrap());
let mut ar = Archive::new(rd);
let mut entries = ar.entries().unwrap();
let entry = entries.next().unwrap().unwrap();
assert_eq!(entry.path().unwrap(), short_path);
assert!(!entry.path_bytes().contains(&b'\\'));
let entry = entries.next().unwrap().unwrap();
assert_eq!(entry.path().unwrap(), long_path);
assert!(!entry.path_bytes().contains(&b'\\'));
assert!(entries.next().is_none());
}
#[test]
#[cfg(unix)]
fn append_path_symlink() {
use std::borrow::Cow;
use std::env;
use std::os::unix::fs::symlink;
let mut ar = Builder::new(Vec::new());
ar.follow_symlinks(false);
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let long_linkname = "abcd".repeat(30);
let long_pathname = "dcba".repeat(30);
env::set_current_dir(td.path()).unwrap();
// "short" path name / short link name
symlink("testdest", "test").unwrap();
ar.append_path("test").unwrap();
// short path name / long link name
symlink(&long_linkname, "test2").unwrap();
ar.append_path("test2").unwrap();
// long path name / long link name
symlink(&long_linkname, &long_pathname).unwrap();
ar.append_path(&long_pathname).unwrap();
let rd = Cursor::new(ar.into_inner().unwrap());
let mut ar = Archive::new(rd);
let mut entries = ar.entries().unwrap();
let entry = entries.next().unwrap().unwrap();
assert_eq!(entry.path().unwrap(), Path::new("test"));
assert_eq!(
entry.link_name().unwrap(),
Some(Cow::from(Path::new("testdest")))
);
assert_eq!(entry.header().size().unwrap(), 0);
let entry = entries.next().unwrap().unwrap();
assert_eq!(entry.path().unwrap(), Path::new("test2"));
assert_eq!(
entry.link_name().unwrap(),
Some(Cow::from(Path::new(&long_linkname)))
);
assert_eq!(entry.header().size().unwrap(), 0);
let entry = entries.next().unwrap().unwrap();
assert_eq!(entry.path().unwrap(), Path::new(&long_pathname));
assert_eq!(
entry.link_name().unwrap(),
Some(Cow::from(Path::new(&long_linkname)))
);
assert_eq!(entry.header().size().unwrap(), 0);
assert!(entries.next().is_none());
}
#[test]
fn name_with_slash_doesnt_fool_long_link_and_bsd_compat() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut ar = Builder::new(Vec::new());
let mut h = Header::new_gnu();
h.set_path("././@LongLink").unwrap();
h.set_size(4);
h.set_entry_type(EntryType::new(b'L'));
h.set_cksum();
ar.append(&h, "foo\0".as_bytes()).unwrap();
let mut header = Header::new_gnu();
header.set_entry_type(EntryType::Regular);
header.set_path("testdir/").unwrap();
header.set_size(0);
header.set_cksum();
ar.append(&header, &mut io::empty()).unwrap();
// Extracting
let rdr = Cursor::new(ar.into_inner().unwrap());
let mut ar = Archive::new(rdr);
ar.unpack(td.path()).unwrap();
// Iterating
let rdr = Cursor::new(ar.into_inner().into_inner());
let mut ar = Archive::new(rdr);
assert!(ar.entries().unwrap().all(|fr| fr.is_ok()));
assert!(td.path().join("foo").is_file());
}
#[test]
fn insert_local_file_different_name() {
let mut ar = Builder::new(Vec::new());
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let path = td.path().join("directory");
fs::create_dir(&path).unwrap();
ar.append_path_with_name(&path, "archive/dir").unwrap();
let path = td.path().join("file");
File::create(&path).unwrap().write_all(b"test").unwrap();
ar.append_path_with_name(&path, "archive/dir/f").unwrap();
let rd = Cursor::new(ar.into_inner().unwrap());
let mut ar = Archive::new(rd);
let mut entries = ar.entries().unwrap();
let entry = entries.next().unwrap().unwrap();
assert_eq!(entry.path().unwrap(), Path::new("archive/dir"));
let entry = entries.next().unwrap().unwrap();
assert_eq!(entry.path().unwrap(), Path::new("archive/dir/f"));
assert!(entries.next().is_none());
}
#[test]
#[cfg(unix)]
fn tar_directory_containing_symlink_to_directory() {
use std::os::unix::fs::symlink;
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let dummy_src = TempBuilder::new().prefix("dummy_src").tempdir().unwrap();
let dummy_dst = td.path().join("dummy_dst");
let mut ar = Builder::new(Vec::new());
symlink(dummy_src.path().display().to_string(), &dummy_dst).unwrap();
assert!(dummy_dst.read_link().is_ok());
assert!(dummy_dst.read_link().unwrap().is_dir());
ar.append_dir_all("symlinks", td.path()).unwrap();
ar.finish().unwrap();
}
#[test]
fn long_path() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let rdr = random_cursor_reader(tar!("7z_long_path.tar"));
let mut ar = Archive::new(rdr);
assert!(ar.unpack(td.path()).is_ok());
}
#[test]
fn unpack_path_larger_than_windows_max_path() {
let dir_name = "iamaprettylongnameandtobepreciseiam91characterslongwhichsomethinkisreallylongandothersdonot";
// 183 character directory name
let really_long_path = format!("{}{}", dir_name, dir_name);
let td = TempBuilder::new()
.prefix(&really_long_path)
.tempdir()
.unwrap();
// directory in 7z_long_path.tar is over 100 chars
let rdr = random_cursor_reader(tar!("7z_long_path.tar"));
let mut ar = Archive::new(rdr);
// should unpack path greater than windows MAX_PATH length of 260 characters
assert!(ar.unpack(td.path()).is_ok());
}
#[test]
fn append_long_multibyte() {
let mut x = tar::Builder::new(Vec::new());
let mut name = String::new();
let data: &[u8] = &[];
for _ in 0..512 {
name.push('a');
name.push('𑢮');
x.append_data(&mut Header::new_gnu(), &name, data).unwrap();
name.pop();
}
}
#[test]
fn read_only_directory_containing_files() {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut b = Builder::new(Vec::<u8>::new());
let mut h = Header::new_gnu();
h.set_path("dir/").unwrap();
h.set_size(0);
h.set_entry_type(EntryType::dir());
h.set_mode(0o444);
h.set_cksum();
b.append(&h, "".as_bytes()).unwrap();
let mut h = Header::new_gnu();
h.set_path("dir/file").unwrap();
h.set_size(2);
h.set_entry_type(EntryType::file());
h.set_cksum();
b.append(&h, "hi".as_bytes()).unwrap();
let contents = b.into_inner().unwrap();
let mut ar = Archive::new(&contents[..]);
assert!(ar.unpack(td.path()).is_ok());
}
// This test was marked linux only due to macOS CI can't handle `set_current_dir` correctly
#[test]
#[cfg(target_os = "linux")]
fn tar_directory_containing_special_files() {
use std::env;
use std::ffi::CString;
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let fifo = td.path().join("fifo");
unsafe {
let fifo_path = CString::new(fifo.to_str().unwrap()).unwrap();
let ret = libc::mknod(fifo_path.as_ptr(), libc::S_IFIFO | 0o644, 0);
if ret != 0 {
libc::perror(fifo_path.as_ptr());
panic!("Failed to create a FIFO file");
}
}
env::set_current_dir(td.path()).unwrap();
let mut ar = Builder::new(Vec::new());
// append_path has a different logic for processing files, so we need to test it as well
ar.append_path("fifo").unwrap();
ar.append_dir_all("special", td.path()).unwrap();
env::set_current_dir("/dev/").unwrap();
// CI systems seem to have issues with creating a chr device
ar.append_path("null").unwrap();
ar.finish().unwrap();
}
#[test]
fn header_size_overflow() {
// maximal file size doesn't overflow anything
let mut ar = Builder::new(Vec::new());
let mut header = Header::new_gnu();
header.set_size(u64::MAX);
header.set_cksum();
ar.append(&header, "x".as_bytes()).unwrap();
let result = ar.into_inner().unwrap();
let mut ar = Archive::new(&result[..]);
let mut e = ar.entries().unwrap();
let err = e.next().unwrap().err().unwrap();
assert!(
err.to_string().contains("size overflow"),
"bad error: {}",
err
);
// back-to-back entries that would overflow also don't panic
let mut ar = Builder::new(Vec::new());
let mut header = Header::new_gnu();
header.set_size(1_000);
header.set_cksum();
ar.append(&header, &[0u8; 1_000][..]).unwrap();
let mut header = Header::new_gnu();
header.set_size(u64::MAX - 513);
header.set_cksum();
ar.append(&header, "x".as_bytes()).unwrap();
let result = ar.into_inner().unwrap();
let mut ar = Archive::new(&result[..]);
let mut e = ar.entries().unwrap();
e.next().unwrap().unwrap();
let err = e.next().unwrap().err().unwrap();
assert!(
err.to_string().contains("size overflow"),
"bad error: {}",
err
);
}
#[test]
#[cfg(unix)]
fn ownership_preserving() {
use std::os::unix::prelude::*;
let mut rdr = Vec::new();
let mut ar = Builder::new(&mut rdr);
let data: &[u8] = &[];
let mut header = Header::new_gnu();
// file 1 with uid = 580800000, gid = 580800000
header.set_gid(580800000);
header.set_uid(580800000);
header.set_path("iamuid580800000").unwrap();
header.set_size(0);
header.set_cksum();
ar.append(&header, data).unwrap();
// file 2 with uid = 580800001, gid = 580800000
header.set_uid(580800001);
header.set_path("iamuid580800001").unwrap();
header.set_cksum();
ar.append(&header, data).unwrap();
// file 3 with uid = 580800002, gid = 580800002
header.set_gid(580800002);
header.set_uid(580800002);
header.set_path("iamuid580800002").unwrap();
header.set_cksum();
ar.append(&header, data).unwrap();
// directory 1 with uid = 580800002, gid = 580800002
header.set_entry_type(EntryType::Directory);
header.set_gid(580800002);
header.set_uid(580800002);
header.set_path("iamuid580800002dir").unwrap();
header.set_cksum();
ar.append(&header, data).unwrap();
// symlink to file 1
header.set_entry_type(EntryType::Symlink);
header.set_gid(580800002);
header.set_uid(580800002);
header.set_path("iamuid580800000symlink").unwrap();
header.set_cksum();
ar.append_link(&mut header, "iamuid580800000symlink", "iamuid580800000")
.unwrap();
ar.finish().unwrap();
let rdr = Cursor::new(ar.into_inner().unwrap());
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let mut ar = Archive::new(rdr);
ar.set_preserve_ownerships(true);
if unsafe { libc::getuid() } == 0 {
ar.unpack(td.path()).unwrap();
// validate against premade files
// iamuid580800001 has this ownership: 580800001:580800000
let meta = std::fs::symlink_metadata(td.path().join("iamuid580800000")).unwrap();
assert_eq!(meta.uid(), 580800000);
assert_eq!(meta.gid(), 580800000);
let meta = std::fs::symlink_metadata(td.path().join("iamuid580800001")).unwrap();
assert_eq!(meta.uid(), 580800001);
assert_eq!(meta.gid(), 580800000);
let meta = std::fs::symlink_metadata(td.path().join("iamuid580800002")).unwrap();
assert_eq!(meta.uid(), 580800002);
assert_eq!(meta.gid(), 580800002);
let meta = std::fs::symlink_metadata(td.path().join("iamuid580800002dir")).unwrap();
assert_eq!(meta.uid(), 580800002);
assert_eq!(meta.gid(), 580800002);
let meta = std::fs::symlink_metadata(td.path().join("iamuid580800000symlink")).unwrap();
assert_eq!(meta.uid(), 580800002);
assert_eq!(meta.gid(), 580800002)
} else {
// it's not possible to unpack tar while preserving ownership
// without root permissions
assert!(ar.unpack(td.path()).is_err());
}
}
#[test]
#[cfg(unix)]
fn pax_and_gnu_uid_gid() {
let tarlist = [tar!("biguid_gnu.tar"), tar!("biguid_pax.tar")];
for file in &tarlist {
let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap();
let rdr = random_cursor_reader(file);
let mut ar = Archive::new(rdr);
ar.set_preserve_ownerships(true);
if unsafe { libc::getuid() } == 0 {
ar.unpack(td.path()).unwrap();
let meta = fs::metadata(td.path().join("test.txt")).unwrap();
let uid = std::os::unix::prelude::MetadataExt::uid(&meta);
let gid = std::os::unix::prelude::MetadataExt::gid(&meta);
// 4294967294 = u32::MAX - 1
assert_eq!(uid, 4294967294);
assert_eq!(gid, 4294967294);
} else {
// it's not possible to unpack tar while preserving ownership
// without root permissions
assert!(ar.unpack(td.path()).is_err());
}
}
}
#[test]
fn append_data_error_does_not_corrupt_subsequent_entries() {
// When append_data fails (e.g., path contains ".."), subsequent
// successful writes must not be corrupted by an orphaned GNU
// long-name extension entry left in the stream.
let mut ar = Builder::new(Vec::new());
// First write: a long path (>100 bytes to trigger GNU long-name extension)
// containing ".." not as the last component, which will fail validation
// in set_truncated_path_for_gnu_header.
let dotdot_path = "a/../b/".to_string() + &"c".repeat(100);
let mut header = Header::new_gnu();
header.set_size(5);
header.set_cksum();
let result = ar.append_data(&mut header, &dotdot_path, &b"first"[..]);
assert!(result.is_err());
// Second write: a clean path that should succeed normally.
let mut header = Header::new_gnu();
header.set_size(6);
header.set_cksum();
ar.append_data(&mut header, "clean.txt", &b"second"[..])
.unwrap();
// Verify: the archive should contain exactly one entry at "clean.txt"
// with content "second". Before the fix, it contained an entry at the
// dotdot path with content "second" — the orphaned long-name stole the data.
let data = ar.into_inner().unwrap();
let mut archive = Archive::new(&data[..]);
let entries: Vec<_> = archive
.entries()
.unwrap()
.collect::<Result<_, _>>()
.unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].path().unwrap().to_str().unwrap(), "clean.txt");
}
/// Build the PAX size smuggling archive described in the original report.
///
/// A PAX extended header declares `size=2048` for a regular file whose
/// actual header size field is 8. A symlink entry is hidden inside the
/// inflated region. A correct parser honours the PAX size and skips over
/// the symlink; a buggy one reads only the header size and exposes it.
fn build_pax_smuggle_archive() -> Vec<u8> {
const B: usize = 512;
const INFLATED: usize = 2048;
let end_of_archive = || std::iter::repeat(0u8).take(B * 2);
let mut ar: Vec<u8> = Vec::new();
// PAX extended header declaring size=2048 for the next entry.
let pax_rec = format!("13 size={INFLATED}\n");
let mut pax_hdr = Header::new_ustar();
pax_hdr.set_path("./PaxHeaders/regular").unwrap();
pax_hdr.set_size(pax_rec.as_bytes().len() as u64);
pax_hdr.set_entry_type(EntryType::XHeader);
pax_hdr.set_cksum();
ar.extend_from_slice(pax_hdr.as_bytes());
ar.extend_from_slice(pax_rec.as_bytes());
ar.resize(ar.len().next_multiple_of(B), 0);
// Regular file whose header says size=8, but PAX says 2048.
let content = b"regular\n";
let mut file_hdr = Header::new_ustar();
file_hdr.set_path("regular.txt").unwrap();
file_hdr.set_size(content.len() as u64);
file_hdr.set_entry_type(EntryType::Regular);
file_hdr.set_cksum();
ar.extend_from_slice(file_hdr.as_bytes());
let mark = ar.len();
ar.extend_from_slice(content);
ar.resize(ar.len().next_multiple_of(B), 0);
// Smuggled symlink hidden in the inflated region.
let mut sym_hdr = Header::new_ustar();
sym_hdr.set_path("smuggled").unwrap();
sym_hdr.set_size(0);
sym_hdr.set_entry_type(EntryType::Symlink);
sym_hdr.set_link_name("/etc/shadow").unwrap();
sym_hdr.set_cksum();
ar.extend_from_slice(sym_hdr.as_bytes());
ar.extend(end_of_archive());
// Pad to fill the inflated window.
let used = ar.len() - mark;
let pad = INFLATED.saturating_sub(used);
ar.extend(std::iter::repeat(0u8).take(pad.next_multiple_of(B)));
// End-of-archive.
ar.extend(end_of_archive());
ar
}
/// Regression test for PAX size smuggling.
///
/// A crafted archive uses a PAX extended header to declare a file size (2048)
/// larger than the header's octal size field (8). Before the fix, `tar-rs`
/// only applied the PAX size override when the header size was 0, so it would
/// read the small header size, advance too little, and expose a symlink entry
/// hidden in the "padding" area. After the fix, the PAX size unconditionally
/// overrides the header size, causing the parser to skip over the smuggled
/// symlink — matching the behavior of compliant parsers.
#[test]
fn pax_size_smuggled_symlink() {
let data = build_pax_smuggle_archive();
let mut archive = Archive::new(random_cursor_reader(&data[..]));
let entries: Vec<_> = archive
.entries()
.unwrap()
.map(|e| {
let e = e.unwrap();
let path = e.path().unwrap().to_path_buf();
let kind = e.header().entry_type();
let link = e.link_name().unwrap().map(|l| l.to_path_buf());
(path, kind, link)
})
.collect();
// With the fix applied, only "regular.txt" should be visible.
// The smuggled symlink must NOT appear.
let expected: Vec<(PathBuf, EntryType, Option<PathBuf>)> =
vec![(PathBuf::from("regular.txt"), EntryType::Regular, None)];
assert_eq!(
entries, expected,
"smuggled symlink visible or unexpected entries\n\
got: {entries:?}"
);
}
/// Cross-validate that `tar` and `astral-tokio-tar` parse the PAX size
/// smuggling archive identically, guarding against parsing differentials.
#[tokio::test]
async fn pax_size_smuggle_matches_astral_tokio_tar() {
use tokio_stream::StreamExt;
let data = build_pax_smuggle_archive();
// Parse with sync tar.
let sync_entries: Vec<_> = {
let mut ar = Archive::new(&data[..]);
ar.entries()
.unwrap()
.map(|e| {
let e = e.unwrap();
let path = e.path().unwrap().to_path_buf();
let kind = e.header().entry_type();
let link = e.link_name().unwrap().map(|l| l.to_path_buf());
(path, kind, link)
})
.collect()
};
// Parse with async astral-tokio-tar.
let async_entries: Vec<_> = {
let mut ar = tokio_tar::Archive::new(&data[..]);
let mut entries = ar.entries().unwrap();
let mut result = Vec::new();
while let Some(e) = entries.next().await {
let e = e.unwrap();
let entry_type = e.header().entry_type();
result.push((
e.path().unwrap().to_path_buf(),
// Map through the raw byte so the two crates' EntryTypes compare.
EntryType::new(entry_type.as_byte()),
e.link_name().unwrap().map(|l| l.to_path_buf()),
));
}
result
};
// Assert exact expected content for both parsers independently,
// so we verify correctness — not just mutual agreement.
let expected: Vec<(PathBuf, EntryType, Option<PathBuf>)> =
vec![(PathBuf::from("regular.txt"), EntryType::Regular, None)];
assert_eq!(
sync_entries, expected,
"tar-rs produced unexpected entries (smuggled symlink visible?)\n\
got: {sync_entries:?}"
);
assert_eq!(
async_entries, expected,
"astral-tokio-tar produced unexpected entries (smuggled symlink visible?)\n\
got: {async_entries:?}"
);
}