From 2f9cfc200f5afd0d946fb7b113b178f3be7dab87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 8 Oct 2025 18:11:30 +0200 Subject: [PATCH 1/2] Deduplicate append_path_with_name()'s [l]stat() error handler --- src/builder.rs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index f2d29464..b2e9e7b4 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -606,20 +606,16 @@ fn append_path_with_name( options: BuilderOptions, ) -> io::Result<()> { let stat = if options.follow { - fs::metadata(path).map_err(|err| { - io::Error::new( - err.kind(), - format!("{} when getting metadata for {}", err, path.display()), - ) - })? + fs::metadata(path) } else { - fs::symlink_metadata(path).map_err(|err| { - io::Error::new( - err.kind(), - format!("{} when getting metadata for {}", err, path.display()), - ) - })? - }; + fs::symlink_metadata(path) + } + .map_err(|err| { + io::Error::new( + err.kind(), + format!("{} when getting metadata for {}", err, path.display()), + ) + })?; let ar_name = name.unwrap_or(path); if stat.is_file() { append_file(dst, ar_name, &mut fs::File::open(path)?, options) From d07bc3e83d7390a454ae492a426ffb3c7f8e49cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BD=D0=B0=D0=B1?= Date: Wed, 8 Oct 2025 18:27:25 +0200 Subject: [PATCH 2/2] Don't follow symlinks in append_dir_all() if follow_symlinks(false) Before (append_dir_all("file")): called `Result::unwrap()` on an `Err` value: Os { code: 20, kind: NotADirectory, message: "Not a directory" } called `Result::unwrap()` on an `Err` value: Os { code: 267, kind: NotADirectory, message: "The directory name is invalid." } after (append_dir_all("file") or append_dir_all("symlink"): called `Result::unwrap()` on an `Err` value: Custom { kind: NotADirectory, error: "append_dir_all() argument not a directory" } Repro with fn main() { let mut tar = tar::Builder::new(std::io::stdout().lock()); tar.follow_symlinks(false); tar.append_dir_all("", std::env::args_os().nth(1).unwrap()) .unwrap(); tar.finish().unwrap(); } before: $ ls -lnd src symlink Cargo.toml -rw-r--r-- 1 1000 100 118 10-08 18:31 Cargo.toml drwxr-xr-x 2 1000 100 60 10-08 17:57 src lrwxrwxrwx 1 1000 100 3 10-08 18:30 symlink -> src $ cargo run -- Cargo.toml | tar -tv called `Result::unwrap()` on an `Err` value: Os { code: 20, kind: NotADirectory, message: "Not a directory" } $ cargo run -- src | tar -tv -rw-r--r-- 1000/100 211 2025-10-08 18:29 main.rs $ cargo run -- symlink | tar -tv -rw-r--r-- 1000/100 211 2025-10-08 18:29 main.rs after: $ cargo run -- Cargo.toml | tar -tv called `Result::unwrap()` on an `Err` value: Custom { kind: NotADirectory, error: "append_dir_all() argument not a directory" } $ cargo run -- src | tar -tv -rw-r--r-- 1000/100 211 2025-10-08 18:29 main.rs $ cargo run -- symlink | tar -tv called `Result::unwrap()` on an `Err` value: Custom { kind: NotADirectory, error: "append_dir_all() argument not a directory" } This also achieves parity with tar(1) --- src/builder.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/builder.rs b/src/builder.rs index b2e9e7b4..abdf2442 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -870,7 +870,18 @@ fn append_dir_all( src_path: &Path, options: BuilderOptions, ) -> io::Result<()> { - let mut stack = vec![(src_path.to_path_buf(), true, false)]; + let src_metadata = if options.follow { + fs::metadata(src_path) + } else { + fs::symlink_metadata(src_path) + }?; + if !src_metadata.is_dir() { + return Err(io::Error::new( + io::ErrorKind::NotADirectory, + "append_dir_all() argument not a directory", + )); + } + let mut stack = vec![(src_path.to_path_buf(), true, src_metadata.is_symlink())]; while let Some((src, is_dir, is_symlink)) = stack.pop() { let dest = path.join(src.strip_prefix(src_path).unwrap()); // In case of a symlink pointing to a directory, is_dir is false, but src.is_dir() will return true