`Path::with_extension` drops the file name for names starting with `..`, returning a parent directory
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 119k
- Forks
- 16.1k
- PR merge metrics
- PR metrics pending
Description
Path::with_extension is documented only as "See [PathBuf::set_extension] for more details", but for a file name that starts with .. and has no further dot, the two disagree: with_extension discards the file name entirely and returns the parent directory.
Code
use std::path::{Path, PathBuf};
fn main() {
for p in ["a/..b", "..a", "/x/..y", "dir/..gitignore"] {
let with = Path::new(p).with_extension("txt");
let mut set = PathBuf::from(p);
set.set_extension("txt");
println!("{p:18} with_extension={:14} set_extension={}", with.display().to_string(), set.display());
}
}
Current output (stable 1.97.0, also main @ be3d26db984c)
a/..b with_extension=a/.. set_extension=a/..txt
..a with_extension=.. set_extension=..txt
/x/..y with_extension=/x/.. set_extension=/x/..txt
dir/..gitignore with_extension=dir/.. set_extension=dir/..txt
Expected output
The two methods agree, i.e. with_extension returns a/..txt, ..txt, /x/..txt, dir/..txt.
Why this matters
p.with_extension("txt") is the idiomatic "same file, different extension" call, and it is common to feed the result straight to File::create / fs::write. For a file name beginning with .. the result is not a sibling file — it is .., the parent directory. The write then fails (or, with a trailing component appended by the caller, targets a directory above the intended one). There is no error and no false return to notice: with_extension discards the bool from the internal set_extension call.
Names like ..gitignore are unusual but perfectly legal on every platform, and a path can arrive from user input, a config file, or a directory walk.
Cause
_with_extension truncates the path to self_len - previous_extension.len(), which removes the old extension but keeps the dot that preceded it, and then calls set_extension on that intermediate. For a/..b, extension() is Some("b"), so the intermediate is a/.. — which has no file_name() — so set_extension returns false and does nothing.
This is a regression from https://github.com/rust-lang/rust/pull/113106 (1a44b45987a5, first released in 1.73.0), which replaced to_path_buf() + set_extension with the hand-rolled byte path to save an allocation. Before that the two agreed by construction. The ..X case was not discussed there, and library/std/tests/path.rs::test_with_extension covers only exact-.. components ("..", "foo/.."), never "..foo" — even though its case list otherwise mirrors test_set_extension line for line.
Scope
An exhaustive differential check of with_extension against to_path_buf() + set_extension over all 1365 paths of length ≤ 5 over {. a b /} crossed with six extensions (8190 pairs) finds 168 disagreements, all of the same shape: file name starting with .. and containing no further dot. Nothing else diverges. with_added_extension is unaffected — it still delegates via to_path_buf().
Not https://github.com/rust-lang/rust/issues/49213
That issue asks whether ..a should split as stem "." + extension "a" in the first place. This report takes no position on that: given the current split, set_extension already produces ..txt, and with_extension should match. If #49213 is ever resolved, both methods change together.
@rustbot label +T-libs-api +A-io +C-bug
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in library/std/src/path.rs at _with_extension and compare its behavior with set_extension. Add a regression case to library/std/tests/path.rs test_with_extension for names beginning with .., then run the path tests and verify that with_extension agrees with set_extension for the reported examples.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100