···77use std::path::Path;
8899/// Sum sizes of all regular files reachable from the given paths, recursing
1010-/// into directories. Best-effort: anything we can't stat (permission denied,
1111-/// racy deletion) is counted as zero; the bar may finish short rather than
1212-/// fail the run.
1010+/// into directories. Uses `fs::metadata` (follows symlinks) so that the
1111+/// file-vs-directory judgment matches `Path::is_file` / `Path::is_dir`
1212+/// semantics used by each backend's walker — otherwise a symlink to a file
1313+/// would contribute 0 to the total while the walker reads (and bar-ticks)
1414+/// the full target. Best-effort: anything we can't stat (permission denied,
1515+/// racy deletion, broken symlink) is counted as zero; the bar may finish
1616+/// short rather than fail the run.
1317pub fn total_input_bytes<P: AsRef<Path>>(paths: &[P]) -> u64 {
1418 paths.iter().map(|p| sum_path(p.as_ref())).sum()
1519}
16201721fn sum_path(path: &Path) -> u64 {
1818- let Ok(meta) = std::fs::symlink_metadata(path) else {
2222+ let Ok(meta) = std::fs::metadata(path) else {
1923 return 0;
2024 };
2125 if meta.is_file() {
···5761 total_input_bytes(&[std::path::PathBuf::from("/nope/xx")]),
5862 0
5963 );
6464+ }
6565+6666+ /// Symlinks to regular files must contribute their target's size so the
6767+ /// bar total matches what the walkers actually read. Regression for
6868+ /// tar/zip/7z bars overshooting past 100% on directories containing
6969+ /// symlinks.
7070+ #[cfg(unix)]
7171+ #[test]
7272+ fn follows_symlink_to_file() {
7373+ let dir = assert_fs::TempDir::new().unwrap();
7474+ dir.child("target.txt").write_str("abcdefghij").unwrap();
7575+ std::os::unix::fs::symlink(dir.path().join("target.txt"), dir.path().join("link.txt"))
7676+ .unwrap();
7777+ // target.txt (10) + link.txt following to target (10) = 20
7878+ assert_eq!(total_input_bytes(&[dir.path().to_path_buf()]), 20);
6079 }
6180}