3

i am trying to get all directories from my usb drive using following code

DirectoryInfo di = new DirectoryInfo("H:\\");
DirectoryInfo[] allDir = di.GetDirectories();

this code works perfectly for directories with ascii names. but one directory has a name of " "(unicode value U+00A0). GetDirectories() cannot get that directory. is there a way to get directory wiht unicode name?

Mike Perrenoud
  • 66,820
  • 29
  • 157
  • 232
Zohaib Aslam
  • 585
  • 1
  • 4
  • 15
  • 1
    What happens? It just doesn't return it? – Mike Perrenoud May 22 '14 at 13:59
  • May be this can help http://stackoverflow.com/questions/1298136/file-paths-with-non-ascii-characters-and-fileinfo-in-c-sharp – tantry2002 May 22 '14 at 14:00
  • 1
    Try `Directory.EnumerateDirectories()`. – Farhad Alizadeh Noori May 22 '14 at 14:00
  • only null is returned :( – Zohaib Aslam May 22 '14 at 14:00
  • Did you tried the [P/invoke Unicode](http://www.pinvoke.net/default.aspx/kernel32/findfirstfile.html) version? – Venson May 22 '14 at 14:00
  • @FarhadAliNoo: internally `EnumerateDirectories` is the same call. – Mike Perrenoud May 22 '14 at 14:01
  • What filesystem is on your USB drive? The filesystem has to support Unicode. If it is FAT16 it won't. – Ben May 22 '14 at 14:11
  • CORRECTED: A file named only the Unicode Non-breaking space character is very odd indeed. Try passing `new DirectoryInfo("\\\\?\\H:\\");` instead... this bypasses a number of backwards-compatibility constraints. – Ben May 22 '14 at 14:31
  • Directory.EnumerateDirectories() worked for me. thanku @Farhad :) – Zohaib Aslam May 22 '14 at 18:48
  • OK I have tested this and on NTFS at least 1) normal `System.IO.Path` methods ***do create and return** Unicode filenames just fine. 2) You cannot create a directory using `CreateDirectory` with the name `(char)160`, but you can rename an existing directory to have that name using the `DirectoryInfo.Move` command. 3) If you have a directory named with the single character `(char)160` then it ***is returned by `GetDirectories`*** – Ben May 23 '14 at 11:37

1 Answers1

1

Alright, so what you're fighting is the .NET framework uses this signature of FindFirstFile:

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false)]
internal static SafeFindHandle FindFirstFile(string fileName, [In, Out] Win32Native.WIN32_FIND_DATA data);

To then add insult to injury, you're fighting the language:

Automatically marshal strings appropriately for the target operating system. The default is Unicode on Windows NT, Windows 2000, Windows XP, and the Windows Server 2003 family; the default is Ansi on Windows 98 and Windows Me. Although the common language runtime default is Auto, languages may override this default. For example, by default C# marks all methods and types as Ansi.

(from the CharSet enumeration documentation emphasis added)

The issue at hand is the CharSet parameter on the DllImport. This means that you're left with one approach; leverage the P/Invoke on your own. You'll need quite a few things. First you'll need the data structure that is returned:

[BestFitMapping(false)]
[Serializable]
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal class WIN32_FIND_DATA
{
  internal int dwFileAttributes;
  internal uint ftCreationTime_dwLowDateTime;
  internal uint ftCreationTime_dwHighDateTime;
  internal uint ftLastAccessTime_dwLowDateTime;
  internal uint ftLastAccessTime_dwHighDateTime;
  internal uint ftLastWriteTime_dwLowDateTime;
  internal uint ftLastWriteTime_dwHighDateTime;
  internal int nFileSizeHigh;
  internal int nFileSizeLow;
  internal int dwReserved0;
  internal int dwReserved1;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
  internal string cFileName;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
  internal string cAlternateFileName;

  [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
  public WIN32_FIND_DATA()
  {
  }
}

Next you'll need the right import for FindFirstFile:

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, BestFitMapping = false)]
internal static SafeFindHandle FindFirstFile(string fileName, [In, Out] Win32Native.WIN32_FIND_DATA data);

Next you'll need GetLastWin32Error to check for errors; that can be accessed via Marshal.GetLastWin32Error in the InteropServices namespace.

Next you're going to need to iterate, so you'll need FindNextFile:

[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, BestFitMapping = false)]
internal static bool FindNextFile(SafeFindHandle hndFindFile, [MarshalAs(UnmanagedType.LPStruct), In, Out] Win32Native.WIN32_FIND_DATA lpFindFileData);

Armed with these you can build your own iterator. However, for your benefit here is what Microsoft does for "init":

[SecurityCritical]
private void CommonInit()
{
  string fileName = Path.InternalCombine(this.searchData.fullPath, this.searchCriteria);
  Win32Native.WIN32_FIND_DATA wiN32FindData = new Win32Native.WIN32_FIND_DATA();
  this._hnd = Win32Native.FindFirstFile(fileName, wiN32FindData);
  if (this._hnd.IsInvalid)
  {
    int lastWin32Error = Marshal.GetLastWin32Error();
    switch (lastWin32Error)
    {
      case 2:
      case 18:
        this.empty = this.searchData.searchOption == SearchOption.TopDirectoryOnly;
        break;
      default:
        this.HandleError(lastWin32Error, this.searchData.fullPath);
        break;
    }
  }
  if (this.searchData.searchOption == SearchOption.TopDirectoryOnly)
  {
    if (this.empty)
    {
      this._hnd.Dispose();
    }
    else
    {
      SearchResult searchResult = this.CreateSearchResult(this.searchData, wiN32FindData);
      if (!this._resultHandler.IsResultIncluded(searchResult))
        return;
      this.current = this._resultHandler.CreateObject(searchResult);
    }
  }
  else
  {
    this._hnd.Dispose();
    this.searchStack.Add(this.searchData);
  }
}

and here is what they do for "iterate" for what you're looking for:

  if (this.searchData != null && this._hnd != null)
  {
    while (Win32Native.FindNextFile(this._hnd, wiN32FindData))
    {
      SearchResult searchResult = this.CreateSearchResult(this.searchData, wiN32FindData);
      if (this._resultHandler.IsResultIncluded(searchResult))
      {
        if (this.needsParentPathDiscoveryDemand)
        {
          this.DoDemand(this.searchData.fullPath);
          this.needsParentPathDiscoveryDemand = false;
        }
        this.current = this._resultHandler.CreateObject(searchResult);
        return true;
      }
    }
    int lastWin32Error = Marshal.GetLastWin32Error();
    if (this._hnd != null)
      this._hnd.Dispose();
    if (lastWin32Error != 0 && lastWin32Error != 18 && lastWin32Error != 2)
      this.HandleError(lastWin32Error, this.searchData.fullPath);
  }

NOTE: you're not going to be able to use this code directly, you'll have to fit it to your solution, but this is a huge jump start on the API's. Get you a copy of dotPeek to fill in the gaps.

NOTE: the fileName parameter accepted by FindFirstFile is going to the parent directory. In your case, H:\.

Mike Perrenoud
  • 66,820
  • 29
  • 157
  • 232
  • OK I have tested this and on NTFS at least 1) normal `System.IO.Path` methods ***do create and return** Unicode filenames just fine. 2) You cannot create a directory using `CreateDirectory` with the name `(char)160`, but you can rename an existing directory to have that name using the `DirectoryInfo.Move` command. 3) If you have a directory named with the single character `(char)160` then it ***is returned by `GetDirectories`*** – Ben May 23 '14 at 11:13