Skip to content

Preserve partial normalized value in NormalizeError#159866

Draft
schneems wants to merge 3 commits into
rust-lang:mainfrom
schneems:schneems/partial-normalize-on-error
Draft

Preserve partial normalized value in NormalizeError#159866
schneems wants to merge 3 commits into
rust-lang:mainfrom
schneems:schneems/partial-normalize-on-error

Conversation

@schneems

@schneems schneems commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

We cannot normalize a relative path past the base because we do not know what the parent is (without touching disk, and assuming CWD). However, a developer might still want a partially normalized value if they're using it for something like comparing if two paths are (roughly) equal or not.

This change adds the ability to normalize_lexically users to retrieve the partially normalized value:

#![feature(normalize_lexically)]
use std::path::{Path, PathBuf};
let a = Path::new("a/../../b")
    .normalize_lexically()
    .unwrap_or_else(|e| e.into_partial());
let b = Path::new("../x/../b")
    .normalize_lexically()
    .unwrap_or_else(|e| e.into_partial());

assert_eq!(a, b);
assert_eq!(a, PathBuf::from("../b"));

Related tracking issue for normalize_lexically unstable feature #134694.

schneems added 3 commits July 24, 2026 15:38
It is common to pin a path normalization to root for many languages:

```ruby
# Ruby
File.expand_path("/tmp/../../lol")
=> "/lol"
```

The error on `normalize_lexically` makes sense for a relative path because Rust CANNOT know how to construct the parent path without performing a call to get CWD. However, for absolute path, I am proposing that we stabilize this behavior to pin at the root for absolute paths. There are links to the POSIX spec and documentation in the commit.
The prior code used `root` which was a len of the **base** of a path sometimes, and simply a low number at other times. It also tracked if the base dir was root or not. Eliminating both yields less code with fewer intermediate variables.
We cannot normalize a relative path past the base because we do not know what the parent is (without touching disk, and assuming CWD). However, a developer might still want a partially normalized value if they're using it for something like comparing if two paths are (roughly) equal or not.

This change adds the ability to `normalize_lexically` users to retrieve the partially normalized value:

```
#![feature(normalize_lexically)]
use std::path::{Path, PathBuf};
let a = Path::new("a/../../b")
    .normalize_lexically()
    .unwrap_or_else(|e| e.into_partial());
let b = Path::new("../x/../b")
    .normalize_lexically()
    .unwrap_or_else(|e| e.into_partial());

assert_eq!(a, b);
assert_eq!(a, PathBuf::from("../b"));
```
@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jul 24, 2026
Comment thread library/std/src/path.rs

/// An error returned from [`Path::normalize_lexically`] if a `..` parent reference
/// would escape the path.
///

@schneems schneems Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

First commit is stacked on #159863. This logic is easier to represent when the absolute path does not error. If that gets merged, I'll promote this from draft. If we want to close it and move forward with this alone, I can.

Also happy to rebase/squash the refactor intermediate commit, I left it in to make the change in form/format different than the logic changes needed to preserve and return the partially normalized path.

View changes since the review

Comment thread library/std/src/path.rs
}
}
Component::Normal(path) => lexical.push(path),
Component::ParentDir => match lexical.components().next_back() {

@schneems schneems Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Perf note: This code is called every time a .. is in the path. Constructing components re-parses the root of the output string, then the next_back parses backward (IIRC). So this is slower. It's balanced out with the PathBuf::with_capacity() optimization above. I didn't profile the difference.`

Reviewer: Please dismiss the note at will. Leaving it as a self-comment to note a design tradeoff in implementation.

View changes since the review

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

Labels

S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants