Skip to content

Preserve leading .. for drive-relative paths during lexical normalization#302

Open
adityasingh2400 wants to merge 1 commit into
apple:mainfrom
adityasingh2400:fix-drive-relative-dotdot-normalization
Open

Preserve leading .. for drive-relative paths during lexical normalization#302
adityasingh2400 wants to merge 1 commit into
apple:mainfrom
adityasingh2400:fix-drive-relative-dotdot-normalization

Conversation

@adityasingh2400
Copy link
Copy Markdown

On Windows, a traditional drive-relative root like C: anchors a path to the current working directory on the named drive, not to a fixed location. That makes a leading .. meaningful: C:.. is the parent of the drive's current directory, which is generally not the root of the drive. lexicallyNormalize() was treating any root as a fixed anchor and collapsing leading .. against it, so it silently changed the meaning of drive-relative paths:

FilePath(#"C:..\foo\bar"#).lexicallyNormalized()    // produced C:foo\bar
FilePath(#"C:..\..\foo"#).lexicallyNormalized()     // produced C:foo
FilePath(#"C:foo\..\..\bar"#).lexicallyNormalized() // produced C:bar

In each case a .. that escapes the relative portion was dropped, which loses information.

The root cause is in _normalizeSpecialDirectories: the branch that skips a leading .. fired whenever the path had a root (hasRoot && writeIdx == relStart). That is correct for fixed anchors (/ on Unix, and \, C:\, UNC and device roots on Windows, where .. at the front refers to the parent of the root and collapses to the root), but it is wrong for the drive-relative C: form, where the .. has to be kept just like a leading .. on a rootless relative path.

The fix distinguishes the two cases. A new internal Root._isTraditionalDriveRelative identifies the X: form (a two-character root ending in :, which the existing isAbsolute logic already treats as the only colon-terminated relative root). Normalization now preserves a leading .. exactly when the root, if any, is drive-relative. isLexicallyNormal is updated to match, which resolves the FIXME that was sitting on it: C:..\foo\bar is now reported as lexically normal, while \..\foo\bar is not.

After the change:

FilePath(#"C:..\foo\bar"#).lexicallyNormalized()    // C:..\foo\bar
FilePath(#"C:..\..\foo"#).lexicallyNormalized()     // C:..\..\foo
FilePath(#"C:foo\..\..\bar"#).lexicallyNormalized() // C:..\bar
FilePath(#"\..\foo\bar"#).lexicallyNormalized()    // \foo\bar  (unchanged)

These match ntpath.normpath for the same inputs (CPython recently corrected its own drive-relative normalization for the same reason in python/cpython#126780). The rooted \ and absolute cases, and all Unix behavior such as /.. => /, are unaffected.

Verification: reproduced the wrong output against the documented/reference behavior using the package's withWindowsPaths test harness on macOS, applied the fix, and confirmed the outputs match. Added regression cases in FilePathSyntaxTest covering leading and interior .. for both drive-relative (C:) and rooted (\) Windows paths; they fail before the change and pass after. swift test passes in both debug and release (67 XCTest tests and 7 swift-testing tests, 0 failures), and one pre-existing case that encoded the old behavior (C:foo\bar\..\..\.. => C:) was corrected to C:...

This is related to the drive-root discussion in #188 (the trapping/validation questions there are separate and not addressed here).

I have not yet completed the Swift project CLA; happy to sign it as part of the review process per CONTRIBUTING.

…zation

On Windows, a traditional drive-relative root such as `C:` anchors a path
to the current working directory on the named drive, not to a fixed
directory. A leading `..` is therefore meaningful: `C:..` is the parent of
the drive's current directory. `lexicallyNormalize()` was collapsing these
`..` components against the root as if `C:` were a fixed anchor, silently
changing the meaning of the path:

    FilePath(#"C:..\foo\bar"#).lexicallyNormalized()   // was C:foo\bar
    FilePath(#"C:foo\..\..\bar"#).lexicallyNormalized() // was C:bar

Both forms now preserve the escaping `..` (C:..\foo\bar and C:..\bar
respectively), matching the behavior of ntpath.normpath. The fix keys off
whether the root anchors leading parents: only drive-relative `X:` roots
preserve a leading `..`. Rooted `\` and absolute roots (`C:\`, UNC, device)
still collapse it, so `\..\foo` continues to normalize to `\foo` and Unix
`/..` to `/`.

isLexicallyNormal is updated to agree, resolving the existing FIXME: a path
like `C:..\foo\bar` is now reported as lexically normal while `\..\foo\bar`
is not.

Adds regression coverage in FilePathSyntaxTest for leading and interior
`..` against both drive-relative and rooted Windows paths.
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