diff --git a/src/builder.rs b/src/builder.rs index 88164c88..959e3447 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -23,6 +23,7 @@ pub struct Builder { #[derive(Clone, Copy)] struct BuilderOptions { mode: HeaderMode, + preserve_absolute: bool, follow: bool, sparse: bool, } @@ -35,6 +36,7 @@ impl Builder { Builder { options: BuilderOptions { mode: HeaderMode::Complete, + preserve_absolute: false, follow: true, sparse: true, }, @@ -50,6 +52,11 @@ impl Builder { self.options.mode = mode; } + /// Peserve absolute path while creating an archive + pub fn preserve_absolute(&mut self, preserve: bool) { + self.options.preserve_absolute = preserve; + } + /// Follow symlinks, archiving the contents of the file they point to rather /// than adding a symlink to the archive. Defaults to true. /// @@ -179,7 +186,8 @@ impl Builder { path: P, data: R, ) -> io::Result<()> { - prepare_header_path(self.get_mut(), header, path.as_ref())?; + let allow_absolute = self.options.preserve_absolute; + prepare_header_path(self.get_mut(), header, path.as_ref(), allow_absolute)?; header.set_cksum(); self.append(header, data) } @@ -222,7 +230,8 @@ impl Builder { where W: Seek, { - EntryWriter::start(self.get_mut(), header, path.as_ref()) + let allow_absolute = self.options.preserve_absolute; + EntryWriter::start(self.get_mut(), header, path.as_ref(), allow_absolute) } /// Adds a new link (symbolic or hard) entry to this archive with the specified path and target. @@ -267,7 +276,8 @@ impl Builder { } fn _append_link(&mut self, header: &mut Header, path: &Path, target: &Path) -> io::Result<()> { - prepare_header_path(self.get_mut(), header, path)?; + let allow_abolute = self.options.preserve_absolute; + prepare_header_path(self.get_mut(), header, path, allow_abolute)?; prepare_header_link(self.get_mut(), header, target)?; header.set_cksum(); self.append(header, std::io::empty()) @@ -515,8 +525,9 @@ impl EntryWriter<'_> { obj: &'a mut dyn SeekWrite, header: &'a mut Header, path: &Path, + allow_absolute: bool, ) -> io::Result> { - prepare_header_path(obj.as_write(), header, path)?; + prepare_header_path(obj.as_write(), header, path, allow_absolute)?; // Reserve space for header, will be overwritten once data is written. obj.write_all([0u8; BLOCK_SIZE as usize].as_ref())?; @@ -624,14 +635,28 @@ fn append_path_with_name( if stat.is_file() { append_file(dst, ar_name, &mut fs::File::open(path)?, options) } else if stat.is_dir() { - append_fs(dst, ar_name, &stat, options.mode, None) + append_fs( + dst, + ar_name, + &stat, + options.mode, + options.preserve_absolute, + None, + ) } else if stat.file_type().is_symlink() { let link_name = fs::read_link(path)?; - append_fs(dst, ar_name, &stat, options.mode, Some(&link_name)) + append_fs( + dst, + ar_name, + &stat, + options.mode, + options.preserve_absolute, + Some(&link_name), + ) } else { #[cfg(unix)] { - append_special(dst, path, &stat, options.mode) + append_special(dst, path, &stat, options.mode, options.preserve_absolute) } #[cfg(not(unix))] { @@ -646,6 +671,7 @@ fn append_special( path: &Path, stat: &fs::Metadata, mode: HeaderMode, + allow_absolute: bool, ) -> io::Result<()> { use ::std::os::unix::fs::{FileTypeExt, MetadataExt}; @@ -669,7 +695,7 @@ fn append_special( let mut header = Header::new_gnu(); header.set_metadata_in_mode(stat, mode); - prepare_header_path(dst, &mut header, path)?; + prepare_header_path(dst, &mut header, path, allow_absolute)?; header.set_entry_type(entry_type); let dev_id = stat.rdev(); @@ -693,7 +719,7 @@ fn append_file( let stat = file.metadata()?; let mut header = Header::new_gnu(); - prepare_header_path(dst, &mut header, path)?; + prepare_header_path(dst, &mut header, path, options.preserve_absolute)?; header.set_metadata_in_mode(&stat, options.mode); let sparse_entries = if options.sparse { prepare_header_sparse(file, &stat, &mut header)? @@ -725,7 +751,14 @@ fn append_dir( options: BuilderOptions, ) -> io::Result<()> { let stat = fs::metadata(src_path)?; - append_fs(dst, path, &stat, options.mode, None) + append_fs( + dst, + path, + &stat, + options.mode, + options.preserve_absolute, + None, + ) } fn prepare_header(size: u64, entry_type: u8) -> Header { @@ -743,12 +776,23 @@ fn prepare_header(size: u64, entry_type: u8) -> Header { header } -fn prepare_header_path(dst: &mut dyn Write, header: &mut Header, path: &Path) -> io::Result<()> { +fn prepare_header_path( + dst: &mut dyn Write, + header: &mut Header, + path: &Path, + allow_absolute: bool, +) -> io::Result<()> { // Try to encode the path directly in the header, but if it ends up not // working (probably because it's too long) then try to use the GNU-specific // long name extension by emitting an entry which indicates that it's the // filename. - if let Err(e) = header.set_path(path) { + let result = if allow_absolute { + header.set_path_absolute(path) + } else { + header.set_path(path) + }; + + if let Err(e) = result { let data = path2bytes(path)?; let max = header.as_old().name.len(); // Since `e` isn't specific enough to let us know the path is indeed too @@ -769,7 +813,7 @@ fn prepare_header_path(dst: &mut dyn Write, header: &mut Header, path: &Path) -> Ok(s) => s, Err(e) => str::from_utf8(&data[..e.valid_up_to()]).unwrap(), }; - header.set_truncated_path_for_gnu_header(truncated)?; + header.set_truncated_path_for_gnu_header(truncated, allow_absolute)?; let header2 = prepare_header(data.len() as u64, b'L'); // null-terminated string @@ -859,11 +903,12 @@ fn append_fs( path: &Path, meta: &fs::Metadata, mode: HeaderMode, + allow_absolute: bool, link_name: Option<&Path>, ) -> io::Result<()> { let mut header = Header::new_gnu(); - prepare_header_path(dst, &mut header, path)?; + prepare_header_path(dst, &mut header, path, allow_absolute)?; header.set_metadata_in_mode(meta, mode); if let Some(link_name) = link_name { prepare_header_link(dst, &mut header, link_name)?; @@ -894,13 +939,20 @@ fn append_dir_all( } else if !options.follow && is_symlink { let stat = fs::symlink_metadata(&src)?; let link_name = fs::read_link(&src)?; - append_fs(dst, &dest, &stat, options.mode, Some(&link_name))?; + append_fs( + dst, + &dest, + &stat, + options.mode, + options.preserve_absolute, + Some(&link_name), + )?; } else { #[cfg(unix)] { let stat = fs::metadata(&src)?; if !stat.is_file() { - append_special(dst, &dest, &stat, options.mode)?; + append_special(dst, &dest, &stat, options.mode, options.preserve_absolute)?; continue; } } diff --git a/src/header.rs b/src/header.rs index 8413d10f..9ba32d4b 100644 --- a/src/header.rs +++ b/src/header.rs @@ -387,7 +387,14 @@ impl Header { /// use `Builder` methods to insert a long-name extension at the same time /// as the file content. pub fn set_path>(&mut self, p: P) -> io::Result<()> { - self.set_path_inner(p.as_ref(), false) + self.set_path_inner(p.as_ref(), false, false) + } + + /// Sets the path name for this header. + /// + /// Same as set_path but allows abosolut paths + pub fn set_path_absolute>(&mut self, p: P) -> io::Result<()> { + self.set_path_inner(p.as_ref(), false, true) } // Sets the truncated path for GNU header @@ -396,18 +403,28 @@ impl Header { pub(crate) fn set_truncated_path_for_gnu_header>( &mut self, p: P, + allow_absolute: bool, ) -> io::Result<()> { - self.set_path_inner(p.as_ref(), true) + self.set_path_inner(p.as_ref(), true, allow_absolute) } - fn set_path_inner(&mut self, path: &Path, is_truncated_gnu_long_path: bool) -> io::Result<()> { + fn set_path_inner( + &mut self, + path: &Path, + is_truncated_gnu_long_path: bool, + allow_absolute: bool, + ) -> io::Result<()> { if let Some(ustar) = self.as_ustar_mut() { - return ustar.set_path(path); + return if allow_absolute { + ustar.set_path_absolute(path) + } else { + ustar.set_path(path) + }; } if is_truncated_gnu_long_path { - copy_path_into_gnu_long(&mut self.as_old_mut().name, path, false) + copy_path_into_gnu_long(&mut self.as_old_mut().name, path, false, allow_absolute) } else { - copy_path_into(&mut self.as_old_mut().name, path, false) + copy_path_into(&mut self.as_old_mut().name, path, false, allow_absolute) } .map_err(|err| { io::Error::new( @@ -461,7 +478,7 @@ impl Header { } fn _set_link_name(&mut self, path: &Path) -> io::Result<()> { - copy_path_into(&mut self.as_old_mut().linkname, path, true).map_err(|err| { + copy_path_into(&mut self.as_old_mut().linkname, path, true, true).map_err(|err| { io::Error::new( err.kind(), format!("{} when setting link name for {}", err, self.path_lossy()), @@ -994,10 +1011,15 @@ impl UstarHeader { /// See `Header::set_path` pub fn set_path>(&mut self, p: P) -> io::Result<()> { - self._set_path(p.as_ref()) + self._set_path(p.as_ref(), false) } - fn _set_path(&mut self, path: &Path) -> io::Result<()> { + /// See `Header::set_path_absolute` + pub fn set_path_absolute>(&mut self, p: P) -> io::Result<()> { + self._set_path(p.as_ref(), true) + } + + fn _set_path(&mut self, path: &Path, allow_absolute: bool) -> io::Result<()> { // This can probably be optimized quite a bit more, but for now just do // something that's relatively easy and readable. // @@ -1009,7 +1031,7 @@ impl UstarHeader { let bytes = path2bytes(path)?; let (maxnamelen, maxprefixlen) = (self.name.len(), self.prefix.len()); if bytes.len() <= maxnamelen { - copy_path_into(&mut self.name, path, false).map_err(|err| { + copy_path_into(&mut self.name, path, false, allow_absolute).map_err(|err| { io::Error::new( err.kind(), format!("{} when setting path for {}", err, self.path_lossy()), @@ -1033,14 +1055,14 @@ impl UstarHeader { break; } } - copy_path_into(&mut self.prefix, prefix, false).map_err(|err| { + copy_path_into(&mut self.prefix, prefix, false, allow_absolute).map_err(|err| { io::Error::new( err.kind(), format!("{} when setting path for {}", err, self.path_lossy()), ) })?; let path = bytes2path(Cow::Borrowed(&bytes[prefixlen + 1..]))?; - copy_path_into(&mut self.name, &path, false).map_err(|err| { + copy_path_into(&mut self.name, &path, false, allow_absolute).map_err(|err| { io::Error::new( err.kind(), format!("{} when setting path for {}", err, self.path_lossy()), @@ -1555,17 +1577,18 @@ fn copy_path_into_inner( path: &Path, is_link_name: bool, is_truncated_gnu_long_path: bool, + allow_absolute: bool, ) -> io::Result<()> { let mut emitted = false; let mut needs_slash = false; let mut iter = path.components().peekable(); while let Some(component) = iter.next() { let bytes = path2bytes(Path::new(component.as_os_str()))?; - match (component, is_link_name) { - (Component::Prefix(..), false) | (Component::RootDir, false) => { + match (component, is_link_name, allow_absolute) { + (Component::Prefix(..), false, false) | (Component::RootDir, false, false) => { return Err(other("paths in archives must be relative")); } - (Component::ParentDir, false) => { + (Component::ParentDir, false, _) => { // If it's last component of a gnu long path we know that there might be more // to the component than .. (the rest is stored elsewhere) // Otherwise it's a clear error @@ -1574,9 +1597,12 @@ fn copy_path_into_inner( } } // Allow "./" as the path - (Component::CurDir, false) if path.components().count() == 1 => {} - (Component::CurDir, false) => continue, - (Component::Normal(_), _) | (_, true) => {} + (Component::CurDir, false, _) if path.components().count() == 1 => {} + (Component::CurDir, false, _) => continue, + (Component::Normal(_), _, _) + | (_, true, _) + | (Component::Prefix(_), false, true) + | (Component::RootDir, false, true) => {} }; if needs_slash { copy(&mut slot, b"/")?; @@ -1616,8 +1642,13 @@ fn copy_path_into_inner( /// * a nul byte was found /// * an invalid path component is encountered (e.g. a root path or parent dir) /// * the path itself is empty -fn copy_path_into(slot: &mut [u8], path: &Path, is_link_name: bool) -> io::Result<()> { - copy_path_into_inner(slot, path, is_link_name, false) +fn copy_path_into( + slot: &mut [u8], + path: &Path, + is_link_name: bool, + allow_absolute: bool, +) -> io::Result<()> { + copy_path_into_inner(slot, path, is_link_name, false, allow_absolute) } /// Copies `path` into the `slot` provided @@ -1630,8 +1661,13 @@ fn copy_path_into(slot: &mut [u8], path: &Path, is_link_name: bool) -> io::Resul /// * the path itself is empty /// /// This is less restrictive version meant to be used for truncated GNU paths. -fn copy_path_into_gnu_long(slot: &mut [u8], path: &Path, is_link_name: bool) -> io::Result<()> { - copy_path_into_inner(slot, path, is_link_name, true) +fn copy_path_into_gnu_long( + slot: &mut [u8], + path: &Path, + is_link_name: bool, + allow_absolute: bool, +) -> io::Result<()> { + copy_path_into_inner(slot, path, is_link_name, true, allow_absolute) } #[cfg(target_arch = "wasm32")] diff --git a/tests/all.rs b/tests/all.rs index c2685746..6aa6d959 100644 --- a/tests/all.rs +++ b/tests/all.rs @@ -8,7 +8,8 @@ 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 std::path::{Component, Path, PathBuf}; +use std::process::Command; use filetime::FileTime; use rand::rngs::SmallRng; @@ -499,6 +500,83 @@ fn writing_and_extracting_directories() { check_dirtree(&td); } +#[test] +fn writing_files_absolute_path_fail() { + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); + let mut ar = Builder::new(Vec::new()); + + let td_abs_path = td.path().to_path_buf(); + if let Err(res) = ar.append_dir(&td_abs_path, &td_abs_path) { + assert!(res + .to_string() + .contains("paths in archives must be relative when setting path for")); + return; + } + + panic!("Expected error"); +} + +#[test] +fn writing_files_absolute_path_succeed() { + let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); + + let mut ar = Builder::new(Vec::new()); + ar.preserve_absolute(true); + + let td_abs_path = td.path().to_path_buf(); + ar.append_dir(&td_abs_path, &td_abs_path).unwrap(); + ar.finish().unwrap(); + + let rdr = Cursor::new(ar.into_inner().unwrap()); + let mut ar = Archive::new(rdr); + let actual: Vec = ar + .entries() + .unwrap() + .map(|e| e.unwrap().path().unwrap().into_owned()) + .collect(); + + assert_eq!(actual, vec![td_abs_path]); +} + +#[test] +fn extract_absolute_path_gnu_tar() { + let td_abs_path = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); + + let test_file = td_abs_path.path().join("tmpfile"); + File::create(&test_file) + .unwrap() + .write_all(b"content") + .unwrap(); + + let test_arr = td_abs_path.path().join("arr.tar"); + + Command::new("tar") + .args([ + "-cf", + &test_arr.display().to_string(), + "-P", + &test_file.display().to_string(), + ]) + .status() + .expect("Failed to create an archive via GNU tar"); + + assert!(fs::metadata(&test_arr).is_ok()); + + fs::remove_file(&test_file).unwrap(); + assert!(fs::metadata(&test_file).is_err()); + + let mut ar = Archive::new(File::open(&test_arr).unwrap()); + ar.unpack(&td_abs_path).unwrap(); + + let unpacked_path = td_abs_path.path().join( + test_file + .components() + .skip_while(|c| matches!(c, Component::RootDir | Component::Prefix(_))) + .collect::(), + ); + assert!(fs::metadata(&unpacked_path).is_ok()); +} + #[test] fn writing_and_extracting_directories_complex_permissions() { let td = TempBuilder::new().prefix("tar-rs").tempdir().unwrap(); diff --git a/tests/header/mod.rs b/tests/header/mod.rs index dbbed991..1884a7d8 100644 --- a/tests/header/mod.rs +++ b/tests/header/mod.rs @@ -57,6 +57,9 @@ fn link_name() { assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("foo\\bar")); assert!(h.set_link_name("\0").is_err()); + + h.set_link_name("/foo").unwrap(); + assert_eq!(h.link_name().unwrap().unwrap().to_str(), Some("/foo")); } #[test] @@ -157,6 +160,10 @@ fn set_path() { assert!(h.set_path("..").is_err()); assert!(h.set_path("foo/..").is_err()); assert!(h.set_path("foo/../bar").is_err()); + assert!(h.set_path("/foo").is_err()); + + assert!(h.set_path_absolute("/foo").is_ok()); + assert_eq!(h.path().unwrap().to_str(), Some("/foo")); h = Header::new_ustar(); h.set_path("foo").unwrap();