diff --git a/src/archive/tar.rs b/src/archive/tar.rs index d5a61fe..e581af5 100644 --- a/src/archive/tar.rs +++ b/src/archive/tar.rs @@ -30,8 +30,26 @@ pub fn unpack_archive(reader: Box, output_folder: &Path, quiet: bool) let mut files_unpacked = 0; for file in archive.entries()? { let mut file = file?; + let entry_type = file.header().entry_type(); + let relative_path = file.path()?.to_path_buf(); + let full_path = output_folder.join(&relative_path); - file.unpack_in(output_folder)?; + match entry_type { + tar::EntryType::Symlink => { + let target = file + .link_name()? + .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidData, "Missing symlink target"))?; + + #[cfg(unix)] + std::os::unix::fs::symlink(&target, &full_path)?; + #[cfg(windows)] + std::os::windows::symlink_file(&target, &full_path)?; + } + tar::EntryType::Regular | tar::EntryType::Directory => { + file.unpack_in(output_folder)?; + } + _ => continue, + } // This is printed for every file in the archive and has little // importance for most users, but would generate lots of @@ -134,23 +152,11 @@ where header.set_entry_type(tar::EntryType::Symlink); header.set_size(0); - let entry_name = path.to_str().ok_or_else(|| { - FinalError::with_title("Tar requires that all directories names are valid UTF-8") - .detail(format!("File at '{path:?}' has a non-UTF-8 name")) + builder.append_link(&mut header, path, &target_path).map_err(|err| { + FinalError::with_title("Could not create archive") + .detail("Unexpected error while trying to read link") + .detail(format!("Error: {err}.")) })?; - - let target_name = target_path.to_str().ok_or_else(|| { - FinalError::with_title("Tar requires that all directories names are valid UTF-8") - .detail(format!("File at '{target_path:?}' has a non-UTF-8 name")) - })?; - - builder - .append_link(&mut header, entry_name, target_name) - .map_err(|err| { - FinalError::with_title("Could not create archive") - .detail("Unexpected error while trying to read link") - .detail(format!("Error: {err}.")) - })?; } else { let mut file = match fs::File::open(path) { Ok(f) => f, diff --git a/src/cli/mod.rs b/src/cli/mod.rs index ce6b5d5..185fae6 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -54,7 +54,7 @@ fn canonicalize_files(files: &[impl AsRef]) -> io::Result> { files .iter() .map(|f| { - if is_path_stdin(f.as_ref()) { + if is_path_stdin(f.as_ref()) || f.as_ref().is_symlink() { Ok(f.as_ref().to_path_buf()) } else { fs::canonicalize(f) diff --git a/tests/integration.rs b/tests/integration.rs index 7a5e549..f6a93c2 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -1,7 +1,11 @@ #[macro_use] mod utils; -use std::{io::Write, iter::once, path::PathBuf}; +use std::{ + io::Write, + iter::once, + path::{Path, PathBuf}, +}; use fs_err as fs; use parse_display::Display; @@ -467,3 +471,59 @@ fn unpack_rar_stdin() -> Result<(), Box> { Ok(()) } + +#[test] +fn tar_symlink_pack_and_unpack() { + let temp_dir = tempdir().unwrap(); + let root_path = temp_dir.path(); + + let src_files_path = root_path.join("src_files"); + fs::create_dir_all(&src_files_path).unwrap(); + + let mut files_path = ["file1.txt", "file2.txt", "file3.txt", "file4.txt", "file5.txt"] + .into_iter() + .map(|f| src_files_path.join(f)) + .map(|path| { + let mut file = fs::File::create(&path).unwrap(); + file.write_all("Some content".as_bytes()).unwrap(); + path + }) + .collect::>(); + + let dest_files_path = root_path.join("dest_files"); + fs::create_dir_all(&dest_files_path).unwrap(); + + let symlink_path = src_files_path.join(Path::new("symlink")); + #[cfg(unix)] + std::os::unix::fs::symlink(&files_path[0], &symlink_path).unwrap(); + #[cfg(windows)] + std::os::windows::symlink_file(&files_path[0], &symlink_path).unwrap(); + + files_path.push(symlink_path); + + let archive = &root_path.join("archive.tar"); + + crate::utils::cargo_bin() + .arg("compress") + .args(files_path) + .arg(archive) + .assert() + .success(); + + crate::utils::cargo_bin() + .arg("decompress") + .arg(archive) + .arg("-d") + .arg(&dest_files_path) + .assert() + .success(); + + assert_same_directory(&src_files_path, &dest_files_path, false); + // check the symlink stand still + for f in dest_files_path.as_path().read_dir().unwrap() { + let f = f.unwrap(); + if f.file_name() == "symlink" { + assert!(f.file_type().unwrap().is_symlink()) + } + } +} diff --git a/tests/utils.rs b/tests/utils.rs index 3648cef..4d2a84f 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -88,7 +88,7 @@ pub fn assert_same_directory(x: impl Into, y: impl Into, prese if ft_x.is_dir() && ft_y.is_dir() { assert_same_directory(x.path(), y.path(), preserve_permissions); - } else if ft_x.is_file() && ft_y.is_file() { + } else if (ft_x.is_file() && ft_y.is_file()) || (ft_x.is_symlink() && ft_y.is_symlink()) { assert_eq!(meta_x.len(), meta_y.len()); assert_eq!(fs::read(x.path()).unwrap(), fs::read(y.path()).unwrap()); } else {