Skip to content

fix: a relative root ending in .. is mis-resolved - #373

Open
VXNCXNX wants to merge 1 commit into
Byron:mainfrom
VXNCXNX:fix/root-path-ending-in-dotdot
Open

fix: a relative root ending in .. is mis-resolved#373
VXNCXNX wants to merge 1 commit into
Byron:mainfrom
VXNCXNX:fix/root-path-ending-in-dotdot

Conversation

@VXNCXNX

@VXNCXNX VXNCXNX commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

A relative root whose last component is .. or . is mis-resolved into a path
that does not exist, so it reports an I/O error and a directory-stat size instead
of traversing.

cwd containing sub/f (100 b) and g (200 b):

$ dua aggregate -f bytes --no-sort sub/.. sub
before:
     4096 b sub/..  <1 IO Error>
     8192 b sub
    12288 b total  <1 IO Error>
exit=1

after:
    12288 b sub/..
     8192 b sub
    20480 b total
exit=0

Cause

Entry::from_path splits a root into the two fields Entry::path() later
rejoins, using a different fallback for each half:

file_name: path.file_name().unwrap_or(path.as_os_str()).to_owned(),
parent_path: Arc::from(path.parent().unwrap_or(Path::new(""))),

Path::file_name returns None exactly when the last component is . or ..,
but Path::parent still returns Some. For sub/.. that gives parent sub and
file name sub/.., and rejoining them produces sub/sub/...

Two things kept this hidden. An absolute root is unaffected, because
Path::join discards the left side when the right side is absolute. And a
single directory root is unaffected, because dua changes into it before
walking. It needs a relative root, ending in .. or ., alongside another
root.

The fix

Split through one helper that keeps the two halves consistent: if the path has
no separable file name, keep it whole as the file name with an empty parent,
which is what the file_name fallback was already reaching for. The
platform-specific from_path in macos/mod.rs and windows.rs carry the same
two lines and get the same substitution.

Verification

split_root_path_round_trips_through_entry_path asserts the property directly,
that the split rejoins into the original path, over sub/.., .., .,
sub/child and child, plus an explicit check that an ordinary path still
splits into parent and name so children stay correctly named.

Reverting only the helper body fails it:

thread 'tests::split_root_path_round_trips_through_entry_path' panicked at crates/dua-lib/src/lib.rs:1100:13:
assertion `left == right` failed: `Entry::path` rejoins the split of "sub/.." into the very same path
  left: "sub/sub/.."
 right: "sub/.."

The end-to-end case is in tests/stateless-journey.sh rather than a unit test,
deliberately: it needs a working directory, and a test that calls
set_current_dir races the suite's own helpers. The journey suite runs the real
binary in a mktemp -d sandbox, and it catches the same mutation independently:

[with] a relative root whose last component is '..'
   [it] traverses it like any other directory - FAIL
Expected actual status 1 to be 0

cargo test --all is 26 + 73 + 6 + 3 passed, 0 failed. Also green:
--no-default-features --features trash-move, cargo check --all-features,
cargo check --no-default-features, cargo fmt --check, and the journey suite
exits 0.

Two things worth saying plainly:

  • cargo clippy -D warnings fails on crates/dua-lib/src/lib.rs with
    clippy::err_expect. I verified by stashing that it fails identically on
    main, so I left it alone.
  • I cannot execute macOS or Windows. The change there is the same mechanical
    substitution of the same two expressions, and the helper and its test are
    platform-independent, but the platform paths themselves are unexercised by me.

Disclosure: written with AI assistance (Claude Code). I produced the before and after above by running binaries built from each tree, and ran the mutation check and the journey suite myself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant