tar: resolve symlink resilience and path normalization issues#323
tar: resolve symlink resilience and path normalization issues#323kaladron wants to merge 1 commit into
Conversation
kaladron
commented
Jul 11, 2026
- Use symlink_metadata instead of exists/is_dir in create_archive, get_tree, and print_verbose_tree to avoid incorrectly following symlinks.
- Disable following symlinks in tar Builder by default.
- Implement clean_path to fix path normalization for paths containing parent directory components (..).
- Add integration tests for symlinks, nested symlinks, and path normalization.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #323 +/- ##
==========================================
+ Coverage 96.84% 97.93% +1.08%
==========================================
Files 11 15 +4
Lines 1492 1982 +490
Branches 29 42 +13
==========================================
+ Hits 1445 1941 +496
+ Misses 46 40 -6
Partials 1 1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
628947a to
a34fc9e
Compare
- Use symlink_metadata instead of exists/is_dir in create_archive, get_tree, and print_verbose_tree to avoid incorrectly following symlinks. - Disable following symlinks in tar Builder by default. - Implement clean_path to fix path normalization for paths containing parent directory components (..). - Add integration tests for symlinks, nested symlinks, and path normalization.
| let mut normalized = cleaned.clone(); | ||
| let mut prefix_removed = PathBuf::new(); | ||
| let mut changed = cleaned != path; | ||
|
|
||
| if !allow_absolute { | ||
| let mut remaining = PathBuf::new(); | ||
| let mut stripped_any = false; | ||
| let mut in_leading = true; |
There was a problem hiding this comment.
6 mut in 7 lines, it is usually a sign that it should be refactored
| let metadata = match path.symlink_metadata() { | ||
| Ok(m) => m, | ||
| Err(e) if e.kind() == std::io::ErrorKind::NotFound => { | ||
| return Err(TarError::FileNotFound { | ||
| path: path.to_path_buf(), | ||
| } | ||
| .into()); | ||
| } | ||
| .into()); | ||
| } | ||
| Err(e) => return Err(TarError::Io(e).into()), | ||
| }; |
There was a problem hiding this comment.
most rust like:
| let metadata = match path.symlink_metadata() { | |
| Ok(m) => m, | |
| Err(e) if e.kind() == std::io::ErrorKind::NotFound => { | |
| return Err(TarError::FileNotFound { | |
| path: path.to_path_buf(), | |
| } | |
| .into()); | |
| } | |
| .into()); | |
| } | |
| Err(e) => return Err(TarError::Io(e).into()), | |
| }; | |
| let metadata = path.symlink_metadata().map_err(|e| { | |
| if e.kind() == std::io::ErrorKind::NotFound { | |
| TarError::FileNotFound { | |
| path: path.to_path_buf(), | |
| } | |
| } else { | |
| TarError::Io(e) | |
| } | |
| })?; |
| fn print_verbose_tree(status_output: &mut impl Write, path: &Path) -> UResult<()> { | ||
| let to_print = get_tree(path)? | ||
| .iter() | ||
| .map(|p| (p.is_dir(), p.display().to_string())) | ||
| .map(|(is_dir, path)| { | ||
| if is_dir { | ||
| format!("{}{}", path, path::MAIN_SEPARATOR) | ||
| } else { | ||
| path | ||
| } | ||
| }) | ||
| .collect::<Vec<_>>() | ||
| .join("\n"); | ||
| writeln!(status_output, "{to_print}").map_err(TarError::Io)?; | ||
| for (p, is_dir) in get_tree(path)? { | ||
| if is_dir { | ||
| writeln!(status_output, "{}{}", p.display(), path::MAIN_SEPARATOR) | ||
| .map_err(TarError::Io)?; | ||
| } else { | ||
| writeln!(status_output, "{}", p.display()).map_err(TarError::Io)?; | ||
| } | ||
| } | ||
| Ok(()) | ||
| } |
There was a problem hiding this comment.
what about ?
fn print_verbose_tree(status_output: &mut impl Write, path: &Path) -> UResult<()> {
for (p, is_dir) in get_tree(path)? {
let suffix = if is_dir {
path::MAIN_SEPARATOR_STR
} else {
""
};
writeln!(status_output, "{}{suffix}", p.display()).map_err(TarError::Io)?;
}
Ok(())
}