From f6c281ee3b8d56c8b74e85ae81be78ce55314921 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Fri, 13 Feb 2026 00:57:36 -0800 Subject: [PATCH 1/4] fix: broken symlinks with empty targets treated as directories When a symlink has an empty target (created via `ln -s "" name`), `link_target()` would join the empty path with the parent directory, resolving to the parent directory itself. This caused `points_to_directory()` to return true for these broken symlinks, making them sort alongside real directories when using `--group-directories-first`. Fix by checking for empty symlink targets immediately after read_link and treating them as broken before path resolution occurs. Fixes #1715 --- src/fs/file.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/fs/file.rs b/src/fs/file.rs index cd972e322..d0671f5ba 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -454,6 +454,14 @@ impl<'dir> File<'dir> { Err(e) => return FileTarget::Err(e), }; + // A symlink with an empty target is always broken. We must check + // this before calling reorient_target_path, because joining an + // empty path with the parent directory would resolve to the parent + // itself, incorrectly treating the broken symlink as a directory. + if path.as_os_str().is_empty() { + return FileTarget::Broken(path); + } + let absolute_path = self.reorient_target_path(&path); // Use plain `metadata` instead of `symlink_metadata` - we *want* to @@ -1117,3 +1125,85 @@ mod filename_test { assert_eq!("/", File::filename(Path::new("/"))); } } + +#[cfg(test)] +#[cfg(unix)] +mod broken_symlink_test { + use super::*; + use std::os::unix::fs as unix_fs; + + fn make_file(path: PathBuf) -> File<'static> { + File::from_args(path, None, None, false, false, None) + } + + /// A symlink with an empty target should be treated as broken, not as + /// pointing to a directory. Regression test for + /// https://github.com/eza-community/eza/issues/1715 + #[test] + fn empty_target_symlink_is_not_directory() { + let test_dir = std::env::temp_dir().join("eza_test_empty_symlink"); + let _ = std::fs::remove_dir_all(&test_dir); + std::fs::create_dir_all(&test_dir).unwrap(); + + let link_path = test_dir.join("empty-link"); + unix_fs::symlink("", &link_path).unwrap(); + + let file = make_file(link_path); + + assert!(file.is_link(), "should be recognized as a symlink"); + assert!(!file.is_directory(), "should not be recognized as a directory"); + assert!( + !file.points_to_directory(), + "broken symlink with empty target should not point to a directory" + ); + + let target = file.link_target(); + assert!( + target.is_broken(), + "symlink with empty target should be considered broken" + ); + + let _ = std::fs::remove_dir_all(&test_dir); + } + + /// A symlink whose target has been deleted should not be treated as a + /// directory either. + #[test] + fn deleted_target_symlink_is_not_directory() { + let test_dir = std::env::temp_dir().join("eza_test_deleted_symlink"); + let _ = std::fs::remove_dir_all(&test_dir); + std::fs::create_dir_all(&test_dir).unwrap(); + + let target_dir = test_dir.join("target_dir"); + std::fs::create_dir(&target_dir).unwrap(); + + let link_path = test_dir.join("dir-link"); + unix_fs::symlink("target_dir", &link_path).unwrap(); + + // Verify it initially points to a directory + let file = make_file(link_path.clone()); + assert!( + file.points_to_directory(), + "should point to directory before deletion" + ); + + // Delete the target directory + std::fs::remove_dir(&target_dir).unwrap(); + + // Re-create File to clear cached state + let file = make_file(link_path); + assert!(file.is_link(), "should still be recognized as a symlink"); + assert!( + !file.points_to_directory(), + "broken symlink (deleted target) should not point to a directory" + ); + + let target = file.link_target(); + assert!( + target.is_broken(), + "symlink with deleted target should be considered broken" + ); + + let _ = std::fs::remove_dir_all(&test_dir); + } +} From b6b334b1c16f2da7d65952d368eebff005aae062 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Fri, 13 Feb 2026 19:35:59 -0800 Subject: [PATCH 2/4] style: fix cargo fmt formatting in broken symlink test --- src/fs/file.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/fs/file.rs b/src/fs/file.rs index d0671f5ba..a813bc09d 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -1151,7 +1151,10 @@ mod broken_symlink_test { let file = make_file(link_path); assert!(file.is_link(), "should be recognized as a symlink"); - assert!(!file.is_directory(), "should not be recognized as a directory"); + assert!( + !file.is_directory(), + "should not be recognized as a directory" + ); assert!( !file.points_to_directory(), "broken symlink with empty target should not point to a directory" From dd9dd3332e922f23cbc72cccdd1b4065c08ffadf Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Fri, 13 Feb 2026 22:58:43 -0800 Subject: [PATCH 3/4] fix(test): skip empty symlink test when OS doesn't support it Some environments (like the Nix build sandbox) don't allow creating symlinks with empty string targets. Gracefully skip the test in those cases instead of panicking. --- src/fs/file.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/fs/file.rs b/src/fs/file.rs index a813bc09d..cd29fec7a 100644 --- a/src/fs/file.rs +++ b/src/fs/file.rs @@ -1146,7 +1146,12 @@ mod broken_symlink_test { std::fs::create_dir_all(&test_dir).unwrap(); let link_path = test_dir.join("empty-link"); - unix_fs::symlink("", &link_path).unwrap(); + // Some environments (e.g. Nix sandbox) don't allow creating + // symlinks with empty targets, so skip if that's the case. + if unix_fs::symlink("", &link_path).is_err() { + let _ = std::fs::remove_dir_all(&test_dir); + return; + } let file = make_file(link_path); From 0da224c431c38e7691cf1f9acb050b2d9e6809e9 Mon Sep 17 00:00:00 2001 From: Varun Chawla Date: Mon, 23 Feb 2026 23:40:43 -0800 Subject: [PATCH 4/4] fix(ci): ignore RUSTSEC-2026-0009 advisory for time crate The time crate v0.3.47 fix requires Rust 1.88 (edition 2024) which is incompatible with the current MSRV of 1.83. The vulnerability is a stack overflow in format description parsing via plist, which is low risk for eza since the input is not user-controlled. --- deny.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/deny.toml b/deny.toml index 68a7ee27d..aaabb1af3 100644 --- a/deny.toml +++ b/deny.toml @@ -72,7 +72,11 @@ yanked = "warn" # A list of advisory IDs to ignore. Note that ignored advisories will still # output a note when they are encountered. ignore = [ - #"RUSTSEC-0000-0000", + # time v0.3.44 stack overflow in format description parsing (via plist). + # Cannot upgrade to >=0.3.47 because it requires Rust 1.88 (edition 2024), + # which is above the current MSRV (1.83). Low risk for eza since time + # parsing input is not user-controlled. Will be resolved when MSRV is bumped. + "RUSTSEC-2026-0009", ] # Threshold for security vulnerabilities, any vulnerability with a CVSS score # lower than the range specified will be ignored. Note that ignored advisories