MAX_PATH и ловушки путей и имён файлов в Windows — лимит 260 символов, зарезервированные имена, точка в конце, регистр

· Обновлено: · · MAX_PATH, Пути к файлам, Длинные пути, Имена файлов, NTFS, Win32, C#, .NET, Разработка под Windows, Диагностика сбоев, Техническая консультация

История изменений (1 обновлений, последнее 30 Aug 2026)

Журнал изменений этой статьи. Там, где версия до правки была заархивирована, она остаётся доступной для чтения по постоянной ссылке с DOI.

Русский текст переписан как полноценный технический перевод, а не калька с японского. Утверждения статьи не менялись.
Первая публикация
Цитирование статьи(DOI (зарегистрированный архив): 10.5281/zenodo.21620046)

Приведённые ниже DOI относятся к ранее зарегистрированным архивным версиям, которые могут отличаться от текущего текста. Для ссылки на текущий текст используйте URL этой страницы.

Го Комура (2026). MAX_PATH и ловушки путей и имён файлов в Windows — лимит 260 символов, зарезервированные имена, точка в конце, регистр. KomuraSoft LLC. https://comcomponent.com/ru/blog/windows-max-path-filename-pitfalls/

DOI (зарегистрированный архив)
10.5281/zenodo.21620046
DOI (последняя зарегистрированная версия)
10.5281/zenodo.21620047

«Только на компьютере одного пользователя файл не находится», «скопировали — а открыть не получается». При разборе сбоев бизнес-приложений с файловым вводом-выводом нередко в итоге оказывается, что дело в длине пути или в самом имени файла. Пользователи запихивают в имена папок названия проектов и даты, уходят в глубокую иерархию и легко выходят за рамки ваших ожиданий.

Сложность этой области в том, что ограничения разнесены по нескольким слоям — лимит 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 считаются зарезервированными даже с расширением (в том числе CON.txt).6
  • Пробел и точка в конце имени молча удаляются при нормализации пути. Отсюда типичные сбои: задали «отчёт_v2.», а получили «отчёт_v2»; или из Windows не удаётся открыть файл с пробелом в конце, созданный другой ОС.67
  • В Windows по умолчанию регистр имён сохраняется, но не учитывается. NTFS также умеет различать регистр на уровне каталога (fsutil.exe file setCaseSensitiveInfo), но у включения этого режима есть побочный эффект: приложения Windows не всегда могут за ним уследить.89
  • На стороне кода учитывайте поведение Path.Combine: если один из последующих аргументов — путь с корнем, все предыдущие отбрасываются (в семействе .NET Core есть ещё Path.Join) и приводите пользовательские имена файлов к безопасному виду через Path.GetInvalidFileNameChars плюс собственную проверку зарезервированных имён и конечных символов.1011

На схеме сплошная линия обозначает отношение, которое выполняется всегда, а пунктирная — условное отношение (условия указаны в пояснении к каждому отношению на странице сведений). Полный список отношений (всего 19, с доказательствами и степенью уверенности) и определения основных понятий собраны на странице сведений карты знаний (на японском). Данные: JSON-LD / Turtle

2. Что стоит за числом MAX_PATH=260

Максимальная длина пути в Win32 API, за некоторыми исключениями, задана как MAX_PATH=260 символов. У этого числа есть состав. Локальный путь складывается из «буквы диска, двоеточия, обратной косой черты, имён, разделённых обратной косой чертой, и завершающего NUL» — например, для диска D максимум это «D:\ + 256 символов текста пути + завершающий NUL».1

Если разложить по частям, получается так.

Место Состав Пример Символов
Начало Буква диска + двоеточие + обратная косая черта D:\ 3
Середина Имена, разделённые обратной косой чертой (папки, файлы и разделители) 2026\проект\…\отчёт.xlsx до 256
Конец Завершающий NUL (на экране не виден) 1
  Итого   260 = MAX_PATH

Иными словами, если считать 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

  1. Значение реестра LongPathsEnabled (REG_DWORD) в разделе HKLM\SYSTEM\CurrentControlSet\Control\FileSystem равно 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

Дополним и шаги со стороны манифеста. XML с longPathAware выше — это фрагмент из официальной документации, сам по себе он файлом не является. На практике его кладут внутрь элемента assembly файла манифеста приложения (по соглашению app.manifest). В Visual Studio выберите «проект правой кнопкой > Добавить > Новый элемент» и добавьте «Файл манифеста приложения»: появится заготовка с настройками UAC, и в её элемент assembly дописывают элемент application.13

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <!-- Сюда Visual Studio подставляет 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 символов или когда ошибку возвращает сама ОС. Даже у существующего приложения с более старой целевой платформой это можно включить переключателями AppContext Switch.System.IO.BlockLongPaths=falseSwitch.System.IO.UseLegacyPathHandling=false, который отключает старую обработку путей).512
  • Чтобы приложение на .NET Framework на практике пропускало длинные пути, помимо настроек среды выполнения выше нужны включение длинных путей на стороне ОС и манифест вместе. Документация по поддержке длинных путей в NuGet.exe явно приводит эту конфигурацию (Windows 10 1607 и новее + манифест longPathAware + отключение UseLegacyPathHandling) как рабочий пример.15

3.4. Реальность: приложения без поддержки остаются и после этого

Даже проделав всё это, вы не заставите все приложения в мире работать с длинными путями. Официальная документация прямо пишет: «у оболочки и файловой системы разные требования; интерфейс оболочки может неверно разобрать путь, который удалось создать через Win32 API»,1 и на практике инструменты, которые не заявляют о поддержке длинных путей, действительно встречаются (например, документация NuGet отмечает, что restore через Visual Studio или msbuild -t: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»; или папку «aux», созданную на стороне Linux, не удаётся распаковать в 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». Наоборот, файл вроде «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 их нет).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 — API начиная с .NET Core 2.0, .NET Standard 2.1 и .NET 5; в .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 сравнивает пути по соглашению ОС по умолчанию. В 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;
}

Намерение этой функции проще увидеть по таблице входов и выходов — её же можно взять как первые случаи для модульных тестов.

Вход Выход Что срабатывает
отчёт_v2. отчёт_v2 Точку в конце снимает TrimEnd (опережаем молчаливое удаление при нормализации)
CON.txt _CON.txt CON без расширения — зарезервированное имя. Смотрим через Split('.')[0] и добавляем префикс
nul.tar.gz _nul.tar.gz Сравнение зарезервированных имён — OrdinalIgnoreCase. При двойном расширении смотрим первый элемент
COM1 и один пробел в конце _COM1 После TrimEnd имя становится зарезервированным. Поэтому проверку делают на итоговой форме
... _ После TrimEnd получилась пустая строка. Пустое имя файла не возвращаем
A/B:C.csv A_B_C.csv / и : из GetInvalidFileNameChars заменяем на _
150 символов + .csv первые 116 символов + .csv (всего 120) Обрезка по длине, расширение сохраняем

Четвёртая и пятая строки как раз объясняют комментарий в коде «проверку всегда делаем на итоговой форме». Если сначала не сделать замену, обрезку и TrimEnd, а потом смотреть зарезервированные имена и пустую строку, вход вроде COM1 с пробелом в конце проверку обойдёт.

С этой проблемой часто встречаются в именах файлов 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

Коротко про чтение dir /x из таблицы. /x — параметр «показать короткое имя в формате 8.3, если оно было сгенерировано для имени, которое в 8.3 не умещается»; формат вывода тот же, что у /n (длинное имя справа), и колонка короткого имени вставляется перед длинным.18 То есть порядок «дата время размер — короткое имя — длинное имя»: справа исходное имя, слева от него короткое (вида T97B4~1.TXT). Если имя изначально умещается в 8.3, короткое имя не создаётся, и эта колонка пустая. Когда путь упирается в лимит и файл не открывается, промежуточные папки можно указать этим коротким именем и тем самым ужать путь — это рабочая временная мера.

Как первый шаг диагностики полезно писать в журнал ошибок приложения сам путь, который пытались открыть, в абсолютном виде и в кавычках. Одного сообщения «файл не найден» недостаточно, чтобы потом понять, обрезали ли путь, изменилось ли имя из-за нормализации или смотрели вообще в другой каталог. Если записывать путь в кавычках, такие труднозаметные проблемы, как пробел в конце, видны сразу.

Неудачная загрузка 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).

Связанные статьи

Смежные области консультаций

В Komura Software LLC мы разбираем сбои файлового ввода-вывода вроде «не открывается только в определённой среде или только определённый файл», пересматриваем поддержку длинных путей и проверку имён файлов в существующих бизнес-приложениях и консультируем по проектированию обмена файлами в смешанных средах Windows и Linux.

Справочные материалы

  1. 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

  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 даёт поддержку длинных путей (до 32 тыс. символов) и снимает лимит в 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 с расширением эквивалентны зарезервированному; имя нельзя заканчивать пробелом или точкой; начальная точка допустима; не следует исходить из различения регистра, POSIX-семантика NTFS; поведение префикса \\?\ и требование 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. Различение регистра на уровне каталога (fsutil.exe file setCaseSensitiveInfo) начиная со сборки Windows 10 17107; для изменения нужны права администратора и пустой каталог; новые подкаталоги наследуют настройку; предупреждение о возможной некорректной работе приложений 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 символов или когда ошибку возвращает сама ОС.  2

  13. Microsoft Learn, Application Manifests. Манифест приложения — XML с корневым элементом assembly; элемент 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, манифест longPathAware плюс отключение UseLegacyPathHandling); restore в Visual Studio и msbuild длинные пути не поддерживает.  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, короткое имя вставляется перед длинным. 

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Японский календарь, праздники и даты закрытия в бизнес-приложениях — устойчивость к смене эры, JapaneseCalendar и расчёт рабочих дней

Разбираем обработку дат, характерную для японских бизнес-приложений: вывод по японскому календарю, расчёт рабочих дней без праздников и д...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Сколько символов может содержать путь в 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-функций работы с файлами. Одного из двух параметров недостаточно. Среда выполнения .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.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог