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 { inner: R, rng: SmallRng, } impl RandomReader { fn new(inner: R) -> Self { RandomReader { inner, rng: SmallRng::seed_from_u64(0), } } } impl Read for RandomReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { if buf.is_empty() { return self.inner.read(buf); } // Take the min of two uniform samples to bias toward small reads. let a = self.rng.gen_range(1..=buf.len()); let b = self.rng.gen_range(1..=buf.len()); self.inner.read(&mut buf[..a.min(b)]) } } /// Convenience: wrap a byte slice in a RandomReader>. /// /// The RNG is seeded from a hash of the data, so different archives /// exercise different read-size sequences while remaining deterministic. fn random_cursor_reader>(data: D) -> RandomReader> { RandomReader::new(Cursor::new(data)) } macro_rules! tar { ($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 = 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(ar: &mut Archive) -> Vec 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(mut entries: Entries) { 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 { inner: R, read_bytes: u64, } impl LoggingReader { fn new(reader: R) -> LoggingReader { LoggingReader { inner: reader, read_bytes: 0, } } } impl Read for LoggingReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { self.inner.read(buf).inspect(|&i| { self.read_bytes += i as u64; }) } } impl Seek for LoggingReader { fn seek(&mut self, pos: io::SeekFrom) -> io::Result { 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::::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::::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::::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> = 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 = 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::::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::::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::::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::::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::>(); 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::::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::::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::>() .unwrap(); assert_eq!(entries.len(), 1); assert_eq!(entries[0].path().unwrap().to_str().unwrap(), "clean.txt"); } /// Build the PAX size smuggling archive described in the original report. /// /// A PAX extended header declares `size=2048` for a regular file whose /// actual header size field is 8. A symlink entry is hidden inside the /// inflated region. A correct parser honours the PAX size and skips over /// the symlink; a buggy one reads only the header size and exposes it. fn build_pax_smuggle_archive() -> Vec { const B: usize = 512; const INFLATED: usize = 2048; let end_of_archive = || std::iter::repeat(0u8).take(B * 2); let mut ar: Vec = Vec::new(); // PAX extended header declaring size=2048 for the next entry. let pax_rec = format!("13 size={INFLATED}\n"); let mut pax_hdr = Header::new_ustar(); pax_hdr.set_path("./PaxHeaders/regular").unwrap(); pax_hdr.set_size(pax_rec.as_bytes().len() as u64); pax_hdr.set_entry_type(EntryType::XHeader); pax_hdr.set_cksum(); ar.extend_from_slice(pax_hdr.as_bytes()); ar.extend_from_slice(pax_rec.as_bytes()); ar.resize(ar.len().next_multiple_of(B), 0); // Regular file whose header says size=8, but PAX says 2048. let content = b"regular\n"; let mut file_hdr = Header::new_ustar(); file_hdr.set_path("regular.txt").unwrap(); file_hdr.set_size(content.len() as u64); file_hdr.set_entry_type(EntryType::Regular); file_hdr.set_cksum(); ar.extend_from_slice(file_hdr.as_bytes()); let mark = ar.len(); ar.extend_from_slice(content); ar.resize(ar.len().next_multiple_of(B), 0); // Smuggled symlink hidden in the inflated region. let mut sym_hdr = Header::new_ustar(); sym_hdr.set_path("smuggled").unwrap(); sym_hdr.set_size(0); sym_hdr.set_entry_type(EntryType::Symlink); sym_hdr.set_link_name("/etc/shadow").unwrap(); sym_hdr.set_cksum(); ar.extend_from_slice(sym_hdr.as_bytes()); ar.extend(end_of_archive()); // Pad to fill the inflated window. let used = ar.len() - mark; let pad = INFLATED.saturating_sub(used); ar.extend(std::iter::repeat(0u8).take(pad.next_multiple_of(B))); // End-of-archive. ar.extend(end_of_archive()); ar } /// Regression test for PAX size smuggling. /// /// A crafted archive uses a PAX extended header to declare a file size (2048) /// larger than the header's octal size field (8). Before the fix, `tar-rs` /// only applied the PAX size override when the header size was 0, so it would /// read the small header size, advance too little, and expose a symlink entry /// hidden in the "padding" area. After the fix, the PAX size unconditionally /// overrides the header size, causing the parser to skip over the smuggled /// symlink — matching the behavior of compliant parsers. #[test] fn pax_size_smuggled_symlink() { let data = build_pax_smuggle_archive(); let mut archive = Archive::new(random_cursor_reader(&data[..])); let entries: Vec<_> = archive .entries() .unwrap() .map(|e| { let e = e.unwrap(); let path = e.path().unwrap().to_path_buf(); let kind = e.header().entry_type(); let link = e.link_name().unwrap().map(|l| l.to_path_buf()); (path, kind, link) }) .collect(); // With the fix applied, only "regular.txt" should be visible. // The smuggled symlink must NOT appear. let expected: Vec<(PathBuf, EntryType, Option)> = vec![(PathBuf::from("regular.txt"), EntryType::Regular, None)]; assert_eq!( entries, expected, "smuggled symlink visible or unexpected entries\n\ got: {entries:?}" ); } /// Cross-validate that `tar` and `astral-tokio-tar` parse the PAX size /// smuggling archive identically, guarding against parsing differentials. #[tokio::test] async fn pax_size_smuggle_matches_astral_tokio_tar() { use tokio_stream::StreamExt; let data = build_pax_smuggle_archive(); // Parse with sync tar. let sync_entries: Vec<_> = { let mut ar = Archive::new(&data[..]); ar.entries() .unwrap() .map(|e| { let e = e.unwrap(); let path = e.path().unwrap().to_path_buf(); let kind = e.header().entry_type(); let link = e.link_name().unwrap().map(|l| l.to_path_buf()); (path, kind, link) }) .collect() }; // Parse with async astral-tokio-tar. let async_entries: Vec<_> = { let mut ar = tokio_tar::Archive::new(&data[..]); let mut entries = ar.entries().unwrap(); let mut result = Vec::new(); while let Some(e) = entries.next().await { let e = e.unwrap(); let entry_type = e.header().entry_type(); result.push(( e.path().unwrap().to_path_buf(), // Map through the raw byte so the two crates' EntryTypes compare. EntryType::new(entry_type.as_byte()), e.link_name().unwrap().map(|l| l.to_path_buf()), )); } result }; // Assert exact expected content for both parsers independently, // so we verify correctness — not just mutual agreement. let expected: Vec<(PathBuf, EntryType, Option)> = vec![(PathBuf::from("regular.txt"), EntryType::Regular, None)]; assert_eq!( sync_entries, expected, "tar-rs produced unexpected entries (smuggled symlink visible?)\n\ got: {sync_entries:?}" ); assert_eq!( async_entries, expected, "astral-tokio-tar produced unexpected entries (smuggled symlink visible?)\n\ got: {async_entries:?}" ); }