MAX_PATH y las trampas de rutas y nombres de archivo en Windows — el límite de 260 caracteres, nombres reservados, el punto final y las mayúsculas y minúsculas

· Actualizado el: · · MAX_PATH, Ruta de archivo, Ruta larga, Nombre de archivo, NTFS, Win32, C#, .NET, Desarrollo en Windows, Investigación de fallos, Consultoría técnica

«En el equipo de un usuario concreto aparece “no se encuentra el archivo”» o «se pudo copiar el archivo, pero luego no se puede abrir»: en la investigación de fallos de aplicaciones de negocio que manejan entrada y salida de archivos, no es raro que la causa final resulte ser la longitud de la ruta o el propio nombre del archivo. Los usuarios meten el nombre del proyecto y la fecha en el nombre de la carpeta, profundizan la jerarquía de directorios y superan con facilidad cualquier previsión que hayamos hecho.

Lo complicado es que las limitaciones de este terreno están repartidas en varias capas —«límite de la API de Win32», «límite del sistema de archivos», «límite del shell (Explorador de archivos)» y «límite del runtime de .NET»— y no siempre resulta obvio hasta dónde se puede llegar y qué queda sin resolver. En este artículo repasamos, junto con su tratamiento práctico en C#, desde la verdadera naturaleza de MAX_PATH=260 caracteres, pasando por las condiciones para manejar legalmente rutas largas, hasta las trampas de los nombres de archivo como los nombres reservados (CON y similares) o el punto final, y el tratamiento de las mayúsculas y minúsculas.

1. Conclusión inicial

  • MAX_PATH=260 es un límite de la API de Win32 que incluye «letra de unidad + dos puntos + barra invertida + hasta 256 caracteres de ruta + carácter NUL final». El lado del sistema de archivos (NTFS, etc.) admite rutas mucho más largas, y si se añade el prefijo \\?\ a la versión Unicode de la API se puede especificar un total de hasta unos 32.767 caracteres. 12
  • Para eliminar el límite de 260 caracteres, en Windows 10 versión 1607 o posterior hacen falta a la vez «LongPathsEnabled=1 en el registro» y «longPathAware en el manifiesto de la aplicación». Con solo uno de los dos no se activa. 3
  • El runtime de .NET (Core) / .NET 5 en adelante no realiza la comprobación de MAX_PATH y gestiona las rutas largas de forma implícita. .NET Framework elimina la comprobación de 260 caracteres del runtime cuando el destino (target) es 4.6.2 o posterior. 45
  • Sin embargo, en la práctica siguen existiendo aplicaciones sin compatibilidad con rutas largas. La propia documentación oficial indica explícitamente que el shell (Explorador de archivos) puede no interpretar correctamente una ruta que la API de Win32 sí es capaz de crear. 1
  • En los nombres de archivo no se pueden usar los caracteres < > : " / \ | ? * ni los caracteres de control (0 a 31), y CON, PRN, AUX, NUL, COM1 a COM9 y LPT1 a LPT9 se tratan como nombres reservados incluso con una extensión añadida (por ejemplo, CON.txt). 6
  • Los espacios y los puntos al final del nombre se eliminan silenciosamente durante la normalización de la ruta. Esto provoca incidentes como escribir «hoge.» y que el archivo termine llamándose «hoge», o no poder acceder desde Windows a un archivo con un espacio final creado por otro sistema operativo. 67
  • En los nombres de archivo de Windows, el comportamiento predeterminado es «conservar las mayúsculas y minúsculas, pero no distinguirlas». NTFS también admite la distinción por directorio (fsutil.exe file setCaseSensitiveInfo), pero tiene el efecto secundario de que las aplicaciones de Windows pueden no ser capaces de seguirle el ritmo. 89
  • En cuanto a la implementación, al combinar rutas hay que tener cuidado con el comportamiento de Path.Combine, que descarta los argumentos anteriores cuando uno de los siguientes lleva una raíz (en la familia .NET Core, Path.Join es también una opción), y los nombres de archivo procedentes de entrada de usuario se sanean con Path.GetInvalidFileNameChars más una comprobación propia de nombres reservados y caracteres finales. 1011

2. La verdadera naturaleza de MAX_PATH=260

En la API de Win32, la longitud máxima de una ruta se define, salvo algunas excepciones, como MAX_PATH=260 caracteres. Estos 260 caracteres tienen un desglose. Una ruta local se compone de «letra de unidad, dos puntos, barra invertida, la parte de nombres separada por barras invertidas y el carácter NUL final», de modo que, por ejemplo, para la unidad D el máximo es «D:\ + hasta 256 caracteres de ruta + NUL final». 1

El desglose, descompuesto, es el siguiente.

Posición Componente Ejemplo Caracteres
Inicio Letra de unidad + dos puntos + barra invertida D:\ 3
Medio Parte de nombres separada por barras invertidas (nombres de carpeta/archivo y separadores) 2026\proyecto\…\informe.xlsx hasta 256
Final Carácter NUL final (no visible en pantalla) 1
  Total   260 = MAX_PATH

Es decir, si se piensa que 260 caracteres es «la longitud disponible para el nombre de archivo», en realidad se están perdiendo 4 caracteres por la notación de la unidad y el NUL final. Como restricción adicional más fina, las API de creación de directorios exigen dejar margen para poder añadir después un nombre de archivo en formato 8.3, por lo que la ruta de un directorio no puede superar MAX_PATH − 12 caracteres. 1

Lo importante es que esto es un límite de la capa de la API de Win32, no un límite del sistema de archivos. NTFS admite nombres de archivo largos y rutas extendidas, y las versiones Unicode de muchas funciones de Win32 aceptan un total de hasta unos 32.767 caracteres de ruta extendida larga. El límite de cada componente individual que forma la ruta (un único nombre de carpeta o archivo) es el valor que devuelve GetVolumeInformation, y en general es de 255 caracteres. 12

Esta brecha entre «la API con 260» y «el sistema de archivos con unos 32.767» es precisamente el origen de los fallos que se ven en el terreno. Es perfectamente posible que se dé la situación asimétrica en la que una ruta que se pudo crear con una herramienta no se pueda abrir con otra herramienta (o con nuestra propia aplicación). Un ejemplo típico, que también aparece en la documentación oficial, es el de expandir un repositorio profundo con git clone en una carpeta de nombre largo y que después la compilación deje de funcionar. 1

Cabe señalar que, en el .NET Framework tradicional, cuando la ruta completa alcanzaba 260 caracteres o más, se lanzaba System.IO.PathTooLongException. Si se ve esta excepción, lo primero que hay que sospechar es la longitud de la ruta. 12

3. Cómo superar el muro de los 260 caracteres, y sus condiciones

Hay, a grandes rasgos, dos formas de manejar rutas largas: «el prefijo \\?\» y «la activación de rutas largas del sistema operativo».

3.1. El prefijo \\?\

Si se añade \\?\ al principio de la cadena de la ruta, la API de Win32 deja de analizar la cadena y la pasa tal cual al sistema de archivos. Esto permite superar el límite de MAX_PATH (las rutas UNC usan el formato \\?\UNC\servidor\recurso). Sin embargo, tiene condiciones y efectos secundarios. 16

  • Debe ser la versión Unicode de la API (las que terminan en W, o las que se invocan en UTF-16 como en .NET).
  • Como se omite la normalización, no se pueden usar separadores / ni referencias relativas con . o ... El prefijo \\?\ no se puede añadir a una ruta relativa, así que las rutas relativas siempre están limitadas a MAX_PATH. 1
  • No todas las API lo admiten; hay que comprobar la compatibilidad en la referencia de cada API. 6

3.2. Activación de rutas largas en Windows 10 1607 y posteriores: la condición es «ambas cosas»

A partir de Windows 10 versión 1607, se puede eliminar el límite de MAX_PATH en muchas funciones habituales de archivos y directorios de Win32 (CreateFileW, FindFirstFileW, GetFileAttributesW, etc.). Sin embargo, esto requiere que la aplicación se adhiera explícitamente (opt-in), y es necesario cumplir las dos condiciones siguientes a la vez. 3

  1. El valor de registro LongPathsEnabled (REG_DWORD) en HKLM\SYSTEM\CurrentControlSet\Control\FileSystem debe ser 1. También se puede configurar mediante la directiva de grupo «Configuración del equipo > Plantillas administrativas > Sistema > Sistema de archivos > Habilitar rutas de acceso largas de Win32».
  2. El manifiesto de la aplicación debe incluir el elemento 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>
# Lado del registro (requiere permisos de administrador)
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" `
-Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force

Conviene añadir también el procedimiento del lado del manifiesto. El XML de longPathAware mostrado arriba es un fragmento que aparece en la documentación oficial, y por sí solo no constituye un archivo. En la práctica, se coloca dentro del elemento assembly del archivo de manifiesto de la aplicación (por convención, app.manifest). En Visual Studio, si se añade un «Archivo de manifiesto de aplicación» desde «Clic derecho en el proyecto > Agregar > Nuevo elemento», se genera una plantilla que ya incluye la configuración de UAC, entre otras cosas, y basta con añadir dentro de su elemento assembly el elemento application como se muestra. 13

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <!-- Aquí va el trustInfo, etc., que genera VS -->
  <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>

Lo que vincula el elemento añadido con la compilación es la propiedad ApplicationManifest de MSBuild. Cuando se añade el elemento desde Visual Studio, normalmente se escribe de forma automática, pero si se edita el csproj directamente hay que añadir la siguiente línea (el manifiesto al que apunta esta propiedad se incrusta en el exe de forma predeterminada). 14

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

Las consultas del tipo «configuré el registro, pero no funciona» suelen deberse, en la mayoría de los casos, a que falta la parte del manifiesto. La documentación oficial también insiste en que «esta configuración del registro solo afecta a las aplicaciones que se han modificado para usar la nueva funcionalidad». Además, el valor del registro se almacena en caché por proceso en la primera llamada a una función de archivo, y no se vuelve a leer mientras el proceso está vivo. Para garantizar que el cambio de configuración se refleje en todas las aplicaciones puede ser necesario reiniciar. 3

3.3. Qué ocurre en .NET

  • .NET (Core) / .NET 5 en adelante: el runtime no realiza la comprobación de MAX_PATH y gestiona las rutas largas de forma implícita. No hace falta ningún código especial en la aplicación. 4
  • .NET Framework: si el destino (target) es 4.6.2 o posterior, se elimina la comprobación de 260 caracteres del runtime, y PathTooLongException queda limitada a los casos de «más de 32.767 caracteres» o «cuando el sistema operativo devuelve un error». Incluso en aplicaciones existentes con un destino anterior, se puede optar por esta funcionalidad mediante los conmutadores de AppContext Switch.System.IO.BlockLongPaths=false (junto con Switch.System.IO.UseLegacyPathHandling=false, que desactiva el procesamiento de rutas heredado). 512
  • Para que una aplicación de .NET Framework consiga pasar rutas largas en la práctica, además de la configuración de runtime anterior, hace falta combinar la activación de rutas largas del lado del sistema operativo con el manifiesto. La documentación de compatibilidad con rutas largas de NuGet.exe describe explícitamente, como ejemplo real, esta configuración (Windows 10 1607 o posterior + manifiesto longPathAware + UseLegacyPathHandling desactivado). 15

3.4. Aun así, la realidad de las «aplicaciones no compatibles»

Incluso haciendo todo esto, no todas las aplicaciones del mundo pasan a ser capaces de manejar rutas largas. La documentación oficial indica explícitamente que «el shell y el sistema de archivos tienen requisitos distintos, y la interfaz del shell puede no interpretar correctamente una ruta que se puede crear con la API de Win32»1, y, de hecho, siguen existiendo herramientas que no declaran compatibilidad con rutas largas (por ejemplo, la documentación de NuGet indica que el restore de Visual Studio o de msbuild -t:restore no admite rutas largas15). Aunque nuestra propia aplicación pueda crear un archivo con una ruta larga, que el usuario pueda abrirlo con el Explorador de archivos u otra herramienta es otra cuestión distinta. Las decisiones de diseño que tienen en cuenta esta asimetría se resumen en la tabla de decisión del capítulo 6.

4. Caracteres no permitidos, nombres de dispositivo reservados y el punto o espacio final

Otro campo minado, junto con la longitud de la ruta, son las propias reglas de los nombres de archivo. A partir de las reglas oficiales de nomenclatura, resumimos las que más fácilmente se pisan en aplicaciones de negocio. 6

Categoría Contenido Observaciones
Caracteres reservados < > : " / \ \| ? * Incluye el \ (y el /) de separador de ruta y los : de la unidad
Caracteres de control El valor entero 0 (NUL) y del 1 al 31 No permitidos, salvo dentro de flujos de datos alternativos
Nombres de dispositivo reservados CON, PRN, AUX, NUL, COM1 a COM9, LPT1 a LPT9 (y las variantes con superíndice COM¹ a ³, LPT¹ a ³) No permitidos ni siquiera con extensión (NUL.txt o NUL.tar.gz equivalen a NUL)
Caracteres finales Nombres que terminan en espacio o en punto El sistema de archivos los admite, pero el shell y la interfaz no

4.1. Nombres de dispositivo reservados: ni siquiera CON.txt vale

CON y NUL son nombres de dispositivo de la época de MS-DOS que siguen existiendo como nombres reservados en el espacio de nombres NT. Por eso no se puede crear un archivo llamado «CON» por los métodos habituales, y aunque se le añada una extensión, como en CON.txt, se sigue interpretando como el nombre reservado. 6 En el terreno, esto se manifiesta, por ejemplo, al intentar guardar un registro de integración con un puerto serie con un nombre como «COM1.log» y que falle, o al no poder extraer en Windows una carpeta llamada «aux» creada en el lado de Linux.

Como complemento, según las especificaciones de normalización de rutas, tradicionalmente una ruta que empezaba con un nombre reservado, como «CON» o «COM1.TXT», se convertía e interpretaba como una ruta de dispositivo (\\.\CON). En Windows 11 esta interpretación cambió, y para referirse a un dispositivo heredado ahora hace falta especificar la forma completa, como \\.\CON. 7 Sin embargo, la documentación oficial lo describe con la granularidad de «antes de Windows 11» / «en Windows 11 esto ya no se aplica», y no indica a partir de qué compilación o qué actualización cambió el comportamiento. 7 Al evaluar el alcance del impacto en un entorno mixto, hay que asumir que solo se puede acotar hasta el nivel de «Windows 10 o anterior» frente a «Windows 11 o posterior», no por compilación concreta. Aun así, como tanto los sistemas operativos antiguos como muchas aplicaciones existentes mantienen la interpretación tradicional, la conclusión de que conviene evitar los nombres reservados para los datos de negocio no cambia.

4.2. El espacio o el punto final desaparecen «en silencio»

Las reglas oficiales de nomenclatura establecen que «los nombres de archivo o de directorio no deben terminar en espacio ni en punto». 6 Yendo más al detalle, la normalización de rutas de Windows tiene una regla explícita: «si la ruta no termina en un carácter separador, se eliminan todos los puntos y espacios (U+0020) finales». 7

Lo complicado de esto en la práctica es que no se produce ningún error: el nombre cambia en silencio. Si un usuario escribe el nombre «informe v2.», lo que se crea es «informe v2». A la inversa, un archivo como «report » (con un espacio final) creado desde el lado de Linux a través de SMB no se puede alcanzar con la especificación de ruta habitual de Windows, porque la normalización cambia el nombre. El medio para acceder a estos «nombres legales que la normalización no permite alcanzar» es el prefijo \\?\, que omite la normalización. La documentación oficial también indica explícitamente este uso: «un archivo como hidden. no es accesible por ningún otro método». 7

Cabe señalar que un punto al principio del nombre sí es legal (nombres como .gitignore se pueden crear sin ningún problema). 6

5. Mayúsculas y minúsculas: «se conservan, pero no se distinguen»

El comportamiento predeterminado del sistema de archivos de Windows es case-preserving, case-insensitive (conserva las mayúsculas y minúsculas, pero no las distingue). Si se crea un archivo con el nombre Readme.txt, esa combinación de mayúsculas y minúsculas se conserva al mostrarlo, pero en la búsqueda o la comparación se ignora, de modo que README.TXT también llega al mismo archivo. La letra de unidad tampoco distingue mayúsculas de minúsculas. 86

Las reglas oficiales de nomenclatura indican explícitamente a los desarrolladores de aplicaciones que «no asuman que se distinguen mayúsculas y minúsculas (OSCAR, Oscar y oscar deben considerarse el mismo nombre)», y al mismo tiempo señalan que el propio NTFS admite la distinción de mayúsculas y minúsculas al estilo POSIX (aunque desactivada de forma predeterminada). 6

Esto sale a la luz en la integración con Linux. Desde la compilación 17107 de Windows 10, se puede habilitar la distinción de mayúsculas y minúsculas por directorio. 9

# En una PowerShell con permisos de administrador
fsutil.exe file setCaseSensitiveInfo C:\work\linux-src enable
fsutil.exe file queryCaseSensitiveInfo C:\work\linux-src

Es un recurso útil cuando se maneja en WSL un árbol de código fuente procedente de Linux (por ejemplo, donde coexisten Makefile y makefile), pero tiene efectos secundarios de los que advierte la propia documentación oficial. Las aplicaciones de Windows que asumen que el sistema de archivos no distingue mayúsculas y minúsculas pueden dejar de poder acceder a archivos en un directorio con la distinción activada. Además, el cambio del indicador solo se puede hacer si el directorio de destino está vacío, y los subdirectorios que se creen después heredan la configuración del directorio padre. 9 Históricamente también se ha documentado de forma oficial el fenómeno de que, cuando hay dos archivos cuyo nombre difiere únicamente en mayúsculas y minúsculas, el Explorador de archivos muestra los dos, pero, se elija el que se elija, solo se puede abrir uno de ellos. 9

Como diseño de aplicación de negocio, el equilibrio práctico consiste en tratar por defecto «en Windows, un nombre que solo difiere en mayúsculas y minúsculas es el mismo nombre», pero comprobar los conflictos de mayúsculas y minúsculas en los nombres de archivo que se van a pasar a Linux. En la integración con Linux también hay trampas de codificación de caracteres, incluso antes de llegar al nombre del archivo, así que conviene revisar también «Introducción a la codificación de caracteres en Windows: mojibake en la integración con Linux».

6. La práctica en aplicaciones de negocio: combinación de rutas, saneamiento y tabla de decisión

6.1. Usar Path.Combine conociendo sus especificaciones para combinar rutas

Concatenar rutas con + queda fuera de toda discusión, pero incluso Path.Combine tiene una especificación que conviene conocer. Si a partir del segundo argumento se pasa una ruta con raíz, todos los argumentos anteriores se ignoran. 10

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

// Si la entrada del usuario lleva raíz, baseDir se descarta silenciosamente
Path.Combine(baseDir, @"C:\Windows\secret.txt"); // → "C:\Windows\secret.txt"
Path.Combine(baseDir, @"\evil.txt");             // → "\evil.txt" (raíz de la unidad actual)

Si se pasa directamente como segundo argumento una cadena procedente de la entrada del usuario o de un archivo de configuración, se convierte en una vulnerabilidad que permite escribir fuera de la carpeta de guardado prevista. La documentación oficial también advierte de que este comportamiento puede derivar en un acceso no intencionado a archivos sensibles, y menciona como alternativa Path.Join / Path.TryJoin (no disponibles en .NET Framework). 1016 Se use lo que se use, la práctica estándar es verificar, en última instancia, si el resultado normalizado con Path.GetFullPath queda dentro del directorio base.

// Normalizamos también la ruta base y evaluamos convirtiendo a ruta relativa.
// Es más robusto frente a diferencias de separador final o de que la base sea la raíz de una unidad, en comparación con una coincidencia de prefijo de cadena
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)) // Si se sale a otra unidad o a una ruta UNC, se devuelve una ruta absoluta
{
    throw new InvalidOperationException("La ruta de guardado apunta fuera de la carpeta prevista.");
}

Cabe señalar que Path.GetRelativePath es una API disponible desde .NET Core 2.0, .NET Standard 2.1 y .NET 5 en adelante, y no existe en .NET Framework. 17 En el lado de Framework se sustituye por la forma de «normalizar la base, añadirle un separador final y evaluar mediante coincidencia de prefijo». El separador final es el punto clave: sin él, una comprobación de C:\App\Data clasificaría erróneamente C:\App\DataBackup como si estuviera dentro.

// Alternativa para .NET Framework (entornos sin Path.GetRelativePath)
var baseFull = Path.GetFullPath(baseDir);
if (!baseFull.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal))
{
    baseFull += Path.DirectorySeparatorChar;   // Evita que "C:\App\Data" coincida por prefijo con "C:\App\DataBackup"
}

var fullPath = Path.GetFullPath(Path.Combine(baseFull, userInput));
if (!fullPath.StartsWith(baseFull, StringComparison.OrdinalIgnoreCase)) // Se ignoran mayúsculas y minúsculas, según el comportamiento predeterminado de Windows
{
    throw new InvalidOperationException("La ruta de guardado apunta fuera de la carpeta prevista.");
}

Path.GetRelativePath compara rutas siguiendo la convención predeterminada del sistema operativo. Es decir, en Windows la comparación asume que no se distinguen mayúsculas y minúsculas, lo cual coincide con el comportamiento «en Windows, por defecto no se distinguen mayúsculas y minúsculas» que se explica en el apartado siguiente. Dicho de otro modo, en un lugar donde se ha habilitado la distinción de mayúsculas y minúsculas por directorio (véase el apartado siguiente), Data y data pueden llegar a ser directorios distintos, por lo que una comprobación que ignora mayúsculas y minúsculas deja margen para clasificar erróneamente como «dentro» una «carpeta distinta que solo difiere en mayúsculas y minúsculas». Si existe la posibilidad de manejar ese tipo de configuración, lo más seguro es adoptar la política de no aceptar como directorio base un lugar con la distinción activada.

Otro punto que conviene tener presente es que esta comprobación no es más que una evaluación sobre la ruta normalizada como cadena de texto. Si existen junctions o enlaces simbólicos dentro del directorio base, puede darse el caso de que, en la cadena, la ruta apunte dentro de la base, pero en realidad el destino real esté fuera de ella. Además, un enlace puede colarse no solo en el archivo final, sino también en una carpeta intermedia (con la forma base\enlace\archivo.txt), por lo que comprobar solo el extremo con File.ResolveLinkTarget no basta para detectarlo. Lo primero es evitar de entrada una configuración en la que un usuario no confiable pueda crear enlaces o junctions dentro de la base. Si además de eso se necesita una garantía estricta, hay que obtener la ruta definitiva a partir del identificador (handle) del archivo realmente abierto (con la función de Win32 GetFinalPathNameByHandle) y verificar que está dentro de la base, o bien examinar uno por uno cada componente de carpeta de la ruta para comprobar que no es un enlace.

6.2. Saneamiento de nombres de archivo procedentes de entrada de usuario

En funciones que construyen un nombre de archivo a partir de entrada de usuario, como «nombre del cliente + fecha.csv», conviene centralizar el saneamiento en un único punto. Path.GetInvalidFileNameChars es el punto de partida, pero la documentación oficial indica explícitamente que este array no garantiza el conjunto completo de caracteres no válidos. 11 Como esta API no detecta los nombres de dispositivo reservados ni el punto o el espacio finales, hay que añadir una comprobación propia.

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³ (con superíndice) también son nombres reservados
        "LPT¹","LPT²","LPT³",  // Lo mismo para 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(' ', '.');            // Se eliminan el espacio y el punto finales, ya que la normalización los descarta en silencio

    // También se ajusta al límite de longitud de un componente de nombre de archivo (habitualmente 255 caracteres).
    // Se recorta de forma conservadora, dejando margen para la jerarquía de carpetas y los sufijos que la aplicación pueda añadir después
    const int MaxNameLength = 120;
    if (name.Length > MaxNameLength)
    {
        var ext = Path.GetExtension(name);
        if (ext.Length > 20)
        {
            ext = ""; // Una "extensión" anormalmente larga no se conserva como extensión (evita la excepción por un rango negativo)
        }
        name = name[..(MaxNameLength - ext.Length)].TrimEnd(' ', '.') + ext;
    }

    // La comprobación de vacío/nombre reservado siempre se hace sobre la "forma final".
    // Esto captura los casos en los que el recorte o el TrimEnd terminan produciendo una cadena vacía o un nombre reservado (como NUL)
    var stem = name.Split('.')[0];            // Contramedida para NUL.txt: el nombre reservado se evalúa sobre la parte anterior a la extensión
    if (name.Length == 0 || ReservedNames.Contains(stem))
    {
        name = "_" + name;                    // Añadir un carácter sigue estando muy por debajo del límite de 255
    }
    return name;
}

La intención de esta función se entiende mejor como una tabla de correspondencia entre entrada y salida, así que la dejamos en un formato que se puede usar tal cual como primeros casos de una prueba unitaria.

Entrada Salida Procesamiento que actúa
informe_v2. informe_v2 El punto final se elimina con TrimEnd (se anticipa a que la normalización lo borre en silencio)
CON.txt _CON.txt CON, sin la extensión, es un nombre reservado. Se evalúa con Split('.')[0] y se añade un prefijo
nul.tar.gz _nul.tar.gz La comprobación de nombre reservado usa OrdinalIgnoreCase. Aunque haya doble extensión, se mira el primer elemento
COM1 con un espacio al final _COM1 El resultado de TrimEnd se convierte en un nombre reservado. Por eso la comprobación se hace sobre la forma final
... _ Caso en el que TrimEnd deja una cadena vacía. No se devuelve un nombre de archivo vacío
A/B:C.csv A_B_C.csv Los caracteres / y :, incluidos en GetInvalidFileNameChars, se sustituyen por _
150 caracteres + .csv Los primeros 116 caracteres + .csv (120 en total) Se recorta por el límite de longitud, conservando la extensión

Las filas 4 y 5 son exactamente la razón por la que el código comenta que «la comprobación siempre se hace sobre la forma final». Si no se completan antes la sustitución, el recorte y el TrimEnd, y solo después se comprueban el nombre reservado y la cadena vacía, una entrada como COM1 con un espacio final se cuela sin ser detectada.

Este problema se encuentra a menudo en los nombres de archivo de salida CSV, así que para la práctica del propio CSV conviene consultar también «El CSV no es «solo texto»: la práctica del CSV en aplicaciones de negocio en C#».

6.3. Las trampas de las rutas relativas y el directorio actual

Las rutas relativas tienen dos trampas. En primer lugar, como el directorio actual es una configuración por proceso, puede cambiarlo cualquier hilo en cualquier momento. La documentación oficial llega a decir que «las rutas relativas son peligrosas en aplicaciones multihilo», y desde .NET Core 2.1 se puede usar Path.GetFullPath(string, string), que permite indicar explícitamente la ruta base. 7 En segundo lugar, un formato como C:tmp.txt, sin barra invertida justo después de la letra de unidad, es una «ruta relativa al directorio actual de la unidad C», no una ruta absoluta. Esta «ruta relativa a la unidad» está señalada de forma explícita en la documentación oficial como una fuente habitual de errores en programas y scripts. 7

Conviene adoptar como hábito convertir a ruta absoluta con Path.GetFullPath en el momento en que se recibe una ruta procedente de un archivo de configuración o de la entrada de usuario, antes de registrarla en el log o de validarla.

6.4. Tabla de decisión: ¿conviene admitir rutas largas o rechazarlas en la entrada?

Situación Recomendación Motivo
Aplicación de negocio habitual en la que el usuario elige libremente el destino de guardado Validar y rechazar en la entrada (comprobar la longitud de la ruta completa y el nombre de archivo antes de guardar, y mostrar un error claro) Aunque la propia aplicación sea compatible, sigue existiendo el riesgo de que el Explorador de archivos u otras herramientas de integración no puedan abrir el archivo1
El lado que «lee» una jerarquía profunda creada por otros, como copias de seguridad, sincronización o extracción de archivos comprimidos Dar soporte a rutas largas (.NET Core y afines, con manifiesto si hace falta; en Framework, configuración 4.6.2+) No se puede controlar la entrada, y si no se puede leer, el trabajo se detiene54
El lado en el que la propia aplicación «crea» una jerarquía profunda Como norma, replantear el diseño para no crearla (aplanar la jerarquía, adoptar nombres con hash, etc.) Es muy probable que quien vaya a usar la ruta creada (una persona u otra aplicación) no sea compatible1
Intercambio de archivos con Linux/WSL Comprobar antes de la transferencia los nombres reservados, los conflictos de mayúsculas y minúsculas y los caracteres finales Se generan archivos inalcanzables desde el lado de Windows69
Generación de nombres de archivo a partir de entrada de usuario Centralizar en una función común el saneamiento con GetInvalidFileNameChars más la comprobación de nombres reservados y caracteres finales El array de la API por sí solo es incompleto11

7. Solución de problemas: «se ve en el Explorador de archivos, pero no se puede abrir»

Estos son los pasos para acotar consultas del tipo «el archivo se ve en el Explorador de archivos, pero al abrirlo con la aplicación aparece “no se encuentra el archivo”».

Punto a comprobar Método Si se cumple
Si la ruta completa ronda los 260 caracteres En PowerShell: (Get-ChildItem -Recurse).FullName \| Where-Object { $_.Length -ge 250 } Acortar el nombre de las carpetas superiores o valorar dar soporte a rutas largas (capítulo 3)
Si el nombre de archivo es un nombre reservado (aux, con, com1, etc.) Comprobación visual del nombre. Incluye también los que llevan extensión6 Cambiar el nombre (si el origen es Linux u otro sistema, convertir en el momento de la transferencia)
Si hay un espacio o un punto al final Comprobar con cmd /c dir /x o con una visualización entre comillas Eliminar o cambiar el nombre usando una ruta con el prefijo \\?\7
Si hay archivos con el mismo nombre que solo difieren en mayúsculas y minúsculas Es frecuente en carpetas procedentes de WSL/Git9 Cambiar el nombre de uno de ellos, o replantear el uso del directorio en cuestión
Si se están usando rutas relativas o rutas relativas a una unidad Registrar en el log, en forma de ruta absoluta, la ruta que realmente se intentó abrir Convertir a absoluta con Path.GetFullPath antes de usarla7

Vamos a añadir solo una aclaración sobre cómo leer el dir /x usado en la tabla. /x es una opción que «muestra el nombre corto generado para los nombres que no caben en el formato 8.3»; el formato de visualización es el mismo que /n (con el nombre a la derecha), y en él se inserta la columna del nombre corto justo antes del nombre largo. 18 Es decir, el orden es «fecha, hora, tamaño, nombre corto, nombre largo»: el que está más a la derecha es el nombre real, y a su izquierda está el nombre corto (con una forma como T97B4~1.TXT). Los archivos cuyo nombre ya cabe desde el principio en el formato 8.3 no generan un nombre corto, por lo que esa columna queda en blanco. Cuando la longitud de la ruta toca el límite y no se puede abrir, este nombre corto también sirve como remedio provisional: especificar una carpeta intermedia con su nombre corto para acortar la ruta.

Como primer paso de la investigación, recomendamos registrar en el log de errores de la aplicación «la propia ruta que se intentó abrir», en forma absoluta y entre comillas. Con un mensaje de excepción del tipo «no se encuentra el archivo» no basta para distinguir a posteriori si la ruta se truncó, si el nombre cambió por la normalización o si, de entrada, se estaba mirando otro directorio. Si se registra entre comillas, un problema difícil de ver a simple vista, como un espacio final, se detecta de un vistazo.

Cabe señalar que los fallos de carga de DLL también son habituales entre los errores del tipo «no se encuentra el archivo», pero en ese caso suele tratarse más de un problema de orden de búsqueda que de longitud de ruta; lo tratamos en «Cómo funciona la resolución de nombres de DLL en Windows: orden de búsqueda y SxS».

8. Resumen

  • MAX_PATH=260 es un límite de la API de Win32 que incluye «D:\ + hasta 256 caracteres + NUL final»; el propio NTFS puede manejar rutas extendidas largas de hasta unos 32.767 caracteres. Los directorios tienen además el límite adicional de MAX_PATH − 12.
  • Para superar los 260 caracteres hace falta el prefijo \\?\ (limitado a la versión Unicode de la API, no válido con rutas relativas) o la activación de rutas largas de Windows 10 1607 en adelante (el registro LongPathsEnabled y el manifiesto longPathAware, ambos a la vez).
  • .NET (Core)/5+ gestiona las rutas largas de forma implícita, y .NET Framework elimina la comprobación del runtime cuando el destino es 4.6.2 o posterior. Sin embargo, siguen existiendo aplicaciones no compatibles, incluido el Explorador de archivos, por lo que hay que decidir por separado «si se puede crear» y «si el usuario puede manejarlo».
  • En los nombres de archivo no se permiten los caracteres reservados (< > : " / \ | ? *) ni los caracteres de control; los nombres de dispositivo reservados como CON, NUL o COM1 tampoco se permiten con extensión, y el espacio o el punto finales desaparecen en silencio durante la normalización.
  • El comportamiento predeterminado es «conservar las mayúsculas y minúsculas, pero no distinguirlas». La distinción por directorio mediante fsutil file setCaseSensitiveInfo es útil en la integración con WSL, pero a cambio conlleva riesgo de fallos en el lado de las aplicaciones de Windows.
  • En la implementación, la práctica estándar consiste en validar el directorio base teniendo en cuenta la especificación de los argumentos con raíz de Path.Combine, centralizar el saneamiento con GetInvalidFileNameChars más la comprobación de nombres reservados y caracteres finales, y eliminar las rutas relativas convirtiéndolas a absolutas con Path.GetFullPath.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se ocupa de la investigación de fallos derivados de la entrada y salida de archivos, como «solo en determinado entorno o con determinado archivo no se puede abrir»; de la revisión del soporte de rutas largas y del diseño de validación de nombres de archivo en aplicaciones de negocio existentes, y de la consultoría de diseño para la integración de archivos en entornos mixtos de Windows y Linux.

Referencias

  1. Microsoft Learn, Maximum Path Length Limitation. Sobre la definición de MAX_PATH=260 y su desglose («letra de unidad + dos puntos + barra invertida + 256 caracteres + NUL final»), las rutas extendidas largas de hasta unos 32.767 caracteres mediante la versión Unicode de la API y el prefijo \\?\, la longitud de los componentes (en general 255 caracteres), que las rutas relativas siempre están limitadas a MAX_PATH, que la creación de directorios está limitada a MAX_PATH − 12, y que el shell y el sistema de archivos tienen requisitos distintos, de modo que la interfaz del shell puede no interpretar una ruta que sí se puede crear con Win32.  2 3 4 5 6 7 8 9 10 11

  2. Microsoft Learn, NTFS overview. Sobre la compatibilidad de NTFS con nombres de archivo largos y rutas extendidas largas de hasta unos 32.767 caracteres, y la compatibilidad retroactiva mediante alias 8.3.  2

  3. Microsoft Learn, Maximum Path Length Limitation ── Enable long paths in Windows 10, version 1607, and later. Sobre la necesidad de que, en Windows 10 1607 y posteriores, se cumplan a la vez el valor de registro LongPathsEnabled=1 y el elemento longPathAware del manifiesto de la aplicación, la configuración mediante directiva de grupo, que el valor del registro se almacena en caché por proceso, y la lista de funciones de Win32 en las que se elimina el límite.  2 3

  4. Microsoft Learn, File path formats on Windows systems ── Skip normalization. Sobre que .NET Core y .NET 5 en adelante procesan las rutas largas de forma implícita y no realizan la comprobación de MAX_PATH (esta comprobación es exclusiva de .NET Framework), y sobre el mecanismo por el que \\?\ omite la normalización.  2 3

  5. Microsoft Learn, Retargeting changes for migration to .NET Framework 4.6.x. Sobre que, al orientar el destino a .NET Framework 4.6.2, se admiten rutas largas (hasta 32K caracteres) y se elimina el límite de 260 caracteres, y que las aplicaciones con un destino anterior pueden adherirse mediante Switch.System.IO.BlockLongPaths=false.  2 3

  6. Microsoft Learn, Naming Files, Paths, and Namespaces. Sobre los caracteres reservados (< > : " / \ | ? *) y los caracteres de control (0 a 31), los nombres de dispositivo reservados (CON/PRN/AUX/NUL/COM1 a 9/LPT1 a 9 y variantes con superíndice), que un nombre con extensión como NUL.txt equivale al nombre reservado, que un nombre no debe terminar en espacio ni en punto, que un punto inicial es legal, que no debe asumirse distinción de mayúsculas y minúsculas y la semántica POSIX de NTFS, y el comportamiento del prefijo \\?\ y el requisito de API Unicode.  2 3 4 5 6 7 8 9 10 11 12

  7. Microsoft Learn, File path formats on Windows systems ── Path normalization. Sobre que la normalización de rutas elimina el punto y el espacio finales, que un nombre como hidden. solo es accesible mediante \\?\, la interpretación de los nombres de dispositivo heredados como CON y su cambio en Windows 11, que la ruta relativa a una unidad (C:tmp.txt) es una causa habitual de errores, que el directorio actual es por proceso y las rutas relativas son peligrosas en entornos multihilo, y sobre 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. Sobre que los nombres de directorio y de archivo conservan las mayúsculas y minúsculas con las que se crearon, mientras que la comparación de nombres las ignora.  2

  9. Microsoft Learn, Adjust case sensitivity. Sobre la distinción de mayúsculas y minúsculas por directorio desde la compilación 17107 de Windows 10 (fsutil.exe file setCaseSensitiveInfo), que el cambio requiere permisos de administrador y un directorio vacío, que los nuevos subdirectorios heredan la configuración, la advertencia de que las aplicaciones de Windows que asumen que no se distinguen mayúsculas y minúsculas pueden fallar, y que dos archivos cuyo nombre difiere solo en mayúsculas y minúsculas se mostraban ambos en el Explorador de archivos pero solo se podía abrir uno de ellos.  2 3 4 5 6

  10. Microsoft Learn, Path.Combine Method. Sobre que, si a partir del segundo argumento se incluye una ruta con raíz, se ignoran los elementos de ruta anteriores y se devuelve una cadena que empieza por el elemento con raíz, que esto puede derivar en un acceso no intencionado a archivos sensibles, y que se mencionan Join/TryJoin como alternativa (no disponibles en .NET Framework).  2 3

  11. Microsoft Learn, Path.GetInvalidFileNameChars Method. Sobre que devuelve un array de caracteres no válidos para nombres de archivo, y que ese array devuelto no garantiza el conjunto completo de caracteres no válidos, que puede variar según el sistema de archivos.  2 3

  12. Microsoft Learn, PathTooLongException Class. Sobre que se trata de una excepción lanzada cuando la ruta supera la longitud máxima definida por el sistema, y que en .NET Framework 4.6.2 y posteriores solo se lanza cuando se superan los 32.767 caracteres o cuando el sistema operativo devuelve un error.  2

  13. Microsoft Learn, Application Manifests. Sobre que el manifiesto de la aplicación es un XML con el elemento raíz assembly, y sobre la declaración de configuración en tiempo de ejecución mediante el elemento application y windowsSettings

  14. Microsoft Learn, Common MSBuild Project Properties. Sobre que la propiedad ApplicationManifest especifica la ruta del archivo de manifiesto, y que en muchos casos el manifiesto se incrusta en el ejecutable. 

  15. Microsoft Learn, Long Path Support (NuGet CLI). Sobre la configuración real necesaria para que las herramientas basadas en .NET Framework usen rutas largas (Windows 10 1607 o posterior, o 1511 + .NET Framework 4.6.2, la directiva de rutas largas de Win32, el manifiesto longPathAware más la desactivación de UseLegacyPathHandling), y que el restore de Visual Studio y de msbuild no admite rutas largas.  2

  16. Microsoft Learn, Path.Join Method. Sobre que Join concatena sin descartar una ruta posterior con raíz, y ejemplos de la diferencia de comportamiento con Combine. 

  17. Microsoft Learn, Path.GetRelativePath Method. Sobre las versiones compatibles (.NET Core 2.0 en adelante, .NET Standard 2.1, .NET 5 en adelante; no incluido en .NET Framework), y sobre el uso de la convención predeterminada de la plataforma para la comparación (OrdinalIgnoreCase en Windows y macOS, Ordinal en Linux). 

  18. Microsoft Learn, dir. Sobre que la opción /x muestra el nombre corto generado para los nombres que no tienen formato 8.3, que el formato de visualización es el mismo que /n y que el nombre corto se inserta justo antes del nombre largo. 

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Cuántos caracteres puede tener una ruta en Windows?
De forma predeterminada, la API de Win32 impone MAX_PATH=260 caracteres, una longitud que incluye «letra de unidad + dos puntos + barra invertida + hasta 256 caracteres de ruta + carácter NUL final». El propio sistema de archivos (por ejemplo, NTFS) admite rutas mucho más largas: si se pasa una ruta con el prefijo \\?\ a la versión Unicode de la API, se puede especificar un total de hasta unos 32.767 caracteres. Sin embargo, el nombre de una sola carpeta o archivo (componente) suele estar limitado a 255 caracteres, y las rutas relativas siempre están limitadas a MAX_PATH.
¿Cómo se puede eliminar el límite de 260 caracteres de MAX_PATH?
A partir de Windows 10 versión 1607, si se configuran a la vez el valor de registro LongPathsEnabled=1 (o la directiva de grupo «Habilitar rutas de acceso largas de Win32») y el elemento longPathAware del manifiesto de la aplicación, el límite de 260 caracteres desaparece en muchas funciones de archivo de Win32. Con solo uno de los dos no se activa. Los runtimes de .NET (Core) / .NET 5 en adelante no realizan la comprobación de MAX_PATH y gestionan las rutas largas de forma implícita, y .NET Framework elimina la comprobación de 260 caracteres del runtime si el destino (target) es 4.6.2 o posterior. Sin embargo, siguen existiendo aplicaciones sin compatibilidad con rutas largas, incluido el Explorador de archivos, por lo que la decisión debe tener en cuenta también quién va a manejar después la ruta larga creada.
¿Por qué no se pueden crear archivos llamados CON o NUL?
CON, PRN, AUX, NUL, COM1 a COM9, LPT1 a LPT9 y nombres similares son nombres de dispositivo reservados que se remontan a la época de MS-DOS, y Windows interpreta esos nombres como dispositivos, no como archivos. Añadir una extensión, como en NUL.txt, no evita el problema, porque se sigue tratando igual que NUL. En Windows 11 el comportamiento de interpretación de rutas cambió en parte, pero como los sistemas operativos antiguos y muchas aplicaciones mantienen la interpretación tradicional, sigue siendo prudente evitar estos nombres para los archivos de datos de negocio.
¿Windows distingue entre mayúsculas y minúsculas en los nombres de archivo?
Por defecto, el comportamiento es «conservar pero no distinguir» (case-preserving, case-insensitive). Si se crea un archivo con el nombre Readme.txt, esa combinación de mayúsculas y minúsculas se conserva al mostrarlo, pero intentar abrir README.TXT también llega al mismo archivo. NTFS también admite la distinción de mayúsculas y minúsculas al estilo POSIX, y desde la compilación 17107 de Windows 10 se puede habilitar esa distinción por directorio con fsutil.exe file setCaseSensitiveInfo; sin embargo, tiene el efecto secundario de provocar fallos en aplicaciones de Windows que asumen que no se distinguen mayúsculas y minúsculas, por lo que debe limitarse a los casos en que realmente se necesita, como la integración con WSL.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog