Skip to content

fix: LocalPath.IsPrefixOf for root paths - #223

Merged
ForNeVeR merged 13 commits into
ForNeVeR:mainfrom
t957095:fix/isprefixof-root
Sep 19, 2026
Merged

ForNeVeR merged 13 commits into
ForNeVeR:mainfrom
t957095:fix/isprefixof-root

Conversation

@t957095

@t957095 t957095 commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Problem

LocalPath.IsPrefixOf returned false for a root prefix: new LocalPath("/").IsPrefixOf(new LocalPath("/frob")) — because for a root path the character after the prefix is not a separator.

Fix

Adds the same Value[Value.Length - 1] == Separator clause that AbsolutePath.IsPrefixOf already uses, plus the rooted-path test from the issue and a Unix InlineData("/", "/frob", true) row.

Note: I couldn't run the .NET suite locally (SDK pin 10.0.401 vs installed 10.0.201 on this machine) — relying on CI for verification.

Closes #221

@ForNeVeR
ForNeVeR self-requested a review September 19, 2026 13:14
@ForNeVeR ForNeVeR self-assigned this Sep 19, 2026
A root path like 'A:\' (Windows) or '/' (Unix) ends with the
directory separator, so the next character in the child path is not a
separator and the old check failed. Adds the same
'value ends with separator' clause that AbsolutePath.IsPrefixOf
already has, plus the rooted-path test from the issue and a Unix
InlineData row.

Closes ForNeVeR#221
An empty path is the normalized form of "", "." and "a/..", and is also
the parent of any single-segment relative path, so it reaches IsPrefixOf
easily. The trailing-separator check added by the first commit of PR ForNeVeR#223
indexed Value[-1] on it and threw IndexOutOfRangeException.

It now answers true: the empty path is the current directory, and so a
prefix of every relative path.
An absolute path is never a prefix of a relative one, nor the other way
round: deciding that would mean resolving the relative path against the
current directory, which this library never does. Previously the answer
fell out of an incidental string comparison, so "." was reported as a
prefix of "/foo" (every string starts with the empty one).

The IsAbsolute check is only an approximation of the four kinds of path
a LocalPath can hold; see the TODOs for ForNeVeR#224. The ordinal comparison it
guards disagrees with Equals on Windows and macOS: TODO for ForNeVeR#225.
Both types implemented StartsWith as a raw string test, so it ignored
segment boundaries: "/foo1".StartsWith("/foo") was true while
"/foo".IsPrefixOf("/foo1") was false. Issue ForNeVeR#43 asked for the two to be
mirrors of each other, so express one through the other.

BREAKING: StartsWith now compares whole path segments on both LocalPath
and AbsolutePath.
AbsolutePath already wraps a LocalPath and delegates Value, Parent and
FileName to it, but carried a character-for-character copy of the prefix
algorithm, so every fix had to be made twice. Delegate instead.

The empty-path and absoluteness branches are unreachable here, since an
AbsolutePath is always rooted and never empty.

IsPrefixOfShouldBeEquivalentToStartsWith compared against the string
method rather than the path one, which is the very semantics that just
changed; it now tests the property its name claims, on both platforms.
The empty path is the current directory, but the arm added for it in
this PR answered true for any relative path, so "" (equally ".", "a/.."
or the parent of any single-segment relative path) was reported as a
prefix of "../evil", which sits outside the current directory. A caller
using IsPrefixOf or StartsWith as a containment check accepted the
escape.

Reported as ../evil rather than found by the tests, none of which
exercised a ".." path.
The single-argument string.StartsWith overload compares under the
current culture, which ignores collation-ignorable characters, so
"­foo" was accepted as a prefix of "fooo/bar" - and likewise for
AbsolutePath, which now delegates here.

This also makes the TODO for ForNeVeR#225 true as written: what remains is the
mismatch between this ordinal comparison and the platform-default one
that Equals uses.
Once StartsWith is defined as other.IsPrefixOf(this), comparing
a.IsPrefixOf(b) with b.StartsWith(a) compares a value with itself, so
the test could not fail for any input. Give every row an expected
result, and assert both directions against it: the equivalence is still
stated, but it is now falsifiable.

@ForNeVeR ForNeVeR left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I found a whole can of worms in various components during review, and decided to address some bits in this branch.

@ForNeVeR
ForNeVeR merged commit d99803f into ForNeVeR:main Sep 19, 2026
10 checks passed
ForNeVeR added a commit that referenced this pull request Sep 19, 2026
An empty path is the normalized form of "", "." and "a/..", and is also
the parent of any single-segment relative path, so it reaches IsPrefixOf
easily. The trailing-separator check added by the first commit of PR #223
indexed Value[-1] on it and threw IndexOutOfRangeException.

It now answers true: the empty path is the current directory, and so a
prefix of every relative path.
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.

LocalPath.IsPrefixOf is incorrect for the root folder

2 participants