How to Get Windows 8.3 Short File Names Using FindFirstFileW (UNC) and GetShortPathName (local) in C#
Working with files and directories on Windows, especially on network shares, often leads to issues with long paths. This post explains what Windows 8.3 short paths are, how they work in NTFS, the difference between local and UNC paths, why browsers and some tools fail with long network paths, and how to programmatically obtain short file names for both local and UNC files.
First, a bit of color on why we need this for our machine learning workflow.
Use Case and Problem
At work we train all our machine learning image models in house on custom build servers featuring GPUs like NVIDIA RTX 4090. We have a lot of data - collected from production sites all over the world - and this data is usually split in two.
- Ground truth annotations are typically defined in
csv
-files that are stored in git repositories and published as versioned NuGet packages. These packages are then consumed by different pipelines that are run as Azure Pipelines on the servers. We then use Renovate to automatically bump versions of these. - Images are stored on a file server on a network share, and typically also cached at specific resolutions locally on the servers to speed up training.
At the same time the path to and file name of the images usually follows a schema so the path alone contains information relevant to a given image. This means paths can get quite long. Certainly above 260 characters on the file server.
Example path schema for directory:
1
\\fileserver\Pipelines\<PROJECTNAME>\<SETDATETIME>_<SETNAME>_<SITE>\<STATION>\
Example file name schema:
1
<DATETIME>_Camera=<CAMERANAME>_Id=<ID>_<DETAILS>.png
For example:
1
20250102.123456.789_Camera=Primary_Id=9999999_x=0123_y=0234_w=1234_h=2345.png
This is a simple pragmatic setup that works very well and a setup we have iterated on over many years and runs very smoothly. Each pipeline is a separate git repo that is easy to get started with by simply cloning and hitting F5 in Visual Studio or similar for local running.
As part of the output of the pipelines we generate a lot of data either as
simple csv
-files, plots png
-files or interactive reports in the form of
html
-files with inline JavaScript and data.
Below is an example showing box plots including all samples as dots for a regression model with different “levels”. The box plots are interactive and when pointing on a singular dot (e.g. a “minimum” outlier) the image for that sample will be shown on the right. Usually, but not here since if the path is too long (> 260 chars) the browser won’t show it.
This is where there can be issues since browsers do not support showing long
file paths, e.g. see chromium: Long file path handling not working on
Windows. And browsers do not
support extended length prefix \\?\
that one can otherwise use to escape the
MAX_PATH
260 char limit in Win32 APIs.
Now if you SHIFT right click on a file in Windows Explorer and click Copy as
path you will get the 8.3 short file name path if the file path is longer than
MAX_PATH
, as shown below both for a UNC file path and a local path name.
Copy as path for a local file resulting in
C:\Temp\LONGFI~1\VERYLO~1\202501~1.PNG
:
Copy as path for a network share file resulting in
\\files\Pipelines\LXF59T~G\V1WO8N~7\290O13~C.PNG
:
Similarly, if you right click and select Open with… and select a browser like Chrome then it will use the short path name for long file paths.
Hence, to fix the above interactive box plots we need to get the short path name
in C#, as this is what we use to generate the html
-files. This has the added
benefit of reducing the amount of data we have to store in the html
-file since
the path name is shorter (we already do some custom path compression on this
using a simple common prefix algorithm). Our data sets can have hundreds of
thousands of images so every byte counts.
Incidentally, it would have been nice if the browsers would support loading gzip’ed html e.g. files directly, but as far as I know they do not, and spinning up a web server just for this just seemed overkill.
So let’s take a quick look at Windows 8.3 short paths and some example code on how get them in C# for both local and UNC paths.
What Are Windows 8.3 Short Paths?
Windows 8.3 short paths (also called “short file names” or SFN) are a legacy
feature from MS-DOS and early Windows versions. They provide a way to represent
long file and directory names (introduced with Windows 95 and NTFS) in a format
compatible with older software that only supports 8-character filenames and
3-character extensions (e.g., MYDOCU~1.TXT
). See Naming Files, Paths, and
Namespaces - Short vs. Long
Names
or wikipedia 8.3 filename for
more. Example:
- Long name:
C:\Program Files\My Application\readme.txt
- Short name:
C:\PROGRA~1\MYAPPL~1\README.TXT
How Are 8.3 Short Paths Stored in NTFS?
NTFS stores both the long and short (8.3) names for each file and directory, if 8.3 name generation is enabled. The short name is generated when a file is created, following specific rules to ensure uniqueness.
- Short names are stored as metadata in the NTFS file system.
- You can disable 8.3 name creation for performance reasons, but this may break compatibility with legacy applications.
Local Paths vs. UNC Paths
- Local paths refer to files on a local drive, e.g.,
C:\folder\file.txt
. - UNC (Universal Naming Convention) paths refer to files on a network share, e.g.,
\\server\share\folder\file.txt
.
Key differences:
- Local paths are handled directly by the local file system.
- UNC Paths are resolved over the network, and some Windows APIs behave differently or have limitations with UNC paths.
Why Browsers and Some Tools Fail with Long Network Paths
Windows has a traditional MAX_PATH
limit of 260
characters
for file paths. While modern Windows versions and .NET can support longer paths
(with configuration), many tools - including browsers - do not support long UNC
paths due to:
- Lack of support for the
\\?\
extended-length path prefix. - Network shares (UNC) are not always handled with the same APIs as local paths.
- Browsers use standard Windows APIs that may not support long paths or UNC paths.
Getting the 8.3 Short Path Programmatically
For Local Paths
Use the Windows API function GetShortPathName:
1
2
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern uint GetShortPathName(string lpszLongPath, StringBuilder lpszShortPath, uint cchBuffer);
- Works for local file system paths (e.g.,
C:\...
). - Does not work for UNC paths.
For UNC Paths (Network Shares)
Use FindFirstFileW to retrieve the 8.3 name for each segment of the path:
- Iterate over each directory/file segment in the UNC path.
- For each, call
FindFirstFileW
and use thecAlternateFileName
field from the returnedWIN32_FIND_DATA
structure. - Rebuild the path using the 8.3 names.
Code for this including struct
definitions is shown below.
Summary Table
Type | API to Use | Notes |
---|---|---|
Local | GetShortPathName |
Direct, simple |
Network | FindFirstFileW |
Iterate segments |
Win32ShortPath Class
The Win32ShortPath
class shown below is factored into the following main
methods.
GetShortPath
- Entry point. Normalizes the path, checks if it exists, and dispatches to the correct method:- For local paths, calls
GetByGetShortPathName
. - For UNC paths, calls
GetByFindFirstFile
.
- For local paths, calls
GetByGetShortPathName
- Uses theGetShortPathName
Win32 API to get the short path for local files and directories.GetShortPathName
requires long paths to be “escaped” with the extended length prefix\\?\
.GetByFindFirstFile
- For UNC paths, splits the path into segments and usesFindFirstFileW
to retrieve the 8.3 short name for each segment, rebuilding the full short path.AppendPartsByFindFirstFile
- Iterates through each path segment, usingFindFirstFileW
to get the short name (cAlternateFileName) if available.
Note that it does not handle all edge cases including using /
as directory
separator or similar. It is also not the version we use internally as here we
already have the path defined as parts (typically) matching CSV-columns.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Text;
public static partial class Win32ShortPath
{
const int MaxPathLength = 260;
const int UncPrefixPartCount = 2;
static readonly char DirectorySeparator = Path.DirectorySeparatorChar;
const string UncPrefix = @"\\";
const string ExtendedLengthPrefix = @"\\?\";
/// <summary>
/// Returns the short (8.3) path for any file or directory if
/// available, covering both local and UNC paths.
/// </summary>
public static string GetShortPath(string longPath)
{
if (string.IsNullOrWhiteSpace(longPath))
{ throw new ArgumentNullException(nameof(longPath)); }
// Normalize
longPath = Path.GetFullPath(longPath);
if (!File.Exists(longPath) && !Directory.Exists(longPath))
{ throw new FileNotFoundException("Path not found", longPath); }
var isUnc = longPath.StartsWith(UncPrefix, StringComparison.Ordinal);
if (isUnc) { return GetByFindFirstFile(longPath); }
var isLong = longPath.Length > MaxPathLength;
if (isLong)
{
// Ensure starts with extended length prefix
var extendedPrefix = longPath.StartsWith(ExtendedLengthPrefix,
StringComparison.Ordinal);
if (!extendedPrefix)
{
longPath = ExtendedLengthPrefix + longPath;
}
var shortPathName = GetByGetShortPathName(longPath);
// Remove the extended length prefix if added
if (!extendedPrefix)
{
shortPathName = shortPathName[ExtendedLengthPrefix.Length..];
}
return shortPathName;
}
else
{
return GetByGetShortPathName(longPath);
}
}
// Local drive (C:\…), use GetShortPathName directly, for long prefix with \\?\
static string GetByGetShortPathName(string longPath)
{
var sb = new StringBuilder(MaxPathLength);
uint size = GetShortPathName(longPath, sb, (uint)sb.Capacity);
if (size == 0) { throw new Win32Exception(Marshal.GetLastWin32Error()); }
return sb.ToString();
}
// UNC path: \\server\share\…\file.ext, use FindFirstFile iteratively
static string GetByFindFirstFile(string longPath)
{
// Rebuild segment by segment, use FindFirstFile for the
// 8.3 name of each.
var parts = longPath.TrimStart(DirectorySeparator)
.Split(DirectorySeparator);
if (parts.Length < UncPrefixPartCount)
{ throw new ArgumentException($"Invalid UNC path '{longPath}'", nameof(longPath)); }
var sb = new StringBuilder(MaxPathLength);
// Re‑prefix with \\server\share
sb.Append(UncPrefix)
.Append(parts[0])
.Append(DirectorySeparator)
.Append(parts[1]);
AppendPartsByFindFirstFile(parts, UncPrefixPartCount, sb);
return sb.ToString();
}
static void AppendPartsByFindFirstFile(string[] parts, int partStart,
StringBuilder sb)
{
for (int i = partStart; i < parts.Length; i++)
{
var currentPart = parts[i];
var pathToQuery = $"{sb}{DirectorySeparator}{currentPart}";
var findHandle = FindFirstFileW(pathToQuery, out var findData);
sb.Append(DirectorySeparator);
if (findHandle != IntPtr.Zero)
{
FindClose(findHandle);
// If there's an alternate (8.3) name, use it
var alternatePart = findData.cAlternateFileName;
currentPart = string.IsNullOrEmpty(alternatePart)
? currentPart : alternatePart;
}
sb.Append(currentPart);
}
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern uint GetShortPathName(string lpszLongPath,
StringBuilder lpszShortPath, uint cchBuffer);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 1)]
struct WIN32_FIND_DATA
{
const int AlternateLength = 14;
public FileAttributes dwFileAttributes;
public long ftCreationTime;
public long ftLastAccessTime;
public long ftLastWriteTime;
public uint nFileSizeHigh;
public uint nFileSizeLow;
public uint dwReserved0;
public uint dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = MaxPathLength)]
public string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = AlternateLength)]
public string cAlternateFileName; // The 8.3 name
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern IntPtr FindFirstFileW(string lpFileName,
out WIN32_FIND_DATA lpFindFileData);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern IntPtr FindFirstFileW(StringBuilder lpFileName,
out WIN32_FIND_DATA lpFindFileData);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool FindClose(IntPtr hFindFile);
}
Example Program
Below is a simple example program using this with some (truncated) example paths that I created for testing:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System.Diagnostics;
Action<string> log = t => { Console.WriteLine(t); Trace.WriteLine(t); };
ReadOnlySpan<Test> tests =
[
// Unc path long
new(@"\\files\Pipelines\LongFilePathTest\VeryLong...\20250102.123456.789_Camera=Primary_Id=9999999_x=0123_y=0234_w=1234_h=2345.png",
@"\\files\Pipelines\LXF59T~G\V1WO8N~7\290O13~C.PNG"),
// Local path long
new(@"C:\Temp\LongFilePathTest\VeryLong...\20250102.123456.789_Camera=Primary_Id=9999999_x=0123_y=0234_w=1234_h=2345.png",
@"C:\Temp\LONGFI~1\VERYLO~1\202501~1.PNG"),
// Local path long with extended prefix
new(@"\\?\C:\Temp\LongFilePathTest\VeryLong...\20250102.123456.789_Camera=Primary_Id=9999999_x=0123_y=0234_w=1234_h=2345.png",
@"\\?\C:\Temp\LONGFI~1\VERYLO~1\202501~1.PNG"),
// Local path not long
new(@"C:\Temp\LongFilePathTest\NotLong\20250102.123456.789_Camera=Primary_Id=9999999_x=0123_y=0234_w=1234_h=2345.png",
@"C:\Temp\LONGFI~1\NotLong\202501~1.PNG"),
];
foreach (var t in tests)
{
log($"Long Path: '{t.Path}'");
log($"File Exists: {File.Exists(t.Path)}");
var shortPath = Win32ShortPath.GetShortPath(t.Path);
log($"Short Path: '{shortPath}'");
log(t.ShortPathExpected == shortPath ? "PASSED" : "FAILED");
log("");
}
public record Test(string Path, string ShortPathExpected);
When run, this will output the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Long Path: '\\files\Pipelines\LongFilePathTest\VeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVery\20250102.123456.789_Camera=Primary_Id=9999999_x=0123_y=0234_w=1234_h=2345.png'
File Exists: True
Short Path: '\\files\Pipelines\LXF59T~G\V1WO8N~7\290O13~C.PNG'
PASSED
Long Path: 'C:\Temp\LongFilePathTest\VeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVery\20250102.123456.789_Camera=Primary_Id=9999999_x=0123_y=0234_w=1234_h=2345.png'
File Exists: True
Short Path: 'C:\Temp\LONGFI~1\VERYLO~1\202501~1.PNG'
PASSED
Long Path: '\\?\C:\Temp\LongFilePathTest\VeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVeryLongVery\20250102.123456.789_Camera=Primary_Id=9999999_x=0123_y=0234_w=1234_h=2345.png'
File Exists: True
Short Path: '\\?\C:\Temp\LONGFI~1\VERYLO~1\202501~1.PNG'
PASSED
Long Path: 'C:\Temp\LongFilePathTest\NotLong\20250102.123456.789_Camera=Primary_Id=9999999_x=0123_y=0234_w=1234_h=2345.png'
File Exists: True
Short Path: 'C:\Temp\LONGFI~1\NotLong\202501~1.PNG'
PASSED
Considerations
8.3 short path names on Windows are not guaranteed to be stable across repeated file deletions and recreations — even if the long file name remains the same.
- 8.3 short names are dynamically assigned by the Windows file system (usually
NTFS) and are not deterministic. If you delete a file and create a new one
with the same long name, the 8.3 name might:
- Remain the same (if no name conflict occurs),
- Change (if the name was reused or reclaimed for another file),
- Or not be created at all (if 8.3 name generation is disabled or restricted).
- Name conflicts are resolved by appending a numeric suffix (e.g.,
LONGFI~1.TXT
,LONGFI~2.TXT
), and the exact suffix depends on what already exists in the directory at the time of file creation.
Links and Further Reading
- Naming Files, Paths, and Namespaces (Microsoft Docs)
- Maximum Path Length Limitation (Microsoft Docs)
- GetShortPathName function (Microsoft Docs)
- FindFirstFileW function (Microsoft Docs)
- Long Filename Specification
- The Definitive Guide on Win32 to NT Path Conversion
- Why Disabling the Creation of 8.3 DOS File Names Will Not Improve Performance. Or Will It?
That’s all!