[llvm] [Windows][Support] Add helper to expand short 8.3 form paths (PR #178480)
Ben Dunbobbin via llvm-commits
llvm-commits at lists.llvm.org
Tue Feb 10 04:24:46 PST 2026
https://github.com/bd1976bris updated https://github.com/llvm/llvm-project/pull/178480
>From 836e04c06ebfebb8fa68bc6de26e6835472d60e9 Mon Sep 17 00:00:00 2001
From: Ben <ben.dunbobbin at sony.com>
Date: Wed, 28 Jan 2026 18:14:24 +0000
Subject: [PATCH 1/7] [Windows][Support] Add helper to expand short 8.3 form
paths
Windows supports short 8.3 form filenames (for example,
compile_commands.json -> COMPIL~1.JSO) for legacy reasons. See:
https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#short-vs-long-names
Such paths are not unusual because, on Windows, the system temporary
directory is commonly derived from the TMP/TEMP environment variables.
For historical compatibility reasons, these variables are often set to
short 8.3 form paths on systems where user names exceed eight
characters.
Introduce windows::makeLongFormPath() to convert UTF-8 paths to their
long form by expanding any 8.3 components via GetLongPathNameW.
As part of this change, extended-length path prefix handling is
centralized by adding stripExtendedPrefix() and reusing it in
realPathFromHandle(), and widenPath() is cleaned up to use shared
prefix constants.
---
.../llvm/Support/Windows/WindowsSupport.h | 5 +
llvm/lib/Support/Windows/Path.inc | 92 ++++++++++--
llvm/unittests/Support/Path.cpp | 133 ++++++++++++++++++
3 files changed, 215 insertions(+), 15 deletions(-)
diff --git a/llvm/include/llvm/Support/Windows/WindowsSupport.h b/llvm/include/llvm/Support/Windows/WindowsSupport.h
index f35e7b55cb8d3..50a2540dba687 100644
--- a/llvm/include/llvm/Support/Windows/WindowsSupport.h
+++ b/llvm/include/llvm/Support/Windows/WindowsSupport.h
@@ -249,6 +249,11 @@ LLVM_ABI std::error_code widenPath(const Twine &Path8,
/// ensuring we're not retrieving a malicious injected module but a module
/// loaded from the system path.
LLVM_ABI HMODULE loadSystemModuleSecure(LPCWSTR lpModuleName);
+
+/// Convert a UTF-8 path to a long form UTF-8 path expanding any short 8.3 form
+/// components.
+LLVM_ABI std::error_code makeLongFormPath(const Twine &Path8,
+ llvm::SmallVectorImpl<char> &Result8);
} // end namespace windows
} // end namespace sys
} // end namespace llvm.
diff --git a/llvm/lib/Support/Windows/Path.inc b/llvm/lib/Support/Windows/Path.inc
index c03b85b2f4bb3..f58b54c3afb0c 100644
--- a/llvm/lib/Support/Windows/Path.inc
+++ b/llvm/lib/Support/Windows/Path.inc
@@ -60,6 +60,35 @@ static bool is_separator(const wchar_t value) {
}
}
+// Extended-length path prefix constants (UTF-8).
+static constexpr llvm::StringLiteral ExtendedPrefix8 = R"(\\?\)";
+static constexpr llvm::StringLiteral ExtendedUNCPrefix8 = R"(\\?\UNC\)";
+
+// Extended-length path prefix constants (UTF-16).
+static constexpr wchar_t ExtendedPrefix16[] = LR"(\\?\)";
+static constexpr wchar_t ExtendedUNCPathPrefix16[] = LR"(\\?\UNC\)";
+
+static constexpr DWORD ExtendedPrefix16Len =
+ static_cast<DWORD>(std::size(ExtendedPrefix16) - 1);
+static constexpr DWORD ExtendedUNCPathPrefix16Len =
+ static_cast<DWORD>(std::size(ExtendedUNCPathPrefix16) - 1);
+
+static void stripExtendedPrefix(wchar_t *&Data, DWORD &CountChars) {
+ if (CountChars >= ExtendedUNCPathPrefix16Len &&
+ ::wmemcmp(Data, ExtendedUNCPathPrefix16, ExtendedUNCPathPrefix16Len) ==
+ 0) {
+ // Convert \\?\UNC\foo\bar to \\foo\bar
+ CountChars -= 6;
+ Data += 6;
+ Data[0] = L'\\';
+ } else if (CountChars >= ExtendedPrefix16Len &&
+ ::wmemcmp(Data, ExtendedPrefix16, ExtendedPrefix16Len) == 0) {
+ // Convert \\?\C:\foo to C:\foo
+ CountChars -= 4;
+ Data += 4;
+ }
+}
+
namespace llvm {
namespace sys {
namespace windows {
@@ -95,10 +124,8 @@ std::error_code widenPath(const Twine &Path8, SmallVectorImpl<wchar_t> &Path16,
return mapWindowsError(::GetLastError());
}
- const char *const LongPathPrefix = "\\\\?\\";
-
if ((Path16.size() + CurPathLen) < MaxPathLen ||
- Path8Str.starts_with(LongPathPrefix))
+ Path8Str.starts_with(ExtendedPrefix8))
return std::error_code();
if (!IsAbsolute) {
@@ -116,17 +143,61 @@ std::error_code widenPath(const Twine &Path8, SmallVectorImpl<wchar_t> &Path16,
assert(!RootName.empty() &&
"Root name cannot be empty for an absolute path!");
- SmallString<2 * MAX_PATH> FullPath(LongPathPrefix);
+ SmallString<2 * MAX_PATH> FullPath;
if (RootName[1] != ':') { // Check if UNC.
- FullPath.append("UNC\\");
+ FullPath.append(ExtendedUNCPrefix8);
FullPath.append(Path8Str.begin() + 2, Path8Str.end());
} else {
+ FullPath.append(ExtendedPrefix8);
FullPath.append(Path8Str);
}
return UTF8ToUTF16(FullPath, Path16);
}
+std::error_code makeLongFormPath(const Twine &Path8,
+ llvm::SmallVectorImpl<char> &Result8) {
+ SmallString<128> PathStorage;
+ StringRef PathStr = Path8.toStringRef(PathStorage);
+ bool HadPrefix = PathStr.starts_with(ExtendedPrefix8);
+
+ SmallVector<wchar_t, 128> Path16;
+ if (std::error_code EC = widenPath(PathStr, Path16))
+ return EC;
+
+ // Start with a buffer equal to input.
+ llvm::SmallVector<wchar_t, 128> Long16;
+ DWORD Len = static_cast<DWORD>(Path16.size());
+
+ do {
+ Long16.resize_for_overwrite(Len);
+
+ Len = ::GetLongPathNameW(Path16.data(), Long16.data(), Len);
+
+ // A zero return value indicates a failure other than insufficient space.
+ if (Len == 0)
+ return mapWindowsError(::GetLastError());
+
+ // If there's insufficient space, the return value is the required size in
+ // characters *including* the null terminator, and therefore greater than
+ // the buffer size we provided. Equality would imply success with no room
+ // for the terminator and should not occur for this API.
+ assert(Len != Long16.size());
+ } while (Len > Long16.size());
+
+ // On success, GetLongPathNameW returns the number of characters not
+ // including the null-terminator.
+ Long16.truncate(Len);
+
+ // Strip \\?\ or \\?\UNC\ extended length prefix if it wasn't part of the
+ // original path.
+ wchar_t *Data = Long16.data();
+ if (!HadPrefix)
+ stripExtendedPrefix(Data, Len);
+
+ return sys::windows::UTF16ToUTF8(Data, Len, Result8);
+}
+
} // end namespace windows
namespace fs {
@@ -407,16 +478,7 @@ static std::error_code realPathFromHandle(HANDLE H,
// paths don't get canonicalized by file APIs.
wchar_t *Data = Buffer.data();
DWORD CountChars = Buffer.size();
- if (CountChars >= 8 && ::memcmp(Data, L"\\\\?\\UNC\\", 16) == 0) {
- // Convert \\?\UNC\foo\bar to \\foo\bar
- CountChars -= 6;
- Data += 6;
- Data[0] = '\\';
- } else if (CountChars >= 4 && ::memcmp(Data, L"\\\\?\\", 8) == 0) {
- // Convert \\?\c:\foo to c:\foo
- CountChars -= 4;
- Data += 4;
- }
+ stripExtendedPrefix(Data, CountChars);
// Convert the result from UTF-16 to UTF-8.
if (std::error_code EC = UTF16ToUTF8(Data, CountChars, RealPath))
diff --git a/llvm/unittests/Support/Path.cpp b/llvm/unittests/Support/Path.cpp
index 7f22b7c2edb4b..cef4d66ce8b26 100644
--- a/llvm/unittests/Support/Path.cpp
+++ b/llvm/unittests/Support/Path.cpp
@@ -32,6 +32,7 @@
#include "llvm/ADT/ArrayRef.h"
#include "llvm/Support/Chrono.h"
#include "llvm/Support/Windows/WindowsSupport.h"
+#include <fileapi.h>
#include <windows.h>
#include <winerror.h>
#endif
@@ -2485,6 +2486,138 @@ TEST_F(FileSystemTest, widenPath) {
#endif
#ifdef _WIN32
+/// Checks whether short 8.3 form names are enabled in the given UTF-8 path.
+/// This is the best way I have found of checking this. Note that short 8.3 form
+/// names can be enabled and disabled on a per-folder, per-volume, and
+/// per-system basis.
+static bool areShortNamesEnabled(llvm::StringRef Path8) {
+ // Create a directory under Path8 with a name long enough that Windows will
+ // provide a short 8.3 form name, if short 8.3 form names are enabled.
+ SmallString<256> Dir(Path8);
+ path::append(Dir, "verylongdir");
+ if (std::error_code EC = fs::create_directories(Dir))
+ return false;
+
+ // Check if the expected short 8.3 form name was created.
+ SmallString<256> Short(Path8);
+ path::append(Short, "verylo~1");
+ bool Result = fs::is_directory(Short);
+
+ fs::remove_directories(Dir);
+ return Result;
+}
+
+/// Returns the short 8.3 form path for the given UTF-8 path, or an empty string
+/// on failure. Uses Win32 GetShortPathNameW.
+static std::string getShortPathName(llvm::StringRef Path8) {
+ // Convert UTF-8 to UTF-16.
+ SmallVector<wchar_t, MAX_PATH> Path16;
+ if (std::error_code EC = sys::windows::widenPath(Path8, Path16))
+ return {};
+
+ // Get the required buffer size for the short 8.3 form path (includes null
+ // terminator).
+ DWORD Required = ::GetShortPathNameW(Path16.data(), nullptr, 0);
+ if (Required == 0)
+ return {};
+
+ SmallVector<wchar_t, MAX_PATH> ShortPath;
+ ShortPath.resize_for_overwrite(Required);
+
+ DWORD Written =
+ ::GetShortPathNameW(Path16.data(), ShortPath.data(), Required);
+ if (Written == 0 || Written >= Required)
+ return {};
+
+ ShortPath.truncate(Written);
+
+ SmallString<128> Utf8Result;
+ if (std::error_code EC = sys::windows::UTF16ToUTF8(
+ ShortPath.data(), ShortPath.size(), Utf8Result))
+ return {};
+
+ return std::string(Utf8Result);
+}
+
+/// Returns true if the two paths refer to the same file or directory by
+/// comparing their UniqueIDs.
+static bool sameEntity(llvm::StringRef P1, llvm::StringRef P2) {
+ fs::UniqueID ID1, ID2;
+ return !fs::getUniqueID(P1, ID1) && !fs::getUniqueID(P2, ID2) && ID1 == ID2;
+}
+
+/// Removes the Windows extended path prefix (\\?\ or \\?\UNC\) from the given
+/// UTF-8 path, if present.
+static std::string stripPrefix(llvm::StringRef P) {
+ if (P.starts_with(R"(\\?\UNC\)"))
+ return "\\" + P.drop_front(7).str();
+ if (P.starts_with(R"(\\?\)"))
+ return P.drop_front(4).str();
+ return P.str();
+}
+
+TEST_F(FileSystemTest, makeLongFormPath) {
+ if (!areShortNamesEnabled(TestDirectory.str()))
+ GTEST_SKIP() << "Short 8.3 form names not enabled in: " << TestDirectory;
+
+ // Setup: A test directory longer than 8 characters for which a distinct
+ // short 8.3 form name will be created on Windows. Typically, 123456~1.
+ constexpr const char *OneDir = "\\123456789"; // >8 chars
+
+ // Setup: Create a path where even if all components were reduced to short 8.3
+ // form names, the total length would exceed MAX_PATH.
+ SmallString<MAX_PATH * 2> Deep(TestDirectory);
+ const size_t NLevels = (MAX_PATH / 8) + 1;
+ for (size_t I = 0; I < NLevels; ++I)
+ Deep.append(OneDir);
+
+ ASSERT_NO_ERROR(fs::create_directories(Deep));
+
+ // Setup: Create prefixed and non-prefixed short 8.3 form paths from the deep
+ // test path we just created.
+ std::string DeepShortWithPrefix = getShortPathName(Deep);
+ ASSERT_TRUE(StringRef(DeepShortWithPrefix).starts_with(R"(\\?\)"))
+ << "Expected prefixed short 8.3 form path, got: " << DeepShortWithPrefix;
+ std::string DeepShort = stripPrefix(DeepShortWithPrefix);
+
+ // Setup: Create a short 8.3 form path for the first-level directory.
+ SmallString<256> FirstLevel(TestDirectory);
+ FirstLevel.append(OneDir);
+ std::string Short = getShortPathName(FirstLevel);
+ ASSERT_FALSE(Short.empty())
+ << "Expected short 8.3 form path for test directory.";
+
+ // Case 1: Non-existent short 8.3 form path.
+ SmallString<128> NoExist("NotEre~1");
+ ASSERT_FALSE(fs::exists(NoExist));
+ SmallString<128> NoExistResult;
+ EXPECT_TRUE(windows::makeLongFormPath(NoExist, NoExistResult));
+ EXPECT_TRUE(NoExistResult.empty());
+
+ // Case 2: Valid short 8.3 form path.
+ SmallString<128> ShortResult;
+ ASSERT_FALSE(windows::makeLongFormPath(Short, ShortResult));
+ EXPECT_TRUE(sameEntity(Short, ShortResult));
+
+ // Case 3: Deep short 8.3 form path without \\?\ prefix.
+ SmallString<128> DeepResult;
+ ASSERT_FALSE(windows::makeLongFormPath(DeepShort, DeepResult));
+ EXPECT_TRUE(sameEntity(DeepShort, DeepResult));
+ EXPECT_FALSE(StringRef(DeepResult).starts_with(R"(\\?\)"))
+ << "Expected unprefixed result, got: " << DeepResult;
+
+ // Case 4: Deep short 8.3 form path with \\?\ prefix.
+ SmallString<128> DeepPrefixedResult;
+ ASSERT_FALSE(
+ windows::makeLongFormPath(DeepShortWithPrefix, DeepPrefixedResult));
+ EXPECT_TRUE(sameEntity(DeepShortWithPrefix, DeepPrefixedResult));
+ EXPECT_TRUE(StringRef(DeepPrefixedResult).starts_with(R"(\\?\)"))
+ << "Expected prefixed result, got: " << DeepPrefixedResult;
+
+ // Cleanup.
+ ASSERT_NO_ERROR(fs::remove_directories(TestDirectory.str()));
+}
+
// Windows refuses lock request if file region is already locked by the same
// process. POSIX system in this case updates the existing lock.
TEST_F(FileSystemTest, FileLocker) {
>From f9c559c35efb99749f395ad2aab628b7d332529a Mon Sep 17 00:00:00 2001
From: Ben <ben.dunbobbin at sony.com>
Date: Mon, 2 Feb 2026 15:39:43 +0000
Subject: [PATCH 2/7] Do not assume the 8.3 name for "verylongdir"
---
llvm/unittests/Support/Path.cpp | 31 +++++++++++++++++++------------
1 file changed, 19 insertions(+), 12 deletions(-)
diff --git a/llvm/unittests/Support/Path.cpp b/llvm/unittests/Support/Path.cpp
index cef4d66ce8b26..4408679737297 100644
--- a/llvm/unittests/Support/Path.cpp
+++ b/llvm/unittests/Support/Path.cpp
@@ -2487,24 +2487,28 @@ TEST_F(FileSystemTest, widenPath) {
#ifdef _WIN32
/// Checks whether short 8.3 form names are enabled in the given UTF-8 path.
-/// This is the best way I have found of checking this. Note that short 8.3 form
-/// names can be enabled and disabled on a per-folder, per-volume, and
-/// per-system basis.
-static bool areShortNamesEnabled(llvm::StringRef Path8) {
+static llvm::Expected<bool> areShortNamesEnabled(llvm::StringRef Path8) {
// Create a directory under Path8 with a name long enough that Windows will
// provide a short 8.3 form name, if short 8.3 form names are enabled.
SmallString<256> Dir(Path8);
path::append(Dir, "verylongdir");
if (std::error_code EC = fs::create_directories(Dir))
- return false;
+ return llvm::errorCodeToError(EC);
+ scope_exit Close([&] { fs::remove_directories(Dir); });
- // Check if the expected short 8.3 form name was created.
- SmallString<256> Short(Path8);
- path::append(Short, "verylo~1");
- bool Result = fs::is_directory(Short);
+ SmallVector<wchar_t, MAX_PATH> Path16;
+ if (std::error_code EC = sys::windows::widenPath(Dir, Path16))
+ return llvm::errorCodeToError(EC);
+
+ WIN32_FIND_DATAW Data;
+ HANDLE H = ::FindFirstFileW(Path16.data(), &Data);
+ if (H == INVALID_HANDLE_VALUE)
+ return llvm::make_error<llvm::StringError>(
+ "FindFirstFileW returned invalid handle",
+ llvm::inconvertibleErrorCode());
+ ::FindClose(H);
- fs::remove_directories(Dir);
- return Result;
+ return (Data.cAlternateFileName[0] != L'\0');
}
/// Returns the short 8.3 form path for the given UTF-8 path, or an empty string
@@ -2557,7 +2561,10 @@ static std::string stripPrefix(llvm::StringRef P) {
}
TEST_F(FileSystemTest, makeLongFormPath) {
- if (!areShortNamesEnabled(TestDirectory.str()))
+ auto Enabled = areShortNamesEnabled(TestDirectory.str());
+ ASSERT_TRUE(static_cast<bool>(Enabled))
+ << llvm::toString(Enabled.takeError());
+ if (!*Enabled)
GTEST_SKIP() << "Short 8.3 form names not enabled in: " << TestDirectory;
// Setup: A test directory longer than 8 characters for which a distinct
>From 43c32d5da064727667398941262341e82609cc48 Mon Sep 17 00:00:00 2001
From: Ben <ben.dunbobbin at sony.com>
Date: Mon, 2 Feb 2026 22:19:28 +0000
Subject: [PATCH 3/7] Use long path terminology.
Additionally:
- Add a '.' and '..' test case.
- Use MAX_PATH consistently for SmallStrings in the new test code.
---
llvm/lib/Support/Windows/Path.inc | 51 +++++++++++++++----------------
llvm/unittests/Support/Path.cpp | 42 ++++++++++++++++++-------
2 files changed, 56 insertions(+), 37 deletions(-)
diff --git a/llvm/lib/Support/Windows/Path.inc b/llvm/lib/Support/Windows/Path.inc
index f58b54c3afb0c..d1dd054fdd829 100644
--- a/llvm/lib/Support/Windows/Path.inc
+++ b/llvm/lib/Support/Windows/Path.inc
@@ -60,29 +60,28 @@ static bool is_separator(const wchar_t value) {
}
}
-// Extended-length path prefix constants (UTF-8).
-static constexpr llvm::StringLiteral ExtendedPrefix8 = R"(\\?\)";
-static constexpr llvm::StringLiteral ExtendedUNCPrefix8 = R"(\\?\UNC\)";
-
-// Extended-length path prefix constants (UTF-16).
-static constexpr wchar_t ExtendedPrefix16[] = LR"(\\?\)";
-static constexpr wchar_t ExtendedUNCPathPrefix16[] = LR"(\\?\UNC\)";
-
-static constexpr DWORD ExtendedPrefix16Len =
- static_cast<DWORD>(std::size(ExtendedPrefix16) - 1);
-static constexpr DWORD ExtendedUNCPathPrefix16Len =
- static_cast<DWORD>(std::size(ExtendedUNCPathPrefix16) - 1);
-
-static void stripExtendedPrefix(wchar_t *&Data, DWORD &CountChars) {
- if (CountChars >= ExtendedUNCPathPrefix16Len &&
- ::wmemcmp(Data, ExtendedUNCPathPrefix16, ExtendedUNCPathPrefix16Len) ==
- 0) {
+// Long path path prefix constants (UTF-8).
+static constexpr llvm::StringLiteral LongPathPrefix8 = R"(\\?\)";
+static constexpr llvm::StringLiteral LongPathUNCPrefix8 = R"(\\?\UNC\)";
+
+// Long path prefix constants (UTF-16).
+static constexpr wchar_t LongPathPrefix16[] = LR"(\\?\)";
+static constexpr wchar_t LongPathUNCPrefix16[] = LR"(\\?\UNC\)";
+
+static constexpr DWORD LongPathPrefix16Len =
+ static_cast<DWORD>(std::size(LongPathPrefix16) - 1);
+static constexpr DWORD LongPathUNCPrefix16Len =
+ static_cast<DWORD>(std::size(LongPathUNCPrefix16) - 1);
+
+static void stripLongPathPrefix(wchar_t *&Data, DWORD &CountChars) {
+ if (CountChars >= LongPathUNCPrefix16Len &&
+ ::wmemcmp(Data, LongPathUNCPrefix16, LongPathUNCPrefix16Len) == 0) {
// Convert \\?\UNC\foo\bar to \\foo\bar
CountChars -= 6;
Data += 6;
Data[0] = L'\\';
- } else if (CountChars >= ExtendedPrefix16Len &&
- ::wmemcmp(Data, ExtendedPrefix16, ExtendedPrefix16Len) == 0) {
+ } else if (CountChars >= LongPathPrefix16Len &&
+ ::wmemcmp(Data, LongPathPrefix16, LongPathPrefix16Len) == 0) {
// Convert \\?\C:\foo to C:\foo
CountChars -= 4;
Data += 4;
@@ -125,7 +124,7 @@ std::error_code widenPath(const Twine &Path8, SmallVectorImpl<wchar_t> &Path16,
}
if ((Path16.size() + CurPathLen) < MaxPathLen ||
- Path8Str.starts_with(ExtendedPrefix8))
+ Path8Str.starts_with(LongPathPrefix8))
return std::error_code();
if (!IsAbsolute) {
@@ -145,10 +144,10 @@ std::error_code widenPath(const Twine &Path8, SmallVectorImpl<wchar_t> &Path16,
SmallString<2 * MAX_PATH> FullPath;
if (RootName[1] != ':') { // Check if UNC.
- FullPath.append(ExtendedUNCPrefix8);
+ FullPath.append(LongPathUNCPrefix8);
FullPath.append(Path8Str.begin() + 2, Path8Str.end());
} else {
- FullPath.append(ExtendedPrefix8);
+ FullPath.append(LongPathPrefix8);
FullPath.append(Path8Str);
}
@@ -159,7 +158,7 @@ std::error_code makeLongFormPath(const Twine &Path8,
llvm::SmallVectorImpl<char> &Result8) {
SmallString<128> PathStorage;
StringRef PathStr = Path8.toStringRef(PathStorage);
- bool HadPrefix = PathStr.starts_with(ExtendedPrefix8);
+ bool HadPrefix = PathStr.starts_with(LongPathPrefix8);
SmallVector<wchar_t, 128> Path16;
if (std::error_code EC = widenPath(PathStr, Path16))
@@ -189,11 +188,11 @@ std::error_code makeLongFormPath(const Twine &Path8,
// including the null-terminator.
Long16.truncate(Len);
- // Strip \\?\ or \\?\UNC\ extended length prefix if it wasn't part of the
+ // Strip \\?\ or \\?\UNC\ long length prefix if it wasn't part of the
// original path.
wchar_t *Data = Long16.data();
if (!HadPrefix)
- stripExtendedPrefix(Data, Len);
+ stripLongPathPrefix(Data, Len);
return sys::windows::UTF16ToUTF8(Data, Len, Result8);
}
@@ -478,7 +477,7 @@ static std::error_code realPathFromHandle(HANDLE H,
// paths don't get canonicalized by file APIs.
wchar_t *Data = Buffer.data();
DWORD CountChars = Buffer.size();
- stripExtendedPrefix(Data, CountChars);
+ stripLongPathPrefix(Data, CountChars);
// Convert the result from UTF-16 to UTF-8.
if (std::error_code EC = UTF16ToUTF8(Data, CountChars, RealPath))
diff --git a/llvm/unittests/Support/Path.cpp b/llvm/unittests/Support/Path.cpp
index 4408679737297..689dfe85aeb73 100644
--- a/llvm/unittests/Support/Path.cpp
+++ b/llvm/unittests/Support/Path.cpp
@@ -2490,7 +2490,7 @@ TEST_F(FileSystemTest, widenPath) {
static llvm::Expected<bool> areShortNamesEnabled(llvm::StringRef Path8) {
// Create a directory under Path8 with a name long enough that Windows will
// provide a short 8.3 form name, if short 8.3 form names are enabled.
- SmallString<256> Dir(Path8);
+ SmallString<MAX_PATH> Dir(Path8);
path::append(Dir, "verylongdir");
if (std::error_code EC = fs::create_directories(Dir))
return llvm::errorCodeToError(EC);
@@ -2535,7 +2535,7 @@ static std::string getShortPathName(llvm::StringRef Path8) {
ShortPath.truncate(Written);
- SmallString<128> Utf8Result;
+ SmallString<MAX_PATH> Utf8Result;
if (std::error_code EC = sys::windows::UTF16ToUTF8(
ShortPath.data(), ShortPath.size(), Utf8Result))
return {};
@@ -2550,7 +2550,7 @@ static bool sameEntity(llvm::StringRef P1, llvm::StringRef P2) {
return !fs::getUniqueID(P1, ID1) && !fs::getUniqueID(P2, ID2) && ID1 == ID2;
}
-/// Removes the Windows extended path prefix (\\?\ or \\?\UNC\) from the given
+/// Removes the Windows long path path prefix (\\?\ or \\?\UNC\) from the given
/// UTF-8 path, if present.
static std::string stripPrefix(llvm::StringRef P) {
if (P.starts_with(R"(\\?\UNC\)"))
@@ -2588,33 +2588,53 @@ TEST_F(FileSystemTest, makeLongFormPath) {
std::string DeepShort = stripPrefix(DeepShortWithPrefix);
// Setup: Create a short 8.3 form path for the first-level directory.
- SmallString<256> FirstLevel(TestDirectory);
+ SmallString<MAX_PATH> FirstLevel(TestDirectory);
FirstLevel.append(OneDir);
std::string Short = getShortPathName(FirstLevel);
ASSERT_FALSE(Short.empty())
<< "Expected short 8.3 form path for test directory.";
+ // Setup: Create a short 8.3 form path with . and .. components for the
+ // first-level directory.
+ llvm::SmallString<MAX_PATH> WithDots(FirstLevel);
+ llvm::sys::path::append(WithDots, ".", "..", OneDir);
+ std::string DotAndDotDot = getShortPathName(WithDots);
+ ASSERT_FALSE(DotAndDotDot.empty())
+ << "Expected short 8.3 form path for test directory.";
+ auto ContainsDotAndDotDot = [](llvm::StringRef S) {
+ return S.contains("\\.\\") && S.contains("\\..\\");
+ };
+ ASSERT_TRUE(ContainsDotAndDotDot(DotAndDotDot))
+ << "Expected '.' and '..' components in: " << DotAndDotDot;
+
// Case 1: Non-existent short 8.3 form path.
- SmallString<128> NoExist("NotEre~1");
+ SmallString<MAX_PATH> NoExist("NotEre~1");
ASSERT_FALSE(fs::exists(NoExist));
- SmallString<128> NoExistResult;
+ SmallString<MAX_PATH> NoExistResult;
EXPECT_TRUE(windows::makeLongFormPath(NoExist, NoExistResult));
EXPECT_TRUE(NoExistResult.empty());
// Case 2: Valid short 8.3 form path.
- SmallString<128> ShortResult;
+ SmallString<MAX_PATH> ShortResult;
ASSERT_FALSE(windows::makeLongFormPath(Short, ShortResult));
EXPECT_TRUE(sameEntity(Short, ShortResult));
- // Case 3: Deep short 8.3 form path without \\?\ prefix.
- SmallString<128> DeepResult;
+ // Case 3: Valid . and .. short 8.3 form path.
+ SmallString<MAX_PATH> DotAndDotDotResult;
+ ASSERT_FALSE(windows::makeLongFormPath(DotAndDotDot, DotAndDotDotResult));
+ EXPECT_TRUE(sameEntity(DotAndDotDot, DotAndDotDotResult));
+ // Assert that '.' and '..' remain as path components.
+ ASSERT_TRUE(ContainsDotAndDotDot(DotAndDotDotResult));
+
+ // Case 4: Deep short 8.3 form path without \\?\ prefix.
+ SmallString<MAX_PATH> DeepResult;
ASSERT_FALSE(windows::makeLongFormPath(DeepShort, DeepResult));
EXPECT_TRUE(sameEntity(DeepShort, DeepResult));
EXPECT_FALSE(StringRef(DeepResult).starts_with(R"(\\?\)"))
<< "Expected unprefixed result, got: " << DeepResult;
- // Case 4: Deep short 8.3 form path with \\?\ prefix.
- SmallString<128> DeepPrefixedResult;
+ // Case 5: Deep short 8.3 form path with \\?\ prefix.
+ SmallString<MAX_PATH> DeepPrefixedResult;
ASSERT_FALSE(
windows::makeLongFormPath(DeepShortWithPrefix, DeepPrefixedResult));
EXPECT_TRUE(sameEntity(DeepShortWithPrefix, DeepPrefixedResult));
>From e1f82d5c009cfbf0cd1cf77831c3e7e3ddf36be4 Mon Sep 17 00:00:00 2001
From: Ben <ben.dunbobbin at sony.com>
Date: Mon, 2 Feb 2026 23:13:14 +0000
Subject: [PATCH 4/7] Add TOCTOU comment
---
llvm/lib/Support/Windows/Path.inc | 1 +
1 file changed, 1 insertion(+)
diff --git a/llvm/lib/Support/Windows/Path.inc b/llvm/lib/Support/Windows/Path.inc
index d1dd054fdd829..97dd75a74d18c 100644
--- a/llvm/lib/Support/Windows/Path.inc
+++ b/llvm/lib/Support/Windows/Path.inc
@@ -168,6 +168,7 @@ std::error_code makeLongFormPath(const Twine &Path8,
llvm::SmallVector<wchar_t, 128> Long16;
DWORD Len = static_cast<DWORD>(Path16.size());
+ // Loop instead of a double call to be defensive against TOCTOU races.
do {
Long16.resize_for_overwrite(Len);
>From 2bc5f142f8d3f6fc7ecbfcf59448af08c742ba79 Mon Sep 17 00:00:00 2001
From: Ben <ben.dunbobbin at sony.com>
Date: Tue, 10 Feb 2026 00:43:55 +0000
Subject: [PATCH 5/7] Ensure path is NUL-terminated before calling
FindFirstFileW
---
llvm/unittests/Support/Path.cpp | 1 +
1 file changed, 1 insertion(+)
diff --git a/llvm/unittests/Support/Path.cpp b/llvm/unittests/Support/Path.cpp
index 689dfe85aeb73..54a8b87182efc 100644
--- a/llvm/unittests/Support/Path.cpp
+++ b/llvm/unittests/Support/Path.cpp
@@ -2499,6 +2499,7 @@ static llvm::Expected<bool> areShortNamesEnabled(llvm::StringRef Path8) {
SmallVector<wchar_t, MAX_PATH> Path16;
if (std::error_code EC = sys::windows::widenPath(Dir, Path16))
return llvm::errorCodeToError(EC);
+ Path16.push_back(L'\0');
WIN32_FIND_DATAW Data;
HANDLE H = ::FindFirstFileW(Path16.data(), &Data);
>From d94058adca9c0f1c7ef90df07246db44da520bad Mon Sep 17 00:00:00 2001
From: Ben <ben.dunbobbin at sony.com>
Date: Tue, 10 Feb 2026 11:34:01 +0000
Subject: [PATCH 6/7] Revert "Ensure path is NUL-terminated before calling
FindFirstFileW"
This reverts commit 2bc5f142f8d3f6fc7ecbfcf59448af08c742ba79.
This is not needed as widenPath does guarantee null termination.
---
llvm/unittests/Support/Path.cpp | 1 -
1 file changed, 1 deletion(-)
diff --git a/llvm/unittests/Support/Path.cpp b/llvm/unittests/Support/Path.cpp
index 54a8b87182efc..689dfe85aeb73 100644
--- a/llvm/unittests/Support/Path.cpp
+++ b/llvm/unittests/Support/Path.cpp
@@ -2499,7 +2499,6 @@ static llvm::Expected<bool> areShortNamesEnabled(llvm::StringRef Path8) {
SmallVector<wchar_t, MAX_PATH> Path16;
if (std::error_code EC = sys::windows::widenPath(Dir, Path16))
return llvm::errorCodeToError(EC);
- Path16.push_back(L'\0');
WIN32_FIND_DATAW Data;
HANDLE H = ::FindFirstFileW(Path16.data(), &Data);
>From e9786a69525686d71183b9909f75a2003fa840b9 Mon Sep 17 00:00:00 2001
From: Ben <ben.dunbobbin at sony.com>
Date: Tue, 10 Feb 2026 12:24:13 +0000
Subject: [PATCH 7/7] Be more explicit about FindFirstFileW error condition
---
llvm/unittests/Support/Path.cpp | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/llvm/unittests/Support/Path.cpp b/llvm/unittests/Support/Path.cpp
index 689dfe85aeb73..08c0c55404d29 100644
--- a/llvm/unittests/Support/Path.cpp
+++ b/llvm/unittests/Support/Path.cpp
@@ -32,6 +32,7 @@
#include "llvm/ADT/ArrayRef.h"
#include "llvm/Support/Chrono.h"
#include "llvm/Support/Windows/WindowsSupport.h"
+#include "llvm/Support/WindowsError.h"
#include <fileapi.h>
#include <windows.h>
#include <winerror.h>
@@ -2503,9 +2504,7 @@ static llvm::Expected<bool> areShortNamesEnabled(llvm::StringRef Path8) {
WIN32_FIND_DATAW Data;
HANDLE H = ::FindFirstFileW(Path16.data(), &Data);
if (H == INVALID_HANDLE_VALUE)
- return llvm::make_error<llvm::StringError>(
- "FindFirstFileW returned invalid handle",
- llvm::inconvertibleErrorCode());
+ return llvm::errorCodeToError(llvm::mapWindowsError(::GetLastError()));
::FindClose(H);
return (Data.cAlternateFileName[0] != L'\0');
More information about the llvm-commits
mailing list