MAX_PATH 與 Windows 路徑・檔案名稱的陷阱 ── 260 字元限制、保留名稱、結尾句點、大小寫

· · MAX_PATH, 檔案路徑, 長路徑, 檔案名稱, NTFS, Win32, C#, .NET, Windows 開發, 故障調查, 技術諮詢

「只有在使用者的裝置上才會出現『找不到檔案』」「檔案明明可以複製,卻打不開」── 在牽涉檔案輸出入的業務應用程式故障調查中,追根究底發現原因出在路徑長度或檔案名稱本身,這種案例並不罕見。使用者會把案件名稱、日期塞進資料夾名稱裡,把階層挖得很深,輕易超出我們原本的預期。

麻煩的是,這個領域的限制分成「Win32 API 的限制」「檔案系統的限制」「殼層(檔案總管)的限制」「.NET 執行環境的限制」等好幾層,很難搞清楚哪些能因應、哪些終究無法解決。本文將從 MAX_PATH=260 字元的真面目開始,整理合法處理長路徑的條件、CON 等保留名稱與結尾句點這類檔案名稱的陷阱,一直到大小寫的處理方式,並搭配 C# 的實務因應方式一併說明。

1. 先講結論

  • MAX_PATH=260 是包含「磁碟機代號+冒號+反斜線+最多 256 字元的路徑字串+結尾 NUL」的 Win32 API 限制。檔案系統端(NTFS 等)可以處理更長的路徑,只要對 Unicode 版 API 加上 \\?\ 前綴,總長度最多可指定約 32,767 字元。12
  • 要解除 260 字元限制,在 Windows 10 版本 1607 以後,必須同時具備「登錄的 LongPathsEnabled=1」與「應用程式資訊清單的 longPathAware」兩者。只設定其中一項並不會生效。3
  • .NET(Core)/.NET 5 以後的執行環境不會進行 MAX_PATH 檢查,會隱含地處理長路徑。.NET Framework 若以 4.6.2 以後為目標,則會解除執行環境端的 260 字元檢查。45
  • 不過現實中仍存在不支援長路徑的應用程式。官方文件本身明確指出,Win32 API 可以建立的路徑,殼層(檔案總管)有時無法正確解讀。1
  • 檔案名稱不能使用 < > : " / \ | ? * 及控制字元(0~31),CON・PRN・AUX・NUL・COM1~9・LPT1~9 即使加上副檔名(如 CON.txt)也會被視為保留名稱6
  • 名稱結尾的空格與句點,會在路徑正規化過程中被靜默移除。這正是「原本想指定『hoge.』卻變成『hoge』」、「無法從 Windows 存取其他作業系統建立的結尾帶空格檔案」等事故的原因。67
  • Windows 檔案名稱預設是「保留大小寫形式,但不區分大小寫」。NTFS 也支援以目錄為單位進行區分(fsutil.exe file setCaseSensitiveInfo),但會有 Windows 應用程式端無法配合的副作用。89
  • 在實作層面,路徑串接時要注意 Path.Combine「傳入含根目錄的引數時,前段會被捨棄」的規格(.NET Core 系列的話,Path.Join 也是一個選項),使用者輸入的檔案名稱則以 Path.GetInvalidFileNameChars + 保留名稱・結尾字元的自訂檢查進行清理。1011

2. MAX_PATH=260 的真面目

Win32 API 中路徑的最大長度,除了部分例外,被定義為 MAX_PATH=260 字元。這 260 個字元有其組成內容。本機路徑由「磁碟機代號、冒號、反斜線、以反斜線分隔的名稱部分、結尾的 NUL 字元」構成,例如 D 磁碟機的話,最大值就是「D:\ + 256 字元份的路徑字串 + 結尾 NUL」。1

拆解組成內容如下。

位置 構成要素 範例 字元數
開頭 磁碟機代號+冒號+反斜線 D:\ 3
中間 以反斜線分隔的名稱部分(資料夾名稱・檔案名稱與分隔符號) 2026\案件\…\報告書.xlsx 最多 256
結尾 結尾的 NUL 字元(畫面上看不到) 1
  合計   260 = MAX_PATH

也就是說,如果把 260 字元想成「可用於檔案名稱的長度」,實際上磁碟機標示與結尾 NUL 就已經佔掉了 4 個字元。此外還有更細部的限制:因為建立目錄的 API 要求後面必須留有可附加 8.3 格式檔案名稱的餘裕,目錄的路徑不能超過 MAX_PATH−12 字元1

重要的是,這是 Win32 API 這一層的限制,而不是檔案系統的極限。NTFS 支援長檔案名稱與擴充路徑,許多 Win32 函式的 Unicode 版本可以接受總長度約 32,767 字元的擴充長路徑。構成路徑的個別元件(單一資料夾名稱・檔案名稱)的上限,則以 GetVolumeInformation 傳回的數值為準,一般是 255 字元。12

這種「API 是 260,檔案系統卻約有 32,767」的落差,正是現場故障的根源。用某個工具建立得出的路徑,換到另一個工具(或自家應用程式)卻打不開,這種不對稱狀況會正當地發生。用 git clone 把層級很深的儲存庫展開到名稱很長的資料夾裡,結果建置失敗,這是官方文件中也記載的典型案例。1

另外,在舊版 .NET Framework 中,只要完整路徑達到 260 字元以上,就會擲回 System.IO.PathTooLongException。看到這個例外時,請先懷疑路徑長度。12

3. 突破 260 字元高牆的方法及其條件

處理長路徑的手段大致有兩種,「\\?\ 前綴」與「OS 端長路徑啟用」。

3.1. \\?\ 前綴

在路徑字串開頭加上 \\?\,Win32 API 就會停止對字串進行解析,直接將其傳給檔案系統。這樣就能突破 MAX_PATH 的限制(UNC 路徑則是 \\?\UNC\server\share 的形式)。不過這有其條件與副作用。16

  • 必須是 Unicode 版 API(如 ~W 這類以 UTF-16 呼叫的 API,或 .NET)。
  • 由於會跳過正規化處理,無法使用 / 分隔或 ... 這類相對指定方式。由於相對路徑無法附加 \\?\,因此相對路徑始終受限於 MAX_PATH 以內1
  • 並非所有 API 都支援,是否支援需要在各個 API 的參考文件中確認。6

3.2. Windows 10 1607 以後的長路徑啟用 ── 條件是「兩者兼備」

在 Windows 10 版本 1607 以後,可以在許多常見的 Win32 檔案・目錄函式(CreateFileW、FindFirstFileW、GetFileAttributesW 等)中解除 MAX_PATH 限制。不過前提是應用程式端要主動選用,必須同時滿足以下兩個條件3

  1. 登錄值 HKLM\SYSTEM\CurrentControlSet\Control\FileSystemLongPathsEnabled(REG_DWORD)必須為 1。也可以透過群組原則「電腦設定 > 系統管理範本 > 系統 > 檔案系統 > 啟用 Win32 長路徑」進行設定。
  2. 應用程式資訊清單中必須有 longPathAware 元素。
<application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
        <ws2:longPathAware>true</ws2:longPathAware>
    </windowsSettings>
</application>
# 登錄機碼端(需要系統管理員權限)
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" `
-Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force

這裡補充一下資訊清單端的步驟。上面列出的 longPathAware XML,是官方文件中刊載的片段,單獨這一段並不能構成一個檔案。實際上要放在應用程式資訊清單檔案(慣例上命名為 app.manifest)的 assembly 元素之中。若使用 Visual Studio,從「在專案上按右鍵 > 加入 > 新增項目」選擇「應用程式資訊清單檔案」加入後,會產生內含 UAC 設定等內容的範本,再於其 assembly 元素中以 application 元素的形式加入即可。13

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <!-- 這裡是 VS 產生的 trustInfo 等內容 -->
  <application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
      <ws2:longPathAware>true</ws2:longPathAware>
    </windowsSettings>
  </application>
</assembly>

把加入的項目與建置連結起來的,是 MSBuild 的 ApplicationManifest 屬性。在 Visual Studio 中加入項目時通常會自動寫入,但若直接編輯 csproj,則需要加上以下這一行(此屬性所指的資訊清單,預設會內嵌到 exe 中)。14

<PropertyGroup>
  <ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>

「明明設定了登錄值卻沒有生效」這類諮詢,多半是資訊清單端漏掉了。官方文件也再三提醒「這個登錄設定,只會對已修改為使用新功能的應用程式產生影響」。此外,登錄值會在第一次呼叫檔案函式時以處理序為單位進行快取,在處理序存續期間不會重新讀取。要確保設定變更確實反映到所有應用程式,有時需要重新啟動。3

3.3. .NET 的情況

  • .NET (Core) / .NET 5 以後:執行環境不會進行 MAX_PATH 檢查,會隱含地處理長路徑。應用程式端不需要撰寫特別的程式碼。4
  • .NET Framework:若以 4.6.2 以後為目標,就會解除 260 字元的執行環境檢查,PathTooLongException 只會限於「超過 32,767 字元」或「OS 傳回錯誤」這兩種情況。即使是以更早版本為目標的既有應用程式,也可以透過 Switch.System.IO.BlockLongPaths=false(以及停用舊版路徑處理的 Switch.System.IO.UseLegacyPathHandling=false)這個 AppContext 開關來選用此功能。512
  • .NET Framework 應用程式若要在實務上讓長路徑真正生效,除了上述執行環境設定外,還需要併用 OS 端的長路徑啟用與資訊清單。NuGet.exe 的長路徑因應文件,就把這套組態(Windows 10 1607 以後+longPathAware 資訊清單+停用 UseLegacyPathHandling)明確記載為實際範例。15

3.4. 即便如此,「不支援的應用程式」依然存在

即使做到這一步,也不代表世界上所有應用程式都能因此處理長路徑。官方文件明確指出「殼層與檔案系統的要求並不相同,Win32 API 能建立的路徑,殼層 UI 有時無法正確解讀」1,實際上,不宣稱支援長路徑的工具依然存在(例如 NuGet 的文件就記載,Visual Studio 或 msbuild -t:restore 的 restore 並不支援長路徑15)。就算自家應用程式能用長路徑建立檔案,使用者能否在檔案總管或其他工具中開啟該檔案,是另一回事。基於這種不對稱性的設計判斷,整理在第 6 章的判斷表中。

4. 不能使用的字元、保留裝置名稱,以及結尾的句點・空格

與路徑長度並列的另一個地雷區,是檔案名稱本身的規則。以下從官方命名規則中,整理出業務應用程式容易踩到的部分。6

分類 內容 備註
保留字元 < > : " / \ \| ? * 包含路徑分隔符號的 \(與 /)、磁碟機的 :
控制字元 整數值 0(NUL)與 1~31 除了替代資料流內部之外不可使用
保留裝置名稱 CON, PRN, AUX, NUL, COM1~COM9, LPT1~LPT9(以及上標數字的 COM¹~³, LPT¹~³) 即使加上副檔名也不可(NUL.txt、NUL.tar.gz 都等同於 NUL)
結尾字元 以空格或句點結尾的名稱 檔案系統即使允許,殼層與 UI 也不支援

4.1. 保留裝置名稱 ── 就算是 CON.txt 也不行

CON 和 NUL 是 MS-DOS 時代的裝置名稱,現在仍以 NT 命名空間的保留名稱形式存在。因此以一般方式無法建立名為「CON」的檔案,即使像 CON.txt 這樣加上副檔名,也會被解讀為保留名稱6 想把序列埠連動的日誌以「COM1.log」這樣的名稱儲存卻出事,或是在 Linux 端建立的「aux」資料夾無法在 Windows 中展開,這些都是實際現場中出現的樣貌。

補充說明,依路徑正規化的規格,過去像「CON」「COM1.TXT」這種以保留名稱開頭的路徑,一直被轉換為裝置路徑(\\.\CON)來解讀。Windows 11 改變了這種解讀方式,要指向舊式裝置,需要像 \\.\CON 這樣以完整格式指定。7 不過官方文件的寫法只到「Windows 11 之前」「Windows 11 中並非如此」這樣的粒度,並未標示是從哪個組建、哪個更新程式開始切換的7 在混合環境中評估影響範圍時,請認知到只能區分到「Windows 10 以前,還是 Windows 11 以後」,無法細分到組建等級。話雖如此,舊版作業系統與既有應用程式都仍維持原本的解讀方式,因此業務資料的名稱應避免使用保留名稱,這個結論並不會改變

4.2. 結尾的空格・句點會「靜默地」消失

官方命名規則規定「檔案名稱・目錄名稱不可以空格或句點結尾」。6 更進一步說,Windows 的路徑正規化中有一條明確的規則:「若路徑不是以分隔符號結尾,則會移除結尾所有的句點與空格(U+0020)」。7

這在實務上麻煩之處在於,不會出現錯誤,而是靜默地變成另一個名稱。使用者輸入「報告書v2.」這個名稱,實際建立出來的卻是「報告書v2」。反過來說,透過 SMB 從 Linux 端建立、名為「report 」(結尾帶空格)這樣的檔案,以 Windows 一般的路徑指定方式,會因正規化而名稱改變,因而無法存取到。要存取這種「正規化下無法到達,但本身合法的名稱」,手段就是跳過正規化的 \\?\ 前綴。官方文件也明確指出這個用途,提到「像 hidden. 這樣的檔案,用其他方式無法存取」。7

另外,開頭的句點是合法的(可以毫無問題地建立像 .gitignore 這樣的名稱)。6

5. 大小寫是「保留形式,但不區分」

Windows 檔案系統的預設行為是 case-preserving, case-insensitive(保留大小寫但不區分)。以 Readme.txt 這個名稱建立時,顯示上會保留該大小寫形式,但在搜尋或比較時會忽略大小寫,用 README.TXT 也能存取到同一個檔案。磁碟機代號同樣不區分大小寫。86

官方命名規則對應用程式開發者明確指出「不要假設會區分大小寫(OSCAR、Oscar、oscar 應視為相同名稱)」,同時也提到NTFS 本身支援 POSIX 式的大小寫區分(但預設為停用)。6

這在 Linux 整合的情境下就會浮上檯面。在 Windows 10 組建 17107 以後,可以以目錄為單位啟用大小寫區分。9

# 在具有系統管理員權限的 PowerShell 中
fsutil.exe file setCaseSensitiveInfo C:\work\linux-src enable
fsutil.exe file queryCaseSensitiveInfo C:\work\linux-src

在 WSL 中處理源自 Linux 的原始碼樹(例如 Makefilemakefile 並存)時,這是有效的手段,不過官方文件本身也警告了其副作用。假設檔案系統不區分大小寫的 Windows 應用程式,在啟用區分功能的目錄中,可能會無法存取檔案。此外,旗標的變更必須在目標目錄為空的狀態下才能進行,而新建立的子目錄會繼承父目錄的設定。9 官方文件中也記錄了一個歷史現象:若存在兩個僅大小寫不同的同名檔案,檔案總管中兩者都會顯示,但無論選擇哪一個,都只能開啟其中同一個。9

在業務應用程式的設計上,預設將「在 Windows 上大小寫不同視為同名」,但對於要傳往 Linux 的檔案名稱,則要檢查大小寫不同所造成的衝突,是實務上可行的折衷點。Linux 整合在檔案名稱之前,字元編碼上也有陷阱,請一併參閱「釐清 Windows 的文字編碼 - 亂碼為什麼會發生,尤其是與 Linux 搭配時什麼地方會偏掉」。

6. 業務應用程式的實務 ── 路徑串接・清理・判斷表

6.1. 了解 Path.Combine 的規格後再使用路徑串接

+ 來串接路徑字串自然是不用談了,但 Path.Combine 也有應該事先了解的規格。當第二個以後的引數傳入含根目錄的路徑時,在它之前的所有引數都會被忽略10

var baseDir = @"C:\App\Data";

// 若使用者輸入含有根目錄,baseDir 會被靜默捨棄
Path.Combine(baseDir, @"C:\Windows\secret.txt"); // → "C:\Windows\secret.txt"
Path.Combine(baseDir, @"\evil.txt");             // → "\evil.txt"(目前磁碟機的根目錄)

若把來自使用者輸入或設定檔的字串原封不動地傳給第二個引數,就會形成寫入到預期儲存資料夾之外的漏洞。官方文件也提醒這種行為可能導致對機敏檔案的非預期存取,並提出 Path.Join / Path.TryJoin(.NET Framework 中沒有)作為替代方案。1016 無論使用哪一種,最終的定石都是用 Path.GetFullPath 正規化後,驗證結果是否位於基底目錄之下。

// 基底端也先正規化,再轉換成相對路徑來判定。
// 相較於字串前方一致,這種做法對「結尾是否有分隔符號」以及「基底為磁碟機根目錄」等情況更穩健
var baseFull = Path.GetFullPath(baseDir);
var fullPath = Path.GetFullPath(Path.Combine(baseFull, userInput));
var relative = Path.GetRelativePath(baseFull, fullPath);
if (relative == ".." ||
    relative.StartsWith(".." + Path.DirectorySeparatorChar) ||
    Path.IsPathRooted(relative)) // 若跳到不同磁碟機・UNC,會傳回絕對路徑
{
    throw new InvalidOperationException("儲存位置指向了預期資料夾之外。");
}

另外,Path.GetRelativePath 是 .NET Core 2.0 以後・.NET Standard 2.1 以後・.NET 5 以後才有的 API,.NET Framework 中沒有17 在 Framework 端,要替換成「將基底正規化後在結尾加上分隔符號,再以前方一致來判定」的形式。結尾的分隔符號是關鍵,若沒有它,C:\App\Data 的檢查就會誤判 C:\App\DataBackup 也在其之下。

// 給 .NET Framework 使用的替代方案(沒有 Path.GetRelativePath 的環境)
var baseFull = Path.GetFullPath(baseDir);
if (!baseFull.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal))
{
    baseFull += Path.DirectorySeparatorChar;   // 讓 "C:\App\Data" 不會與 "C:\App\DataBackup" 前方一致
}

var fullPath = Path.GetFullPath(Path.Combine(baseFull, userInput));
if (!fullPath.StartsWith(baseFull, StringComparison.OrdinalIgnoreCase)) // 配合 Windows 預設,忽略大小寫
{
    throw new InvalidOperationException("儲存位置指向了預期資料夾之外。");
}

Path.GetRelativePath 會以 OS 預設的方式比較路徑。也就是說,在 Windows 上是以不區分大小寫為前提來判定,這與下一章要說明的「Windows 預設不區分大小寫」的行為一致。反過來說,在以目錄為單位啟用了大小寫區分的位置(參見下一章),Datadata 可能會是不同的目錄,因此以忽略大小寫的方式判定,就會產生把「僅大小寫不同的另一個資料夾」誤判為在其之下的餘地。若可能會處理這類組態,安全的做法是採取不接受已啟用區分功能的位置作為基底目錄的方針。

還有一點也請留意,這種判定終究只是針對以字串正規化後的路徑所做的判定。若基底目錄之下存在接合點(junction)或符號連結,即使字串上指向其下,實體卻可能位於基底之外。而且連結不只能置於末端檔案,也能藏在途中的資料夾中(形式如 基底\連結\檔案.txt),只用 File.ResolveLinkTarget 檢查末端是無法察覺的。首先最重要的,是本來就避免採用讓不受信任的使用者能在基底之下建立連結或接合點的架構。在此之上,若仍需要嚴格保證,就要從實際開啟檔案取得的控制代碼取出確定的路徑(Win32 的 GetFinalPathNameByHandle)來驗證是否位於基底之下,或依序檢查路徑上每一個資料夾組成元件是否為連結。

6.2. 使用者輸入檔案名稱的清理

在像「往來對象名稱+日期.csv」這樣從使用者輸入組合出檔案名稱的功能中,要把清理處理集中到一個地方。Path.GetInvalidFileNameChars 是出發點,但官方明確指出這個陣列並不保證是不合法字元的完整集合11 保留裝置名稱與結尾的句點・空格,這個 API 無法偵測,因此要額外加上自訂檢查。

private static readonly HashSet<string> ReservedNames =
    new(StringComparer.OrdinalIgnoreCase)
    {
        "CON", "PRN", "AUX", "NUL",
        "COM1","COM2","COM3","COM4","COM5","COM6","COM7","COM8","COM9",
        "LPT1","LPT2","LPT3","LPT4","LPT5","LPT6","LPT7","LPT8","LPT9",
        "COM¹","COM²","COM³",  // 上標數字的 COM¹~COM³ 也是保留名稱
        "LPT¹","LPT²","LPT³",  // 同樣地 LPT¹~LPT³
    };

public static string SanitizeFileName(string input)
{
    var invalid = Path.GetInvalidFileNameChars();
    var name = new string(input.Select(c => invalid.Contains(c) ? '_' : c).ToArray());

    name = name.TrimEnd(' ', '.');            // 結尾空格・句點會被靜默移除,因此先行去除

    // 也要控制在單一檔案名稱元件的長度上限(通常為 255 字元)以內。
    // 保守地截斷,為資料夾階層或應用程式後續附加的字尾預留空間
    const int MaxNameLength = 120;
    if (name.Length > MaxNameLength)
    {
        var ext = Path.GetExtension(name);
        if (ext.Length > 20)
        {
            ext = ""; // 異常長的「副檔名」不當作副檔名保留(避免負範圍指定導致例外)
        }
        name = name[..(MaxNameLength - ext.Length)].TrimEnd(' ', '.') + ext;
    }

    // 空字串・保留名稱的判定,一定要對「最終形態」進行。
    // 為了抓出截斷或 TrimEnd 之後變成空字串或保留名稱(如 NUL)的情況
    var stem = name.Split('.')[0];            // 因應 NUL.txt: 以副檔名之前的部分判定保留名稱
    if (name.Length == 0 || ReservedNames.Contains(stem))
    {
        name = "_" + name;                    // 多加 1 個字元,仍足以收在上限 255 之內
    }
    return name;
}

這個函式的意圖,做成輸入輸出對照表會更容易理解,因此整理成可以直接當作單元測試第一批案例使用的形式。

輸入 輸出 生效的處理
報告書v2. 報告書v2 TrimEnd 移除結尾句點(搶先於正規化靜默消除之前處理)
CON.txt _CON.txt 去除副檔名後的 CON 是保留名稱。用 Split('.')[0] 判定後加上前綴
nul.tar.gz _nul.tar.gz 保留名稱判定為 OrdinalIgnoreCase。即使是雙重副檔名,也只看開頭元素
COM1 結尾加 1 個空格 _COM1 TrimEnd 的結果變成保留名稱。所以判定要對最終形態進行
... _ TrimEnd 變成空字串的情況。不傳回空的檔案名稱
A/B:C.csv A_B_C.csv GetInvalidFileNameChars 中包含的 /: 替換為 _
150 字元+.csv 開頭 116 字元+.csv(共 120 字元) 依長度上限截斷,保留副檔名

表格的第 4 行與第 5 行,正是程式碼中註解「判定一定要對最終形態進行」的原因所在。若不先完成替換・截斷・TrimEnd,再檢查保留名稱與空字串,像結尾帶空格的 COM1 這類輸入就會逃過檢查。

CSV 輸出的檔案名稱經常遇到這個問題,因此 CSV 本身的實務也請參考「CSV不是「純文字」而已 ── C# 業務應用程式的 CSV 實務(字元編碼・Excel 相容性・注入攻擊防範)」。

6.3. 相對路徑與目前目錄的陷阱

相對路徑有兩個陷阱。第一,目前目錄是以處理序為單位的設定,因此可能在任何時候被任何執行緒變更。官方文件甚至寫到「相對路徑在多執行緒應用程式中很危險」,若是 .NET Core 2.1 以後,可以使用能明確指定基準路徑的 Path.GetFullPath(string, string)7 第二,像 C:tmp.txt 這樣磁碟機代號後面沒有反斜線的形式,代表的是「C 磁碟機目前目錄起算的相對路徑」,而不是絕對路徑。這種「磁碟機相對路徑」被官方明確點名為程式或指令碼的經典錯誤來源。7

從設定檔或使用者輸入接收到的路徑,請養成在接收當下就用 Path.GetFullPath 轉換成絕對路徑,再記錄到日誌・進行驗證的習慣。

6.4. 判斷表 ── 該支援長路徑,還是在入口就擋下

狀況 建議 理由
使用者可自由選擇儲存位置的一般業務應用程式 在入口驗證後擋下(儲存前檢查完整路徑長度・檔案名稱,並出示明確錯誤) 即使自家應用程式支援,仍會殘留檔案總管或協作工具打不開的事故1
讀取由他人建立的深層階層(如備份・同步・封存展開) 支援長路徑(.NET Core 系列+視需要加上資訊清單,Framework 則設定 4.6.2+) 無法控制輸入內容,讀不到就會導致業務停擺54
自家應用程式是建立深層階層的一方 原則上改為不建立深層階層的設計(階層扁平化、採用雜湊名稱等) 使用建立出的路徑的一方(人・其他應用程式)很可能不支援1
與 Linux/WSL 交換檔案 在傳輸前檢查保留名稱・大小寫衝突・結尾字元 會產生 Windows 端無法到達的檔案69
從使用者輸入產生檔案名稱 GetInvalidFileNameChars + 保留名稱・結尾字元的清理邏輯集中到共用函式 僅靠 API 的陣列並不完整11

7. 疑難排解 ── 「檔案總管看得到,卻打不開」

這裡整理「檔案總管中看得到檔案,但用應用程式開啟時卻出現『找不到檔案』」這類諮詢的排查步驟。

確認要點 手段 若符合則
完整路徑是否接近 260 字元 在 PowerShell 中執行 (Get-ChildItem -Recurse).FullName \| Where-Object { $_.Length -ge 250 } 縮短上層資料夾名稱,或考慮支援長路徑(第 3 章)
檔案名稱是否為保留名稱(aux、con、com1 等) 目視確認名稱。含副檔名的形式也要納入檢查6 重新命名(若建立來源是 Linux 等,則在傳輸時進行轉換)
結尾是否有空格・句點 cmd /c dir /x 或加引號顯示來確認 用加上 \\?\ 前綴的路徑刪除・重新命名7
是否存在僅大小寫不同的同名檔案 容易發生在源自 WSL/Git 的資料夾中9 將其中一個重新命名,或重新檢視目標目錄的用途
是否使用了相對路徑・磁碟機相對路徑 在日誌中以絕對路徑記錄實際嘗試開啟的路徑 Path.GetFullPath 轉為絕對路徑後再使用7

表格中用到的 dir /x 讀法,補充說明一下。/x 是「顯示為無法收在 8.3 格式內的名稱所產生的短檔名」選項,顯示格式與 /n(名稱位於最右側的形式)相同,短檔名欄位會插入在長名稱之前18 也就是「日期 時間 大小 ─ 短檔名 ─ 長名稱」的排列,最右邊是原本的名稱,其左側則是短檔名(形式如 T97B4~1.TXT)。原本名稱就已經收在 8.3 格式內的檔案,不會產生短檔名,該欄位會是空白。當路徑長度觸及上限而無法開啟時,也可以用這個短檔名指定途中的資料夾,藉此壓縮路徑長度,作為應急處置。

調查的第一步,建議在應用程式的錯誤日誌中,以絕對路徑並加上引號,記錄「當時嘗試開啟的路徑本身」。單靠「找不到檔案」這樣的例外訊息,事後無法區分究竟是路徑被截斷、正規化改變了名稱,還是原本就指向了不同的目錄。若加上引號記錄下來,像結尾空格這種不容易目視發現的問題,也能一眼看出。

另外,DLL 載入失敗也是「找不到檔案」系錯誤的常客,不過這類問題多半與其說是路徑長度,不如說是搜尋順序的問題,已整理在「Windows 的 DLL 名稱解析機制 - 以實務角度整理搜尋順序、Known DLLs、API set、SxS」中。

總結

  • MAX_PATH=260 是包含「D:\+最多 256 字元+結尾 NUL」的 Win32 API 限制,NTFS 本身可以處理約 32,767 字元的擴充長路徑。目錄還有更進一步的限制,不能超過 MAX_PATH−12。
  • 要突破 260 字元,需要 \\?\ 前綴(僅限 Unicode 版 API・不可使用相對路徑),或是 Windows 10 1607 以後的長路徑啟用(登錄 LongPathsEnabled +資訊清單 longPathAware 兩者兼備)。
  • .NET (Core)/5+ 會隱含地處理長路徑,.NET Framework 以 4.6.2 以後為目標則會解除執行環境檢查。不過包含檔案總管在內,仍有不支援的應用程式存在,因此要把「能否建立」與「使用者能否處理」分開判斷。
  • 檔案名稱不能使用保留字元(< > : " / \ | ? *)與控制字元,CON・NUL・COM1 等保留裝置名稱即使加上副檔名也不可使用,結尾的空格・句點會在正規化時被靜默移除。
  • 大小寫預設是「保留但不區分」。透過 fsutil file setCaseSensitiveInfo 以目錄為單位進行區分,對 WSL 整合有效,但代價是 Windows 應用程式端可能誤動作的風險。
  • 在實作上,考量 Path.Combine 含根目錄引數規格所做的基底目錄驗證、GetInvalidFileNameChars + 保留名稱・結尾字元檢查的清理共用化,以及排除相對路徑(以 Path.GetFullPath 絕對化)是標準做法。

相關文章

相關諮詢領域

合同會社小村軟體承接「只有在特定環境・特定檔案上才打不開」這類由檔案輸出入引起的故障調查、既有業務應用程式的長路徑因應與檔案名稱驗證設計的重新檢討,以及 Windows・Linux 混合環境中檔案整合的設計諮詢。

參考連結

  1. Microsoft Learn, Maximum Path Length Limitation。關於 MAX_PATH=260 的定義,以及「磁碟機代號+冒號+反斜線+256 字元+結尾 NUL」這樣的組成內容、Unicode 版 API 與 \\?\ 前綴所帶來的約 32,767 字元擴充長路徑、元件長度(一般為 255 字元)、相對路徑始終受限於 MAX_PATH、目錄建立最多到 MAX_PATH−12,以及殼層與檔案系統要求不同、Win32 能建立的路徑殼層 UI 有時無法解讀等內容的說明。  2 3 4 5 6 7 8 9 10 11

  2. Microsoft Learn, NTFS overview。關於 NTFS 支援長檔案名稱與約 32,767 字元的擴充長路徑,以及 8.3 別名的向下相容性的說明。  2

  3. Microsoft Learn, Maximum Path Length Limitation ── Enable long paths in Windows 10, version 1607, and later。關於 Windows 10 1607 以後,必須同時具備登錄值 LongPathsEnabled=1 與應用程式資訊清單的 longPathAware 元素、群組原則的設定方式、登錄值以處理序為單位快取,以及解除限制的 Win32 函式清單的說明。  2 3

  4. Microsoft Learn, File path formats on Windows systems ── Skip normalization。關於 .NET Core 與 .NET 5 以後會隱含地處理長路徑、不進行 MAX_PATH 檢查(MAX_PATH 檢查僅限於 .NET Framework),以及 \\?\ 是跳過正規化的機制的說明。  2 3

  5. Microsoft Learn, Retargeting changes for migration to .NET Framework 4.6.x。關於以 .NET Framework 4.6.2 為目標時支援長路徑(最多 32K 字元)並移除 260 字元限制,以及以舊版為目標的應用程式可透過 Switch.System.IO.BlockLongPaths=false 選用此功能的說明。  2 3

  6. Microsoft Learn, Naming Files, Paths, and Namespaces。關於保留字元(< > : " / \ | ? *)與控制字元(0~31)、保留裝置名稱(CON/PRN/AUX/NUL/COM1~9/LPT1~9 及上標數字)、如 NUL.txt 這類加上副檔名者也等同於保留名稱、名稱不可以結尾空格・句點結束、開頭句點合法,以及不應假設會區分大小寫與 NTFS 的 POSIX 語意、\\?\ 前綴的行為與 Unicode API 要求的說明。  2 3 4 5 6 7 8 9 10 11 12

  7. Microsoft Learn, File path formats on Windows systems ── Path normalization。關於路徑正規化會移除結尾的句點與空格、像 hidden. 這樣的名稱只能透過 \\?\ 存取、CON 等舊式裝置名稱的解讀方式及其在 Windows 11 中的變更、磁碟機相對路徑(C:tmp.txt)是常見的錯誤來源、目前目錄以處理序為單位而相對路徑在多執行緒中很危險,以及 Path.GetFullPath(String, String) 的說明。  2 3 4 5 6 7 8 9

  8. Microsoft Learn, File path formats on Windows systems ── Case and the Windows file system。關於目錄名稱・檔案名稱會保留建立時的大小寫形式,而名稱比較不區分大小寫的說明。  2

  9. Microsoft Learn, Adjust case sensitivity。關於 Windows 10 組建 17107 以後可以目錄為單位區分大小寫(fsutil.exe file setCaseSensitiveInfo)、變更需要系統管理員權限與空目錄、新建立的子目錄會繼承設定、假設不區分大小寫的 Windows 應用程式可能因此誤動作的警告,以及大小寫不同的兩個檔案在檔案總管中都會顯示卻只能開啟其中一個的說明。  2 3 4 5 6

  10. Microsoft Learn, Path.Combine Method。關於當第一個以外的引數包含含根目錄的路徑時,在它之前的路徑元素會被忽略,並傳回以該含根目錄元素開頭的字串,以及這可能導致對機敏檔案的非預期存取,並列出 Join/TryJoin(.NET Framework 中無法使用)作為替代方案的說明。  2 3

  11. Microsoft Learn, Path.GetInvalidFileNameChars Method。關於此方法會傳回檔案名稱不可使用字元的陣列,而傳回的陣列並不保證是不合法字元的完整集合,且可能因檔案系統而異的說明。  2 3

  12. Microsoft Learn, PathTooLongException Class。關於路徑超過系統定義的最大長度時會擲回此例外,以及在 .NET Framework 4.6.2 以後,僅限於「超過 32,767 字元」或「OS 傳回錯誤」時才會擲回的說明。  2

  13. Microsoft Learn, Application Manifests。關於應用程式資訊清單是以 assembly 為根元素的 XML,以及 application 元素與 windowsSettings 用於宣告執行期設定的說明。 

  14. Microsoft Learn, Common MSBuild Project Properties。關於 ApplicationManifest 屬性用於指定資訊清單檔案的路徑,且多數情況下資訊清單會內嵌至可執行檔中的說明。 

  15. Microsoft Learn, Long Path Support (NuGet CLI)。關於以 .NET Framework 為基礎的工具要使用長路徑所需的實際組態(Windows 10 1607 以後或 1511+.NET Framework 4.6.2、Win32 long paths 原則、longPathAware 資訊清單+停用 UseLegacyPathHandling),以及 Visual Studio 與 msbuild 的 restore 不支援長路徑的說明。  2

  16. Microsoft Learn, Path.Join Method。關於 Join 不會捨棄含根目錄的後續路徑而會將其串接,以及與 Combine 行為差異之實例的說明。 

  17. Microsoft Learn, Path.GetRelativePath Method。關於支援的版本(.NET Core 2.0 以後、.NET Standard 2.1、.NET 5 以後;不包含 .NET Framework),以及比較時使用平台預設慣例(Windows 與 macOS 為 OrdinalIgnoreCase,Linux 為 Ordinal)的說明。 

  18. Microsoft Learn, dir。關於 /x 選項會顯示為非 8.3 格式名稱所產生的短檔名,顯示格式與 /n 相同,短檔名會插入在長名稱之前的說明。 

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

Windows 的路徑最多可以使用幾個字元?
Win32 API 的預設值是 MAX_PATH=260 字元,這是包含「磁碟機代號+冒號+反斜線+256 字元份的路徑字串+結尾 NUL」的長度。NTFS 等檔案系統本身可以處理更長的路徑,只要對 Unicode 版 API 傳入加上 `\?\` 前綴的路徑,總長度最多可指定約 32,767 字元。不過單一資料夾名稱・檔案名稱(元件)一般以 255 字元為上限,而相對路徑則始終受限於 MAX_PATH 以內。
MAX_PATH 的 260 字元限制該如何解除?
在 Windows 10 版本 1607 以後,只要同時設定登錄值 LongPathsEnabled=1(或群組原則的「啟用 Win32 長路徑」)以及應用程式資訊清單中的 longPathAware 元素,許多 Win32 檔案函式的 260 字元限制就會解除。只設定其中一項並不會生效。.NET(Core)/.NET 5 以後的執行環境不會進行 MAX_PATH 檢查,會隱含地處理長路徑;.NET Framework 若以 4.6.2 以後為目標,則會解除執行環境端的 260 字元檢查。不過包含檔案總管在內,仍有不支援長路徑的應用程式存在,因此必須連同「產生的長路徑會由誰來處理」一併納入判斷。
為什麼無法建立名為 CON 或 NUL 的檔案?
CON、PRN、AUX、NUL、COM1~COM9、LPT1~LPT9 等,是延續自 MS-DOS 時代的裝置保留名稱,Windows 會將這些名稱解讀為裝置而非檔案。即使像 NUL.txt 這樣加上副檔名,也會被視為與 NUL 相同,因此無法藉此迴避。Windows 11 中路徑解讀的行為有部分變更,但由於舊版作業系統與許多應用程式仍維持原本的解讀方式,業務資料的檔案名稱仍應繼續避免使用這些名稱,較為安全。
Windows 是否會區分檔案名稱的大小寫?
預設是「保留但不區分」(case-preserving, case-insensitive)。以 Readme.txt 這個名稱建立檔案時,顯示上會保留該大小寫形式,但即使嘗試開啟 README.TXT,也會存取到同一個檔案。NTFS 也支援 POSIX 式的大小寫區分,在 Windows 10 組建 17107 以後,可以用 fsutil.exe file setCaseSensitiveInfo 以目錄為單位啟用區分功能,但這會產生副作用──以「不區分大小寫」為前提的 Windows 應用程式可能因此動作異常,因此應僅限定在如 WSL 整合等確有需要的情境下使用。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽