Skip to content

Optimize PathBuf::push for verbatim paths - #162490

Open
ChrisDenton wants to merge 2 commits into
rust-lang:mainfrom
ChrisDenton:optimize-path-push
Open

Optimize PathBuf::push for verbatim paths#162490
ChrisDenton wants to merge 2 commits into
rust-lang:mainfrom
ChrisDenton:optimize-path-push

Conversation

@ChrisDenton

@ChrisDenton ChrisDenton commented Sep 8, 2026

Copy link
Copy Markdown
Member

View all comments

Collecting all the parts is unnecessary.

The added tests are there to test that existing behaviour is kept. This PR does not change the behaviour of push,

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Sep 8, 2026
@rustbot

rustbot commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

r? @Darksonn

rustbot has assigned @Darksonn.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: @ChrisDenton, libs
  • @ChrisDenton, libs expanded to 13 candidates
  • Random selection from Darksonn, JohnTitor, Mark-Simulacrum

@Darksonn Darksonn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comment thread library/std/tests/path.rs
Comment on lines +1762 to +1764
tp!(r"\\?\C:\foo\bar", "../baz", r"\\?\C:\foo\baz");
tp!(r"\\?\C:\foo\bar\", "../baz", r"\\?\C:\foo\baz");
tp!(r"\\?\C:\foo\bar\", "..", r"\\?\C:\foo");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What about pushing .. to \\?\C:\foo, going all the way to the root? Or even pushing .. to \\?\C:\? What about pushing .. to \\?\C:\foo\..?

What about the UNC share case? What if you push .. to \\?\UNC\server\share\foo or \\?\UNC\server\share\?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

What about pushing .. to \\?\C:\foo, going all the way to the root? Or even pushing .. to \\?\C:\? What about pushing .. to \\?\C:\foo\..?

Aren't some these cases not handled above and below? I can add some more though.

What about the UNC share case? What if you push .. to \\?\UNC\server\share\foo or \\?\UNC\server\share\?

Hm, looking through the tests it seems we don't have as good coverage of \\?\UNC paths as I thought. I'll add some.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done. There were basically two cases where the old and new version differed:

  • When a root ended with a \ (e.g. \\?\C:\) the old version always preserved the \ but the new version didn't (e.g. \\?\C:). I think either is arguable for verbatim paths but I'll side with keep the existing behaviour.
  • In the old version a verbatim path ending with .. was not removed at all. So pushing .. to \\?\C:\foo\.. resulted in \\?\C:\foo\... I cannot see how that could be right, especially as it didn't have a test case. So I think fixing it is the right thing to do.

In any case I've split the commits into two so you can see what changed more easily.

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Sep 8, 2026
@rustbot

rustbot commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@Darksonn Darksonn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Are we really sure that this optimization is a good idea? It's pretty error-prone.

@rustbot author

View changes since this review

Comment thread library/std/src/path.rs Outdated
Comment thread library/std/tests/path.rs Outdated
tp!(r"\\?\C:\foo\bar\", "..", r"\\?\C:\foo");
tp!(r"\\?\C:\foo", "..", r"\\?\C:\");
tp!(r"\\?\C:\", "..", r"\\?\C:\");
tp!(r"\\?\C:\foo\..", "..", r"\\?\C:\foo");

@Darksonn Darksonn Sep 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doesn't make sense. Resolving the paths, you're starting at the root \\?\C:\ (except written slightly funny as \\?\C:\foo\..), then you push .., and now you are in a sub-directory of the root \\?\C:\foo.

Surely .. should never navigate to a sub-directory.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is confusing but for verbatim paths, the .. in \\?\C:\foo\.. is a literal directory name so it's equivalent to something like \\?\C:\foo\bar. It is intended that there's a special case for pushing .. so that it behaves like a pop for verbatim paths.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not sure, but I think we should stick to keeping existing behavior in this PR. If you think this behavior is wrong, can we fix that in a separate and more targeted PR?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I just seems buggy to me that PathBuf::from(r"\\?\C:\foo\..").push(r"..\..\..\bar") gives the path \\?\C:\foo\..\bar. But I can try to recreate it.

@ChrisDenton

Copy link
Copy Markdown
Member Author

Are we really sure that this optimization is a good idea? It's pretty error-prone.

I think the previous implementation was not quite working as intended either but that aside, it's very slow. Every single component you push required collecting a new Vec of all the components in the base path and the base path would grow on every iteration. When pushing long paths this could get very expensive.

@Darksonn

Darksonn commented Sep 9, 2026

Copy link
Copy Markdown
Member

If the generic implementation is O(n^2), could we fix that rather than add a special case that skips it?

@ChrisDenton

Copy link
Copy Markdown
Member Author

I mean it was already a 40 line special case, no? It was using OsString::push directly and not the generic PathBuf::push. I'm not sure that can be rescued without rewriting it into something else because we want to avoid the initial Vec and we want to reuse the underlying PathBuf allocation instead of making a new OsString.

@Darksonn

Darksonn commented Sep 9, 2026

Copy link
Copy Markdown
Member

Ok, I didn't catch this was entirely replacing the O(n^2) logic

@Darksonn

Darksonn commented Sep 9, 2026

Copy link
Copy Markdown
Member

Could we potentially treat the path as non-verbatim when it contains an .. component?

@ChrisDenton

Copy link
Copy Markdown
Member Author

Could we potentially treat the path as non-verbatim when it contains an .. component?

I like the idea that how the path is handled is decided by its prefix alone. And I'm not too keen on needing to scan the entire path on every push.

In any case I've now updated it so there's no behaviour change in this PR.

@Darksonn Darksonn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since Path::components doesn't allocate, could we make use of it here? For instance, invoke self.components().next_back() to check whether the trailing component is Component::Normal(_), instead of the current ad-hoc check.

@rustbot author

View changes since this review

Comment thread library/std/src/path.rs Outdated
if let Some(Component::Normal(_)) = buf.last() {
buf.pop();
// Preserve pre-existing behaviour of not going above any literal `..` in the base path.
if self.inner.as_encoded_bytes().trim_suffix(b"\\").ends_with(b"\\..") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This doesn't handle repeated slashes, so pushing .. to a path like \\?\C:\foo\..\\ still changes behavior.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hm, empty path components in verbatim paths are not valid. I'm not sure that we particularly need to preserve the behaviour in such cases? Or at least I worry about specifying it.

Comment thread library/std/src/path.rs Outdated
Comment on lines +1390 to +1391
// Preserve pre-existing behaviour of not going above any literal `..` in the base path.
if self.inner.as_encoded_bytes().trim_suffix(b"\\").ends_with(b"\\..") {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The pre-existing behavior is to only go up for normal components, and .. is not the only non-normal component. So for example, \\?\C:\foo\. will remain unchanged when pushing .. on main, but this PR changes it to remove the trailing CurDir component.

Comment thread library/std/src/path.rs Outdated
buf.pop();
// Preserve pre-existing behaviour of not going above any literal `..` in the base path.
if self.inner.as_encoded_bytes().trim_suffix(b"\\").ends_with(b"\\..") {
continue;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When you hit this continue, no changes are made to the existing path, but the code on main will remove trailing slashes. So for example, you changed the behavior of pushing .. to \\?\C:\foo\..\.

Comment thread library/std/src/path.rs Outdated
// Manually implement a simplified version of pop optimized for verbatim paths.
let mut iter = self.inner.as_encoded_bytes()[prefix_len..].iter();
iter.rfind(|&&b| b != sep_byte);
let pos = iter.rposition(|&b| b == sep_byte).unwrap_or(0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This use of rposition means that pushing .. to \\?\C:\foo\\\\bar will return \\?\C:\foo\\\, whereas on main this will return \\?\C:\foo on main.

@ChrisDenton

Copy link
Copy Markdown
Member Author

I rewrote the Component::ParentDir branch to be in terms of self.components. Thus it should preserve existing behaviour.

@ChrisDenton
ChrisDenton force-pushed the optimize-path-push branch 3 times, most recently from 6691859 to 42ee54e Compare September 10, 2026 00:55
@ChrisDenton

Copy link
Copy Markdown
Member Author

Ok, I've rewritten it to use pre-existing functions instead of hand coding it. I've also changed it to match the existing behaviour, though I do find that behaviour to be questionable.

@Darksonn Darksonn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I do agree that the current behavior is somewhat questionale, but if we're changing behavior I think we need to libs-nominate it to check that the libs team is ok with the change.

Process-wise, it's easier to do that by making no behavior changes here, and then changing it in a follow-up PR, because that follow-up PR can then show a diff where the behaviors that are changed show up as modifications to the tests.

View changes since this review

Comment thread library/std/src/path.rs Outdated
Comment thread library/std/tests/path.rs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We're still changing the behavior when there are internal double slashes. For example:

  • PathBuf::from(r"\\?\C:\foo\\").push("bar")
  • PathBuf::from(r"\\?\C:\foo\\\\bar").push("baz")

@ChrisDenton ChrisDenton Sep 10, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I've added back iterating over the full base path to fixup consecutive \ but without allocating, which does require a way to get a mutable slice from an OsStr.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think you can use mem::take on the PathBuf, then convert it to a vector, then use retain, then convert it back.

Comment thread library/std/tests/path.rs
@rust-log-analyzer

This comment has been minimized.

@ChrisDenton

Copy link
Copy Markdown
Member Author

I created #162643 to fix issues with the trailing sep methods when used with verbatim paths.

@Darksonn Darksonn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I now agree that this does not change behavior. A few nits below, but otherwise LGTM.

View changes since this review

Comment thread library/std/src/path.rs
Comment on lines +1375 to +1387
// collapse multiple verbatim separators in the base path.
let mut i = prefix_len;
while i < self.inner.len() {
// SAFETY: we're only removing the platform's separator characters
let bytes = &mut unsafe { self.inner.as_bytes_mut() }[i..];
let pos = bytes.iter().position(|&b| !is_verbatim_sep(b)).unwrap_or(bytes.len());
if pos > 1 {
bytes.copy_within(pos.., 1);
let new_len = i + (bytes.len() - pos) + 1;
self.inner.truncate(new_len);
}
i += 1;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Might be slightly simpler if you convert the path into a vector and use retain.

let mut skip = prefix_len;
let mut prev_sep = false;

vector.retain_mut(|&mut v| {
    if skip > 0 {
        skip -= 1;
        return true;
    }
    let cur_sep = is_verbatim_sep(v);
    let is_duplicate = prev_sep && cur_sep;
    prev_sep = cur_sep;
    !is_duplicate
});

self.inner.eq_ignore_ascii_case(&other.inner)
}

pub unsafe fn as_mut_slice(&self) -> &[u8] {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Only used on a tier 3 target, but this does not compile.

Suggested change
pub unsafe fn as_mut_slice(&self) -> &[u8] {
pub unsafe fn as_bytes_mut(&self) -> &[u8] {

Comment thread library/std/tests/path.rs

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think the test coverage is missing cases starting with Verbatim("") such as \\?\\foo (as opposed to \\?\foo) where the double \\ right before foo must not be deleted by our loop.

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.

4 participants