rust-lang / rust-lang/rust

`Path::with_extension` drops the file name for names starting with `..`, returning a parent directory

Open
#160,239 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

A-io C-bug needs-triage T-libs
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.