MAX_PATH и подводные камни путей/имён файлов в Windows — лимит 260 символов, зарезервированные имена, конечная точка, регистр
· Го Комура · MAX_PATH, Пути к файлам, Длинные пути, Имя файла, NTFS, Win32, C#, .NET, Разработка Windows, Расследование сбоев, Техническая консультация
«Только у одного пользователя на компьютере файл не находится», «скопировали файл, а открыть его не получается» — при расследовании ошибок бизнес-приложений, работающих с файловым вводом-выводом, нередко в итоге оказывается, что причина в длине пути или в самом имени файла. Пользователи набивают в имена папок названия проектов и даты, закапываются в глубокую иерархию каталогов и легко выходят за рамки ваших предположений.
Сложность этой области в том, что ограничения разбиты на несколько слоёв — «ограничение Win32 API», «ограничение файловой системы», «ограничение оболочки (Проводника)», «ограничение runtime .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
- Runtime .NET (Core) / .NET 5 и новее не выполняет проверку MAX_PATH и неявно обрабатывает длинные пути. При таргетировании .NET Framework на 4.6.2 и новее снимается собственная проверка runtime на 260 символов.45
- При этом приложения без поддержки длинных путей реально остаются в ходу. Сама официальная документация прямо указывает, что оболочка (Проводник) может неверно интерпретировать путь, который удалось создать через Win32 API.1
- В именах файлов нельзя использовать
< > : " / \ | ? *и управляющие символы (0-31), а CON, PRN, AUX, NUL, COM1-9 и LPT1-9 трактуются как зарезервированные даже с расширением (например, CON.txt).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 символов. У этого числа есть точный состав. Локальный путь складывается из «буквы диска, двоеточия, обратной косой черты, разделённых обратными косыми чертами компонентов имени и завершающего NUL» — например, для диска D максимум составляет «D:\ + 256 символов текста пути + завершающий NUL».1
Иными словами, если считать 260 «длиной, доступной для имени файла», на самом деле её на 4 символа меньше — за счёт обозначения диска и завершающего NUL. Есть и более узкое ограничение: поскольку API создания каталогов требуют оставить место для последующего добавления имени в формате 8.3, путь к каталогу не может превышать MAX_PATH минус 12 символов.1
Важно понимать, что это ограничение уровня Win32 API, а не предел файловой системы. NTFS поддерживает длинные имена файлов и пути расширенной длины, а Unicode-версии многих Win32-функций принимают пути расширенной длины общим объёмом около 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» или вызовы через UTF-16, как в .NET).
- Поскольку нормализация пропускается, нельзя использовать разделитель
/или относительную запись через./... Префикс\\?\нельзя добавить к относительному пути, поэтому относительные пути всегда ограничены значением MAX_PATH.1 - Поддерживают это не все API — совместимость нужно проверять по документации конкретной функции.6
3.2. Включение длинных путей на Windows 10 1607 и новее — условие «оба параметра сразу»
На Windows 10 версии 1607 и новее можно снять ограничение MAX_PATH со многих распространённых Win32-функций работы с файлами и каталогами (CreateFileW, FindFirstFileW, GetFileAttributesW и т. п.). Однако это требует явного включения со стороны приложения — нужно выполнить оба следующих условия.3
- Значение реестра
LongPathsEnabled(REG_DWORD) в разделеHKLM\SYSTEM\CurrentControlSet\Control\FileSystemдолжно быть равно 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 и новее: runtime не выполняет проверку MAX_PATH и неявно обрабатывает длинные пути. Особый код на стороне приложения не требуется.4
- .NET Framework: при таргетировании на 4.6.2 и новее снимается проверка runtime на 260 символов, и
PathTooLongExceptionтеперь выбрасывается только при превышении 32 767 символов или когда ошибку возвращает сама ОС. Даже существующее приложение с более старым таргетом может подключить это через AppContext-переключателиSwitch.System.IO.BlockLongPaths=false(а такжеSwitch.System.IO.UseLegacyPathHandling=false, отключающий устаревшую обработку путей).512 - Чтобы приложение на .NET Framework реально пропускало длинные пути на практике, помимо настроек runtime выше требуется совместное использование включения длинных путей на стороне ОС и манифеста. Документация по поддержке длинных путей в NuGet.exe явно приводит эту конфигурацию (Windows 10 1607+ + манифест longPathAware + отключение UseLegacyPathHandling) как рабочий пример.13
3.4. Реальность: «неподдерживающие приложения» остаются даже после всего этого
Даже проделав всё это, вы не заставите все приложения в мире работать с длинными путями. Официальная документация прямо указывает: «у оболочки и файловой системы разные требования, интерфейс оболочки может неверно интерпретировать путь, который удалось создать через Win32 API»,1 и на практике инструменты, не заявляющие о поддержке длинных путей, действительно встречаются (например, документация NuGet отмечает, что восстановление пакетов (restore) через Visual Studio или msbuild -t: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) |
| Конечные символы | Имя, заканчивающееся пробелом или точкой | Файловая система может это допустить, но оболочка и UI — нет |
4.1. Зарезервированные имена устройств — даже CON.txt не годится
CON и NUL — это имена устройств из эпохи MS-DOS, и они по сей день остаются зарезервированными в пространстве имён NT. Поэтому файл с именем «CON» обычным способом создать нельзя, и даже с добавлением расширения, как в CON.txt, оно всё равно интерпретируется как зарезервированное имя.6 На практике это проявляется, например, как сбой при попытке сохранить лог взаимодействия с последовательным портом под именем вроде «COM1.log», или как невозможность распаковать в Windows папку «aux», созданную на стороне Linux.
Заметим также, что по правилам нормализации пути раньше путь, начинающийся с зарезервированного имени — например, «CON» или «COM1.TXT», — преобразовывался и интерпретировался как путь устройства (\\.\CON). В Windows 11 эта интерпретация изменилась, и теперь для обращения к легаси-устройству требуется полная форма записи вроде \\.\CON.7 Тем не менее, поскольку и старые версии ОС, и существующие приложения по-прежнему используют прежнюю интерпретацию, вывод о том, что зарезервированных имён стоит избегать для бизнес-данных, не меняется.
4.2. Пробелы и точки в конце имени исчезают «молча»
Официальные правила именования гласят: «не завершайте имя файла или каталога пробелом или точкой».6 Если копнуть глубже, в нормализации путей Windows есть чёткое правило: «если путь не заканчивается символом-разделителем, все конечные точки и пробелы (U+0020) удаляются».7
На практике это неприятно тем, что без всякой ошибки имя молча превращается в другое. Если пользователь вводит имя «отчёт_v2.», в действительности создаётся файл «отчёт_v2» — конечная точка исчезает. И наоборот, файл вроде «report » (с конечным пробелом), созданный со стороны Linux через SMB, недостижим через обычную запись пути в 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 не поможет её обнаружить. В первую очередь стоит вообще избегать конфигураций, где недоверенный пользователь способен создавать ссылки или junction внутри базового каталога. Если же строгое соблюдение действительно необходимо, либо получайте окончательный путь из дескриптора реально открытого файла (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 — это не «просто текст» — практика работы с CSV в бизнес-приложениях на C#».
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/Git9 | Переименовать один из файлов или пересмотреть назначение каталога |
| Используется ли относительный или относительный-к-диску путь | Логировать реально используемый путь в абсолютном виде | Приводить к абсолютному через Path.GetFullPath перед использованием7 |
В качестве первого шага диагностики рекомендуем записывать в лог ошибок приложения точный путь, который пытались открыть, в абсолютном виде и в кавычках. Одного сообщения об исключении «файл не найден» недостаточно, чтобы впоследствии понять, был ли путь обрезан, изменилось ли имя из-за нормализации или речь вообще шла о другом каталоге. Если записывать путь в кавычках, такие трудноразличимые проблемы, как конечный пробел, видны сразу.
Отметим также, что неудачная загрузка DLL — тоже частый представитель ошибок вида «файл не найден», но здесь чаще дело в порядке поиска, а не в длине пути; это разобрано в статье «Как работает разрешение имён DLL в Windows — порядок поиска и 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 и новее снимает проверку runtime. Однако приложения без поддержки, включая Проводник, по-прежнему встречаются, поэтому вопросы «можно ли создать» и «сможет ли пользователь этим воспользоваться» нужно рассматривать раздельно.
- В именах файлов недопустимы зарезервированные символы (
< > : " / \ | ? *) и управляющие символы, зарезервированные имена устройств вроде CON, NUL, COM1 недопустимы даже с расширением, а конечные пробелы и точки молча удаляются при нормализации. - Регистр по умолчанию «сохраняется, но не различается». Различение регистра на уровне каталога через
fsutil file setCaseSensitiveInfoполезно при взаимодействии с WSL, но достаётся ценой риска некорректной работы приложений Windows. - На стороне реализации стандартная практика — проверка базового каталога с учётом особенности
Path.Combineв отношении корневых аргументов, объединение санитизации вокругGetInvalidFileNameCharsплюс проверка зарезервированных имён и конечных символов, а также исключение относительных путей через приведение к абсолютным с помощьюPath.GetFullPath.
Похожие статьи
- CSV — это не «просто текст» — практика работы с CSV в бизнес-приложениях на C# (кодировка символов, совместимость с Excel, защита от инъекций)
- Введение в кодировки символов Windows — мойибакэ при взаимодействии с Linux
- Кодировки символов и символы перевода строки в Windows — основы мойибакэ и CRLF/LF
- Как работает разрешение имён DLL в Windows — порядок поиска и SxS
Смежные области консультаций
В Komura Software LLC мы занимаемся расследованием ошибок, связанных с файловым вводом-выводом, вроде «не открывается только в определённом окружении или только определённый файл», пересмотром поддержки длинных путей и валидации имён файлов в существующих бизнес-приложениях, а также консультациями по проектированию файлового взаимодействия в смешанных окружениях Windows и Linux.
- Разработка приложений для Windows
- Расследование сбоев и анализ первопричин
- Техническая консультация и ревью архитектуры
- Контакты
Справочные материалы
-
Microsoft Learn, Maximum Path Length Limitation. Об определении MAX_PATH=260 и его составе «буква диска + двоеточие + обратная косая черта + 256 символов + завершающий NUL», о путях расширенной длины около 32 767 символов через Unicode-версии API и префикс
\\?\, об ограничении длины компонента (обычно 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. О необходимости одновременно значения реестра LongPathsEnabled=1 и элемента longPathAware в манифесте приложения на Windows 10 1607 и новее, о настройке через групповую политику, о кэшировании значения реестра на уровне процесса и о перечне 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 обеспечивает поддержку длинных путей (до 32 тыс. символов) и снимает ограничение в 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 с расширением зарезервированному имени, о недопустимости конечных пробелов и точек в имени, о легальности начальной точки, о том, что не стоит предполагать различение регистра, и о POSIX-семантике NTFS, а также о поведении префикса\\?\и требовании 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), — распространённый источник ошибок, о том, что текущий каталог задаётся на уровне процесса, а относительные пути опасны при многопоточности, а также о 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. О различении регистра на уровне каталога (fsutil.exe file setCaseSensitiveInfo), доступном начиная со сборки Windows 10 17107, о требовании прав администратора и пустого каталога для изменения, о наследовании настройки новыми подкаталогами, о предупреждении насчёт возможного некорректного поведения приложений 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), а также о том, что восстановление пакетов (restore) в Visual Studio и msbuild не поддерживает длинные пути. ↩ ↩2
-
Microsoft Learn, Path.Join Method. О том, что Join объединяет последующий корневой путь, а не отбрасывает его, с примерами отличия поведения от Combine. ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Спящий режим, гибернация, Modern Standby и долго работающие приложения — как проектированием предотвратить «остановилось ночью»
Разбираем, почему долго работающее Windows-приложение оказывается «остановленным к утру», начиная с различий между спящим режимом S3, гиб...
Подводные камни сетевых дисков и UNC-путей ── как бизнес-приложения работают с файловым сервером (общей папкой)
Разбираем типичные проблемы, возникающие при выводе данных и мониторинге общей папки из бизнес-приложения: почему буква диска (Z:) не вид...
Безопасный вызов Win32 API из C# — практическое руководство по P/Invoke (DllImport / LibraryImport / CsWin32)
Разбираем практические аспекты вызова Win32 API из C# через P/Invoke: разницу между DllImport и LibraryImport, автогенерацию сигнатур чер...
Если ваше Windows-приложение приняли за вирус — как реагировать на ложные срабатывания Microsoft Defender и жить с влиянием на производительность
Разбираем правильный порядок действий, если Microsoft Defender ложно определяет ваше Windows-приложение как вредоносное: как устроена сов...
Работают ли бизнес-приложения на Windows на Arm — реальность x64-эмуляции (Prism) и нативных DLL/COM
Отвечаем разработчикам и ИТ-специалистам на вопрос «заработает ли наше бизнес-приложение на Windows на Arm». Разбираем принцип работы x64...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы 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 в манифесте приложения, лимит в 260 символов снимается для многих Win32-функций работы с файлами. Задание только одного из двух параметров не даёт эффекта. Runtime .NET (Core) / .NET 5 и новее не выполняет проверку MAX_PATH и обрабатывает длинные пути неявно, а при таргетировании .NET Framework на версию 4.6.2 и новее снимается проверка на 260 символов на стороне runtime. Однако приложения без поддержки длинных путей — включая сам Проводник — по-прежнему встречаются, поэтому нужно учитывать, кто в итоге будет работать с созданным вами длинным путём.
- Почему нельзя создать файл с именем 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.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки