「只有在某个用户的电脑上才会出现『找不到文件』」「明明复制成功了,那个文件却打不开」── 在排查涉及文件输入输出的业务应用故障时,最终发现根本原因是路径长度或文件名本身的情况并不少见。用户会把项目名称、日期塞进文件夹名称,把目录层级挖得很深,轻而易举地超出我们原本的预期。
这一领域棘手的地方在于,限制分布在好几个层面 ──「Win32 API 的限制」「文件系统的限制」「外壳程序(资源管理器)的限制」「.NET 运行时的限制」── 让人很难判断到底能应对到什么程度,又有哪些问题会始终存在。本文将从 MAX_PATH=260 字符的真相说起,梳理合法处理长路径的条件、CON 等保留名称与末尾句点等文件名陷阱,以及大小写的处理方式,并配合 C# 的实务应对一并整理。
1. 先说结论
- MAX_PATH=260 是 Win32 API 的限制,其中包含「驱动器盘符+冒号+反斜杠+最多 256 个字符的路径文本+末尾 NUL 终止符」。文件系统一侧(如 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 即使加上扩展名也依旧会被当作保留名称。6 - 名称末尾的空格和句点,会在路径规范化的过程中被悄悄去除。 这正是「明明输入的是『报告v2.』,结果却变成了『报告v2』」,或者「无法用 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
也就是说,如果把 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 字符限制的方法及其条件
处理长路径的手段大致有两种 ──「\\?\ 前缀」和「在操作系统层面启用长路径」。
3.1. \\?\ 前缀
在路径字符串开头加上 \\?\,Win32 API 就会停止对字符串本身的解析,直接原样传递给文件系统。这样便可以突破 MAX_PATH 的限制(UNC 路径的形式为 \\?\UNC\server\share)。不过,这里有一些条件和副作用。16
- 必须是 Unicode 版本的 API(即以 W 结尾的函数,或者像 .NET 那样以 UTF-16 方式调用的函数)。
- 由于会跳过规范化处理,因此无法使用以
/分隔的写法,也无法使用.、..这样的相对路径写法。由于相对路径无法附加\\?\,因此相对路径始终受限于 MAX_PATH。1 - 并非所有 API 都支持这一前缀,是否支持需要查阅各个 API 的参考文档来确认。6
3.2. Windows 10 1607 及更高版本的长路径启用 ── 条件是「两者都要满足」
在 Windows 10 版本 1607 及更高版本中,可以为许多常用的 Win32 文件・目录函数(CreateFileW、FindFirstFileW、GetFileAttributesW 等)解除 MAX_PATH 限制。不过这需要应用程序一侧主动开启,且必须同时满足以下两个条件。3
HKLM\SYSTEM\CurrentControlSet\Control\FileSystem下的注册表值LongPathsEnabled(REG_DWORD)为 1。也可以通过组策略「计算机配置 > 管理模板 > 系统 > 文件系统 > 启用 Win32 长路径」进行设置。- 应用程序清单中包含
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
「明明设置了注册表却不生效」这类问题,大多是因为清单一侧缺失了相应设置。官方文档也再三强调「这个注册表设置只会影响那些经过修改、能够利用新功能的应用程序」。此外,注册表值会在进程首次调用文件函数时按进程缓存,进程存活期间不会重新读取。要确保设置变更对所有应用程序都生效,有时需要重启系统。3
3.3. .NET 中的情况如何
- .NET (Core) / .NET 5 及更高版本:运行时不会执行 MAX_PATH 检查,会隐式处理长路径,应用程序一侧不需要编写特殊代码。4
- .NET Framework:只要以 4.6.2 及更高版本为目标,就能解除运行时的 260 字符检查,此时
PathTooLongException仅限于「路径超过 32,767 个字符」或「操作系统返回错误」这两种情况才会抛出。即使是以更早版本为目标的既有应用程序,也可以通过Switch.System.IO.BlockLongPaths=false(以及用来禁用旧版路径处理的Switch.System.IO.UseLegacyPathHandling=false)这类 AppContext 开关来主动启用。512 - .NET Framework 应用程序要在实务中真正打通长路径,除了上述运行时设置之外,还需要同时配合操作系统一侧的长路径启用与清单。NuGet.exe 关于长路径支持的文档,就把这套组合(Windows 10 1607 及更高版本+longPathAware 清单+禁用 UseLegacyPathHandling)作为实际案例明确记载了下来。13
3.4. 即便如此,「不支持的应用程序」依然存在
即使做到了这一步,也不意味着世界上所有应用程序都能处理长路径了。官方文档明确写道:「外壳程序与文件系统的要求不同,外壳程序界面有可能无法正确解析 Win32 API 能够创建出来的路径」,1 实际上也确实还存在不宣称支持长路径的工具(例如 NuGet 的文档中就记载了 Visual Studio 以及 msbuild -t:restore 的 restore 操作不支持长路径 13)。即使我们自己的应用能够以长路径创建文件,用户能否用资源管理器或其他工具打开它,是另外一回事。考虑到这种不对称性该如何做设计决策,会在第 6 章的判断表中汇总说明。
4. 不可用字符、保留设备名称,以及末尾的句点・空格
与路径长度并列的另一个雷区,是文件名本身的规则。下面把官方命名规则中业务应用最容易踩中的部分整理出来。6
| 分类 | 内容 | 备注 |
|---|---|---|
| 保留字符 | < > : " / \ \| ? * |
包含路径分隔符 \(以及 /)、驱动器分隔符 : |
| 控制字符 | 整数值 0(NUL)以及 1~31 | 除备用数据流内部外均不可使用 |
| 保留设备名称 | CON, PRN, AUX, NUL, COM1~COM9, LPT1~LPT9(以及上标数字形式的 COM¹~³、LPT¹~³) | 即使加上扩展名也不可用(NUL.txt 或 NUL.tar.gz 均等同于 NUL) |
| 末尾字符 | 以空格或句点结尾的名称 | 文件系统本身可能允许,但外壳程序与用户界面不支持 |
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 不过,由于旧版操作系统和既有应用程序仍然普遍沿用传统的解释方式,业务数据的名称应当避开保留名称这一结论并没有改变。
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
在处理源自 Linux 的源代码树(例如 Makefile 与 makefile 共存)时,这是一个有效的手段,但官方文档本身也警告了其副作用。如果 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 中不可用)作为替代方案。1014 不管用哪一种,最终都应当遵循这样的惯例:用 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 会按照操作系统默认的方式来比较路径 —— 也就是说在 Windows 上,比较时不区分大小写,这与下一章将说明的「Windows 默认不区分大小写」这一行为是相符的。反过来说,在按目录单独启用了大小写区分的位置(参见下一章),Data 与 data 有可能是两个不同的目录,因此不区分大小写的判断就有可能把「仅大小写不同的另一个文件夹」误判为位于基准目录之下。如果有可能处理这类配置,比较安全的做法是不接受启用了大小写区分的位置作为基准目录。
还有一点也需要留意,这种判断终究只是针对经过字符串规范化的路径进行的。如果基准目录之下存在联接点(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; // 多加一个字符,也完全在 255 的上限之内
}
return name;
}
CSV 输出的文件名中经常会遇到这个问题,关于 CSV 本身的实务,也请参考「CSV 并不只是「纯文本」── C# 业务应用中的 CSV 实务(字符编码・Excel 兼容性・注入对策)」。
6.3. 相对路径与当前目录的陷阱
相对路径存在两个陷阱。第一,当前目录是按进程设置的,因此任何线程都可能在任意时刻改变它。官方文档甚至写道「相对路径在多线程应用程序中是危险的」,从 .NET Core 2.1 起,可以使用能够显式指定基准路径的 Path.GetFullPath(string, string)。7 第二,像 C:tmp.txt 这样驱动器盘符后面紧接着没有反斜杠的形式,表示的是「相对于 C 盘当前目录的路径」,而不是绝对路径。这种「驱动器相对路径」被官方文档明确指出,是程序和脚本中常见的 bug 来源。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 |
排查的第一步,建议把应用错误日志中「尝试打开的路径本身」以绝对路径、并加上引号的形式记录下来。仅凭「找不到文件」这样的异常消息,事后是无法区分路径是被截断了、是在规范化过程中改了名字,还是本来就指向了另一个目录。如果用引号括起来记录,像末尾空格这类不容易察觉的问题也能一眼看出来。
另外,DLL 加载失败也是「找不到文件」类错误中的常客,但这类问题往往和搜索顺序的关系比路径长度更大,我们在「Windows DLL 名称解析机制 ── 搜索顺序与 SxS」中做了整理。
8. 总结
- MAX_PATH=260 是 Win32 API 的限制,包含「
D:\+最多 256 个字符+末尾 NUL」,而 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转换为绝对路径来消除相对路径。
相关文章
- CSV 并不只是「纯文本」── C# 业务应用中的 CSV 实务(字符编码・Excel 兼容性・注入对策)
- Windows 字符编码入门 ── 与 Linux 互操作时出现乱码的原因
- Windows 的字符编码与换行符 ── 乱码与 CRLF/LF 的基础知识
- Windows DLL 名称解析机制 ── 搜索顺序与 SxS
相关咨询领域
小村软件有限公司承接诸如「仅在特定环境・特定文件下打不开」这类由文件 I/O 引发的故障排查、既有业务应用的长路径支持与文件名校验设计的重新审视,以及 Windows・Linux 混合环境下文件互操作的设计咨询。
参考链接
-
Microsoft Learn, Maximum Path Length Limitation。关于 MAX_PATH=260 的定义及其「驱动器盘符+冒号+反斜杠+256 个字符+末尾 NUL」的构成、通过 Unicode 版本 API 与
\\?\前缀可实现的约 32,767 个字符的扩展长路径、组成部分长度(通常为 255 个字符)、相对路径始终受限于 MAX_PATH、目录创建不能超过 MAX_PATH−12,以及外壳程序与文件系统要求不同、外壳程序界面有可能无法解析 Win32 能够创建出来的路径等内容。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 -
Microsoft Learn, NTFS overview。关于 NTFS 支持长文件名与约 32,767 个字符的扩展长路径,以及通过 8.3 别名实现的向后兼容性等内容。 ↩ ↩2
-
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
-
Microsoft Learn, File path formats on Windows systems ── Skip normalization。关于 .NET Core 以及 .NET 5 及更高版本会隐式处理长路径而不执行 MAX_PATH 检查(MAX_PATH 检查仅存在于 .NET Framework 中),以及
\\?\是跳过规范化处理的机制等内容。 ↩ ↩2 ↩3 -
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
-
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 -
Microsoft Learn, File path formats on Windows systems ── Path normalization。关于路径规范化会去除末尾的句点和空格、像
hidden.这样的名称只能通过\\?\访问、CON 等旧式设备名称的解释方式及其在 Windows 11 中的变化、驱动器相对路径(C:tmp.txt)是常见的 bug 来源、当前目录是按进程设置的且相对路径在多线程环境下存在风险,以及 Path.GetFullPath(String, String) 等内容。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 -
Microsoft Learn, File path formats on Windows systems ── Case and the Windows file system。关于目录名・文件名会保留创建时的大小写形式,而名称比较时不区分大小写等内容。 ↩ ↩2
-
Microsoft Learn, Adjust case sensitivity。关于从 Windows 10 内部版本 17107 起可按目录启用大小写区分(fsutil.exe file setCaseSensitiveInfo)、变更该设置需要管理员权限且目标目录必须为空、新建的子目录会继承该设置、默认假设不区分大小写的 Windows 应用程序可能出现异常的警告,以及历史上仅大小写不同的两个文件会同时显示在资源管理器中但只能打开其中一个等内容。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Path.Combine Method。关于第一个参数之后的任何参数只要是带根路径,之前的路径元素就会被舍弃,返回从该带根路径元素开始的字符串、这可能导致对敏感文件的意外访问,以及提供了 Join/TryJoin(在 .NET Framework 中不可用)作为替代方案等内容。 ↩ ↩2 ↩3
-
Microsoft Learn, Path.GetInvalidFileNameChars Method。关于该方法返回文件名中不可使用的字符数组,以及返回的数组并不保证是不合法字符的完整集合、可能因文件系统而异等内容。 ↩ ↩2 ↩3
-
Microsoft Learn, PathTooLongException Class。关于该异常会在路径超过系统定义的最大长度时抛出,以及在 .NET Framework 4.6.2 及更高版本中,仅限于「超过 32,767 个字符」或「操作系统返回错误」这两种情况才会抛出等内容。 ↩ ↩2
-
Microsoft Learn, Long Path Support (NuGet CLI)。关于基于 .NET Framework 的工具要使用长路径所需的实际配置(Windows 10 1607 及更高版本或 1511+.NET Framework 4.6.2、Win32 长路径策略、longPathAware 清单+禁用 UseLegacyPathHandling),以及 Visual Studio 与 msbuild 的 restore 操作不支持长路径等内容。 ↩ ↩2
-
Microsoft Learn, Path.Join Method。关于 Join 不会舍弃后续的带根路径而是直接拼接,以及与 Combine 行为差异的具体示例等内容。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
用 PerfView 与 dotnet-trace 定位“变慢”的原因 ── .NET 性能排查实务入门
当业务应用出现“变慢”“CPU 占满”“偶尔卡死”时,该用哪个工具看什么?本文梳理 PerfView 与 dotnet-trace 的分工、CPU 采样的解读方法(inclusive/exclusive)、用 ThreadTime 排查阻塞时间,以及与 EventSourc...
睡眠、休眠、Modern Standby 与长时间运行应用 ── 用设计防止「夜里意外停止」
本文从 S3 睡眠、休眠、Modern Standby 的区别入手,整理长时间运行的 Windows 应用为何会出现「早上一看已经停止」的原因,并讲解睡眠期间计时器与 TCP 连接的行为,以及如何用 SetThreadExecutionState 进行抑制。
Windows 事件日志・ETW 入门 ── 让业务应用程序的日志接入操作系统标准机制
Windows 业务应用程序的日志,是不是只靠文件日志来解决?事件日志与 ETW,是运维人员和操作系统标准工具所能看到的另一层记录。本文整理三种手段的分工方式、.NET 中的写入方法、通过 EventSource 进行的 ETW 埋点、收集与排查的实务做法,以及来源未注册、...
用 WinDbg + SOS 解读崩溃转储 ── 采集之后的实务分析入门
本文说明如何用 WinDbg 与 SOS 扩展实际读取已采集的 Windows 崩溃转储。内容涵盖符号路径设置、以 !clrstack、!pe、!dumpheap -stat、!gcroot 追踪异常与内存泄漏的方法、原生崩溃的 !analyze -v,以及与 dotnet...
在 C# 中安全调用 Win32 API —— P/Invoke 实务指南(DllImport / LibraryImport / CsWin32)
本文整理在 C# 中通过 P/Invoke 调用 Win32 API 或原生 DLL 时的实务要点:DllImport 与 LibraryImport 的区别、用 CsWin32 自动生成签名、字符串封送的陷阱、用 SafeHandle 管理句柄、SetLastError ...
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
与本主题相关的服务
本文与以下服务页面相关联,欢迎从最接近的入口查看。
Windows 应用程序开发
支持包含常驻处理、设备联动、运行日志与可维护结构的 Windows 桌面应用程序。
技术咨询 & 设计评审
协助梳理设计方向、架构边界、生命周期责任,以及既有 Windows 资产的处理方式。
常见问题
汇总了咨询这一主题时常见的问题。
- 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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。