[libcxx-commits] [libcxx] [libc++] Do not remove a root-name followed by ".." in `path::lexically_normal()` (PR #201261)

Igor Kudrin via libcxx-commits libcxx-commits at lists.llvm.org
Tue Jun 2 21:08:16 PDT 2026


https://github.com/igorkudrin created https://github.com/llvm/llvm-project/pull/201261

For Windows paths like `"C:.."`, `path::lexically_normal()` should preserve both the root-name and the `".."` component. In [fs.path.generic]p6, clause (5) prescribes removing `".."` only after non-dot-dot filenames. Since a root-name is not a filename, this clause does not apply. Clause (6) only applies when there is a root-directory, which is absent in this case. Therefore, the root-name and these `".."` components should not be removed.

This change aligns `libc++` with MSVC.

Example code:
```
#include <filesystem>
#include <iostream>

int main()
{
  static const char *samples[] = {
    "C:..",
    "C:../..",
    "C:foo\\..\\..",
    0
  };
  for (int i = 0; samples[i]; ++i) {
    auto norm = std::filesystem::path(samples[i]).lexically_normal();
    std::cout << "'" << samples[i] << "' ==> '" << norm.string() << "'\n";
  }
}
```

Output comparison:
```
> test.msvc.exe
'C:..' ==> 'C:..'
'C:../..' ==> 'C:..\..'
'C:foo\..\..' ==> 'C:..'

> test.clang.exe (before fix)
'C:..' ==> '.'
'C:../..' ==> '..'
'C:foo\..\..' ==> '.'
```

>From c1cbcbf05db12483a9b2c6b576e527319b85e924 Mon Sep 17 00:00:00 2001
From: Igor Kudrin <ikudrin at accesssoftek.com>
Date: Tue, 2 Jun 2026 19:45:58 -0700
Subject: [PATCH] [libc++] Do not remove a root-name followed by ".." in
 `path::lexically_normal()`

For Windows paths like `"C:.."`, `path::lexically_normal()` should
preserve both the root-name and the `".."` component. In
[fs.path.generic]p6, clause (5) prescribes removing `".."` only after
non-dot-dot filenames. Since a root-name is not a filename, this clause
does not apply. Clause (6) only applies when there is a root-directory,
which is absent in this case. Therefore, the root-name and these `".."`
components should not be removed.

This change aligns `libc++` with MSVC.

Example code:
```
#include <filesystem>
#include <iostream>

int main()
{
  static const char *samples[] = {
    "C:..",
    "C:../..",
    "C:foo\\..\\..",
    0
  };
  for (int i = 0; samples[i]; ++i) {
    auto norm = std::filesystem::path(samples[i]).lexically_normal();
    std::cout << "'" << samples[i] << "' ==> '" << norm.string() << "'\n";
  }
}
```

Output comparison:
```
> test.msvc.exe
'C:..' ==> 'C:..'
'C:../..' ==> 'C:..\..'
'C:foo\..\..' ==> 'C:..'

> test.clang.exe (before fix)
'C:..' ==> '.'
'C:../..' ==> '..'
'C:foo\..\..' ==> '.'
```
---
 libcxx/src/filesystem/path.cpp                | 21 +++++++++----------
 .../path.gen/lexically_normal.pass.cpp        |  6 ++++++
 2 files changed, 16 insertions(+), 11 deletions(-)

diff --git a/libcxx/src/filesystem/path.cpp b/libcxx/src/filesystem/path.cpp
index 12a698da901a4..7b60e00625a36 100644
--- a/libcxx/src/filesystem/path.cpp
+++ b/libcxx/src/filesystem/path.cpp
@@ -140,21 +140,20 @@ string_view_t path::__extension() const { return parser::separate_filename(__fil
 ////////////////////////////////////////////////////////////////////////////
 // path.gen
 
-enum PathPartKind : unsigned char { PK_None, PK_RootSep, PK_Filename, PK_Dot, PK_DotDot, PK_TrailingSep };
+enum PathPartKind : unsigned char { PK_None, PK_RootName, PK_RootSep, PK_Filename, PK_Dot, PK_DotDot, PK_TrailingSep };
 
-static PathPartKind ClassifyPathPart(string_view_t Part) {
+static PathPartKind ClassifyPathPart(const PathParser &PP) {
+  if (PP.inRootName())
+    return PK_RootName;
+  if (PP.inRootDir())
+    return PK_RootSep;
+  string_view_t Part = *PP;
   if (Part.empty())
     return PK_TrailingSep;
   if (Part == PATHSTR("."))
     return PK_Dot;
   if (Part == PATHSTR(".."))
     return PK_DotDot;
-  if (Part == PATHSTR("/"))
-    return PK_RootSep;
-#if defined(_LIBCPP_WIN32API)
-  if (Part == PATHSTR("\\"))
-    return PK_RootSep;
-#endif
   return PK_Filename;
 }
 
@@ -184,13 +183,13 @@ path path::lexically_normal() const {
   // Build a stack containing the remaining elements of the path, popping off
   // elements which occur before a '..' entry.
   for (auto PP = PathParser::CreateBegin(__pn_); PP; ++PP) {
-    auto Part         = *PP;
-    PathPartKind Kind = ClassifyPathPart(Part);
+    PathPartKind Kind = ClassifyPathPart(PP);
     switch (Kind) {
     case PK_Filename:
+    case PK_RootName:
     case PK_RootSep: {
       // Add all non-dot and non-dot-dot elements to the stack of elements.
-      AddPart(Kind, Part);
+      AddPart(Kind, *PP);
       MaybeNeedTrailingSep = false;
       break;
     }
diff --git a/libcxx/test/std/input.output/filesystems/class.path/path.member/path.gen/lexically_normal.pass.cpp b/libcxx/test/std/input.output/filesystems/class.path/path.member/path.gen/lexically_normal.pass.cpp
index e90f67bb80931..5dfb24d52471e 100644
--- a/libcxx/test/std/input.output/filesystems/class.path/path.member/path.gen/lexically_normal.pass.cpp
+++ b/libcxx/test/std/input.output/filesystems/class.path/path.member/path.gen/lexically_normal.pass.cpp
@@ -107,6 +107,12 @@ int main(int, char**) {
       {"foo/bar/baz/../../", "foo/"},
       {"foo/bar/./..", "foo/"},
       {"foo/bar/./../", "foo/"},
+#ifdef _WIN32
+      /// A root-name followed by a dot-dot filename should not be removed.
+      {"C:..", "C:.."},
+      {"C:..\\..", "C:..\\.."},
+      {"C:foo\\..\\..", "C:.."},
+#endif
       // p6: If there is a root-directory, remove all dot-dot filenames and any
       // directory-separators immediately following them. [ Note: These dot-dot
       // filenames attempt to refer to nonexistent parent directories. - end note ]



More information about the libcxx-commits mailing list