[llvm-branch-commits] [llvm] 86407b6 - [Windows][Support] Add helper to expand short 8.3 form paths (#178480)
Cullen Rhodes via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Wed Feb 18 00:19:28 PST 2026
Author: Ben Dunbobbin
Date: 2026-02-18T08:19:19Z
New Revision: 86407b6e65fcf2fe230f27cd0a2a71ec71a178d1
URL: https://github.com/llvm/llvm-project/commit/86407b6e65fcf2fe230f27cd0a2a71ec71a178d1
DIFF: https://github.com/llvm/llvm-project/commit/86407b6e65fcf2fe230f27cd0a2a71ec71a178d1.diff
LOG: [Windows][Support] Add helper to expand short 8.3 form paths (#178480)
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 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()`.
- `widenPath()` is cleaned up to use shared prefix constants.
This was split out from #178303 at the request of the codeowner so that
the Windows support parts can be reviewed separately.
(cherry picked from commit e6f5e4910df519a3f14e0db86d24abe8fd25082b)
Added:
Modified:
llvm/include/llvm/Support/Windows/WindowsSupport.h
llvm/lib/Support/Windows/Path.inc
llvm/unittests/Support/Path.cpp
Removed:
################################################################################
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..97dd75a74d18c 100644
--- a/llvm/lib/Support/Windows/Path.inc
+++ b/llvm/lib/Support/Windows/Path.inc
@@ -60,6 +60,34 @@ static bool is_separator(const wchar_t value) {
}
}
+// 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 >= LongPathPrefix16Len &&
+ ::wmemcmp(Data, LongPathPrefix16, LongPathPrefix16Len) == 0) {
+ // Convert \\?\C:\foo to C:\foo
+ CountChars -= 4;
+ Data += 4;
+ }
+}
+
namespace llvm {
namespace sys {
namespace windows {
@@ -95,10 +123,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(LongPathPrefix8))
return std::error_code();
if (!IsAbsolute) {
@@ -116,17 +142,62 @@ 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(LongPathUNCPrefix8);
FullPath.append(Path8Str.begin() + 2, Path8Str.end());
} else {
+ FullPath.append(LongPathPrefix8);
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(LongPathPrefix8);
+
+ 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());
+
+ // Loop instead of a double call to be defensive against TOCTOU races.
+ 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\ long length prefix if it wasn't part of the
+ // original path.
+ wchar_t *Data = Long16.data();
+ if (!HadPrefix)
+ stripLongPathPrefix(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;
- }
+ 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 7f22b7c2edb4b..08c0c55404d29 100644
--- a/llvm/unittests/Support/Path.cpp
+++ b/llvm/unittests/Support/Path.cpp
@@ -32,6 +32,8 @@
#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>
#endif
@@ -2485,6 +2487,163 @@ TEST_F(FileSystemTest, widenPath) {
#endif
#ifdef _WIN32
+/// Checks whether short 8.3 form names are enabled in the given UTF-8 path.
+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<MAX_PATH> Dir(Path8);
+ path::append(Dir, "verylongdir");
+ if (std::error_code EC = fs::create_directories(Dir))
+ return llvm::errorCodeToError(EC);
+ scope_exit Close([&] { fs::remove_directories(Dir); });
+
+ 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::errorCodeToError(llvm::mapWindowsError(::GetLastError()));
+ ::FindClose(H);
+
+ return (Data.cAlternateFileName[0] != L'\0');
+}
+
+/// 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<MAX_PATH> 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 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\)"))
+ return "\\" + P.drop_front(7).str();
+ if (P.starts_with(R"(\\?\)"))
+ return P.drop_front(4).str();
+ return P.str();
+}
+
+TEST_F(FileSystemTest, makeLongFormPath) {
+ 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
+ // 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<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<MAX_PATH> NoExist("NotEre~1");
+ ASSERT_FALSE(fs::exists(NoExist));
+ SmallString<MAX_PATH> NoExistResult;
+ EXPECT_TRUE(windows::makeLongFormPath(NoExist, NoExistResult));
+ EXPECT_TRUE(NoExistResult.empty());
+
+ // Case 2: Valid short 8.3 form path.
+ SmallString<MAX_PATH> ShortResult;
+ ASSERT_FALSE(windows::makeLongFormPath(Short, ShortResult));
+ EXPECT_TRUE(sameEntity(Short, ShortResult));
+
+ // 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 5: Deep short 8.3 form path with \\?\ prefix.
+ SmallString<MAX_PATH> 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) {
More information about the llvm-branch-commits
mailing list