Long Paths in .NET, Part 2 of 3: Long Path Workarounds [Kim Hamilton]

For now, our suggested workaround for users that encounter the MAX_PATH issue is to rearrange directories so that the names are shorter. This may sound like a cop out, but this is ultimately easier on users because of (1) limited tool support (i.e. Explorer doesn't work with long paths) and (2) getting the full System.IO functionality for long paths results in a significant code delta for users. However, if you really want to work with paths longer than MAX_PATH you can, and this part of the series demonstrates how.

Recall from Part 1 that if you prefix the path with \\?\ and use the Unicode versions of the Win32 APIs, you can use paths up to 32K characters in length. These code samples will use that fact to show a few common file operations with long path files.

Deleting a File

Let's start with the simplest example – deleting a file. Recall that Explorer won't let you delete long path files, so you'll need this to clean up the files you create in the subsequent section.

First, we look at the Win32 API docs for DeleteFile and confirm that it supports long paths. DeleteFile does according to this comment:

In the ANSI version of this function, the name is limited to MAX_PATH characters. To extend this limit to 32,767 wide characters, call the Unicode version of the function and prepend "\\?\" to the path. For more information, see Naming a File.

So we specify the PInvoke signature:

using System;

using System.Runtime.InteropServices;

 

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]

[return: MarshalAs(UnmanagedType.Bool)]

internal static extern bool DeleteFile(string lpFileName);

And then all we have to do is call it with a file name prefixed by \\?\:

// This code snippet is provided under the Microsoft Permissive License.

public static void Delete(string fileName) {

    string formattedName = @"\\?\" + fileName;

    DeleteFile(formattedName);

}

For some tasks such as deleting, moving, and renaming a file, you simply PInvoke to the Win32 APIs and you're done. For other cases, such as writing to a file, you mix the Win32 calls with the System.IO APIs.

Writing to or Reading from a file

First you need to create or open a file with the Win32 CreateFile function. CreateFile returns a file handle, which you can pass to a System.IO.FileStream constructor. Then you simply work with the FileStream as normal.

// This code snippet is provided under the Microsoft Permissive License.

using System;

using System.IO;

using System.Runtime.InteropServices;

using Microsoft.Win32.SafeHandles;

 

[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]

internal static extern SafeFileHandle CreateFile(

    string lpFileName,

    EFileAccess dwDesiredAccess,

    EFileShare dwShareMode,

    IntPtr lpSecurityAttributes,

    ECreationDisposition dwCreationDisposition,

    EFileAttributes dwFlagsAndAttributes,

    IntPtr hTemplateFile);

 

public static void TestCreateAndWrite(string fileName) {

 

    string formattedName = @"\\?\" + fileName;

    // Create a file with generic write access

    SafeFileHandle fileHandle = CreateFile(formattedName,

        EFileAccess.GenericWrite, EFileShare.None, IntPtr.Zero,

        ECreationDisposition.CreateAlways, 0, IntPtr.Zero);

 

    // Check for errors

    int lastWin32Error = Marshal.GetLastWin32Error();

    if (fileHandle.IsInvalid) {

        throw new System.ComponentModel.Win32Exception(lastWin32Error);

    }

 

    // Pass the file handle to FileStream. FileStream will close the

    // handle

    using (FileStream fs = new FileStream(fileHandle,

                                    FileAccess.Write)) {

        fs.WriteByte(80);

        fs.WriteByte(81);

        fs.WriteByte(83);

        fs.WriteByte(84);

    }

}

This sample shows writing a few bytes, but once you have the FileStream, you can do anything you would normally do: wrap it in a BinaryWriter, etc.

If you wanted to open a file instead of creating it, you would change the creation disposition from CreateAlways to OpenExisting. If you wanted to read a file instead of writing, you would change the file access from GenericWrite to GenericRead.

See the end of the article for definitions of the enums and structs in this example.

Finding Files and Directories

So far the workarounds have been fairly minor, but suppose you want to get the files and folders contained in a folder. Unfortunately, now you're starting to rewrite the .NET libraries.

// This code snippet is provided under the Microsoft Permissive License.

using System;

using System.Collections.Generic;

using System.IO;

using System.Runtime.InteropServices;

using Microsoft.Win32.SafeHandles;

 

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]

internal static extern IntPtr FindFirstFile(string lpFileName, out

                                WIN32_FIND_DATA lpFindFileData);

 

[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]

internal static extern bool FindNextFile(IntPtr hFindFile, out

                                WIN32_FIND_DATA lpFindFileData);

 

[DllImport("kernel32.dll", SetLastError = true)]

[return: MarshalAs(UnmanagedType.Bool)]

internal static extern bool FindClose(IntPtr hFindFile);

 

// Assume dirName passed in is already prefixed with \\?\

public static List<string> FindFilesAndDirs(string dirName) {

 

    List<string> results = new List<string>();

    WIN32_FIND_DATA findData;

    IntPtr findHandle = FindFirstFile(dirName + @"\*", out findData);

 

    if (findHandle != INVALID_HANDLE_VALUE) {

        bool found;

        do {

            string currentFileName = findData.cFileName;

 

            // if this is a directory, find its contents

            if (((int)findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0) {

                if (currentFileName != "." && currentFileName != "..")

                {

                List<string> childResults = FindFilesAndDirs(Path.Combine(dirName, currentFileName));

                // add children and self to results

                results.AddRange(childResults);

                results.Add(Path.Combine(dirName, currentFileName));

                }

            }

 

            // it's a file; add it to the results

            else {

                results.Add(Path.Combine(dirName, currentFileName));

            }

 

            // find next

            found = FindNextFile(findHandle, out findData);

        }

        while (found);

    }

 

    // close the find handle

    FindClose(findHandle);

    return results;

}

Constants, Structs and Enums for the code samples

internal static IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);

internal static int FILE_ATTRIBUTE_DIRECTORY = 0x00000010;

internal const int MAX_PATH = 260;

 

[StructLayout(LayoutKind.Sequential)]

internal struct FILETIME {

    internal uint dwLowDateTime;

    internal uint dwHighDateTime;

};

 

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]

internal struct WIN32_FIND_DATA {

    internal FileAttributes dwFileAttributes;

    internal FILETIME ftCreationTime;

    internal FILETIME ftLastAccessTime;

    internal FILETIME ftLastWriteTime;

    internal int nFileSizeHigh;

    internal int nFileSizeLow;

    internal int dwReserved0;

    internal int dwReserved1;

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)]

    internal string cFileName;

    // not using this

    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]

    internal string cAlternate;

}

 

[Flags]

public enum EFileAccess : uint {

    GenericRead = 0x80000000,

    GenericWrite = 0x40000000,

    GenericExecute = 0x20000000,

    GenericAll = 0x10000000,

}

 

[Flags]

public enum EFileShare : uint {

    None = 0x00000000,

    Read = 0x00000001,

    Write = 0x00000002,

    Delete = 0x00000004,

}

 

public enum ECreationDisposition : uint {

    New = 1,

    CreateAlways = 2,

    OpenExisting = 3,

    OpenAlways = 4,

    TruncateExisting = 5,

}

 

[Flags]

public enum EFileAttributes : uint {

    Readonly = 0x00000001,

    Hidden = 0x00000002,

    System = 0x00000004,

    Directory = 0x00000010,

    Archive = 0x00000020,

    Device = 0x00000040,

    Normal = 0x00000080,

    Temporary = 0x00000100,

    SparseFile = 0x00000200,

    ReparsePoint = 0x00000400,

    Compressed = 0x00000800,

    Offline = 0x00001000,

    NotContentIndexed = 0x00002000,

    Encrypted = 0x00004000,

    Write_Through = 0x80000000,

    Overlapped = 0x40000000,

    NoBuffering = 0x20000000,

    RandomAccess = 0x10000000,

    SequentialScan = 0x08000000,

    DeleteOnClose = 0x04000000,

    BackupSemantics = 0x02000000,

    PosixSemantics = 0x01000000,

    OpenReparsePoint = 0x00200000,

    OpenNoRecall = 0x00100000,

    FirstPipeInstance = 0x00080000

}

 

[StructLayout(LayoutKind.Sequential)]

public struct SECURITY_ATTRIBUTES {

    public int nLength;

    public IntPtr lpSecurityDescriptor;

    public int bInheritHandle;

}

Update: The SizeConst of the WIN32_FIND_DATA.cAlternate member incorrectly stated 10 instead of 14 and has been revised.