Wednesday, 18 June 2014

File name lengths can't reliably exceed 260 characters

It's astonishing to think that the developers of C# didn't take the opportunity to remove this arcane restriction especially since (most of) the underlying Windows APIs CAN!

Most Unicode Windows file-based APIs accept a canonicalised filename with the prefix '\\?\' or '\\?\UNC\' thus allowing file names of (approximately) 32,767 characters.

See: http://msdn.microsoft.com/en-gb/library/windows/desktop/aa365247(v=vs.85).aspx for details.

Here's a simple example of a replacement for File.Copy which uses this prefix and the relevant APIs.




  /// <summary>The prefix to use if the filename is a UNC path.</summary>
  /// <remarks>See http://msdn.microsoft.com/en-gb/library/windows/desktop/aa365247(v=vs.85).aspx for details</remarks>
  private const string LongUncPathPrefix = @"\\?\unc\";
 
  /// <summary>The prefix to use if the filename is rooted in a drive.</summary>
  /// <remarks>See http://msdn.microsoft.com/en-gb/library/windows/desktop/aa365247(v=vs.85).aspx for details</remarks>
  private const string LongDrivePathPrefix = @"\\?\";
 
  [DllImport("shlwapi.dll"CharSet = CharSet.AutoSetLastError = true)]
  private static extern bool PathCanonicalizeW(StringBuilder lpszDst, string lpszSrc);
 
  /// <summary>Convert the indicated filename to one which can exceed the 260 character limit.</summary>
  /// <param name="normalFilename">Filename to be converted.</param>
  /// <returns>The canonicalized filename with the appropriate 'long' filename prefix.</returns>
  /// <remarks>See http://msdn.microsoft.com/en-gb/library/windows/desktop/aa365247(v=vs.85).aspx for details</remarks>
  private static string ConvertToLongFilename(string normalFilename)
  {
    // check to see if the filename is already a 'long' filename
    if (normalFilename.StartsWith(LongUncPathPrefixtrueCultureInfo.InvariantCulture)
        || normalFilename.StartsWith(LongDrivePathPrefixtrueCultureInfo.InvariantCulture))
    {
      return normalFilename;
    }
 
    // can't use this on long filenames! ...
    //    normalFilename = Path.GetFullPath(normalFilename);
    // so use PathCanonicalizeW instead
    // NOTE: Canonicalization 'should' always return a string shorter than the original
    var sb = new StringBuilder(normalFilename.Length + 1);
    if (!PathCanonicalizeW(sb, normalFilename))
    {
      //// Debug.WriteLine(string.Format("Unable to canonicalize '{0}'", normalFilename));
 
      // failed to canonicalize this filename so hope for the best by returning the original filename
      return normalFilename;
    }
 
    // Prefix the canonicalized filename with the relevant prefix
    sb.Insert(
      0, 
      normalFilename.StartsWith(@"\\"StringComparison.Ordinal) ? LongUncPathPrefix : LongDrivePathPrefix);
 
    return sb.ToString();
  }
 
  [DllImport("kernel32.dll"CharSet = CharSet.AutoSetLastError = true)]
  private static extern bool CopyFileW(string src, string dst, bool failIfExists);
 
  /// <summary>Copy a local file.</summary>
  /// <param name="source">The source filename.</param>
  /// <param name="dest">The destination filename.</param>
  private static void CopyLocalFile(string source, string dest)
  {
    if (source == null)
    {
      throw new ArgumentNullException("source");
    }
 
    if (dest == null)
    {
      throw new ArgumentNullException("dest");
    }
 
    Trace.TraceInformation("Copying local file '{0}' to '{1}'", source, dest);
 
    source = ConvertToLongFilename(source);
    dest = ConvertToLongFilename(dest);
 
    // File.Copy doesn't cope with long filenames
    //   File.Copy(source, dest, true);
    // so use CopyFileW instead
    if (!CopyFileW(source, dest, false))
    {
      throw new Win32Exception(Marshal.GetLastWin32Error());
    }
  }
}

No comments: