Since April 2018, Windows 10 has had the ability to get or set if a directory was marked as case sensitive using fsutil.exe
.
Is there a way to programmatically query the case sensitivity of a directory without running fsutil.exe
or hackishly creating files with different casing to see if they collide?
I haven't really found any way to test this through research. I've read this is an actual NTFS attribute yet it doesn't show up when getting the file's attributes. I also noticed FindFirstFile will return the casing of the correct file if two different casings exist. Other than this I have no idea where to go as there really isn't a lot of information on this. This stuff is still pretty new.
As others have mentioned, making something case-sensitive is not a good idea in Windows due to comparability issues. I'm aware of that and my goal is to scan and work with existing case sensitive directories in the file system.
Progress:
I've discovered that Windows' FindFirstFile
and friends functions respects the case sensitivity of the directory even without using the FIND_FIRST_EX_CASE_SENSITIVE
. It will not return a file with invalid casing. Now I'm trying to figure out if there's a good way to make use of this.
Windows 10 now offers an optional case-sensitive file system, just like Linux and other UNIX-like operating systems. All Windows processes will handle case-sensitive files and folders properly if you enable this feature. In other words, they'll see “file” and “File” as two separate files.
Progress: I've discovered that Windows' FindFirstFile and friends functions respects the case sensitivity of the directory even without using the FIND_FIRST_EX_CASE_SENSITIVE . It will not return a file with invalid casing.
Windows file system treats file and directory names as case-insensitive.
Enable or Disable Case Sensitive Attribute for Folders in Windows 10: Although you can use Windows subsystem for Linux (WSL) which enables you to run native Linux command-line tools directly on Windows but the only drawback of this integration is how Windows handles the filename cases, as Linux is case sensitive ...
Here is my P/Invoke solution thanks to the help of @eryksun's comment.
Edit 2: Added SetDirectoryCaseSensitive()
Edit 3: Added IsDirectoryCaseSensitivitySupported()
I've implemented the the native method NtQueryInformationFile
while using the FILE_INFORMATION_CLASS
FileCaseSensitiveInformation
to read the FILE_CASE_SENSITIVE_INFORMATION
structure.
public static partial class NativeMethods {
public static readonly IntPtr INVALID_HANDLE = new IntPtr(-1);
public const FileAttributes FILE_FLAG_BACKUP_SEMANTICS = (FileAttributes) 0x02000000;
public enum NTSTATUS : uint {
SUCCESS = 0x00000000,
NOT_IMPLEMENTED = 0xC0000002,
INVALID_INFO_CLASS = 0xC0000003,
INVALID_PARAMETER = 0xC000000D,
NOT_SUPPORTED = 0xC00000BB,
DIRECTORY_NOT_EMPTY = 0xC0000101,
}
public enum FILE_INFORMATION_CLASS {
None = 0,
// Note: If you use the actual enum in here, remember to
// start the first field at 1. There is nothing at zero.
FileCaseSensitiveInformation = 71,
}
// It's called Flags in FileCaseSensitiveInformation so treat it as flags
[Flags]
public enum CASE_SENSITIVITY_FLAGS : uint {
CaseInsensitiveDirectory = 0x00000000,
CaseSensitiveDirectory = 0x00000001,
}
[StructLayout(LayoutKind.Sequential)]
public struct IO_STATUS_BLOCK {
[MarshalAs(UnmanagedType.U4)]
public NTSTATUS Status;
public ulong Information;
}
[StructLayout(LayoutKind.Sequential)]
public struct FILE_CASE_SENSITIVE_INFORMATION {
[MarshalAs(UnmanagedType.U4)]
public CASE_SENSITIVITY_FLAGS Flags;
}
// An override, specifically made for FileCaseSensitiveInformation, no IntPtr necessary.
[DllImport("ntdll.dll")]
[return: MarshalAs(UnmanagedType.U4)]
public static extern NTSTATUS NtQueryInformationFile(
IntPtr FileHandle,
ref IO_STATUS_BLOCK IoStatusBlock,
ref FILE_CASE_SENSITIVE_INFORMATION FileInformation,
int Length,
FILE_INFORMATION_CLASS FileInformationClass);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr CreateFile(
[MarshalAs(UnmanagedType.LPTStr)] string filename,
[MarshalAs(UnmanagedType.U4)] FileAccess access,
[MarshalAs(UnmanagedType.U4)] FileShare share,
IntPtr securityAttributes, // optional SECURITY_ATTRIBUTES struct or IntPtr.Zero
[MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
[MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
IntPtr templateFile);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CloseHandle(
IntPtr hObject);
public static bool IsDirectoryCaseSensitive(string directory, bool throwOnError = true) {
// Read access is NOT required
IntPtr hFile = CreateFile(directory, 0, FileShare.ReadWrite,
IntPtr.Zero, FileMode.Open,
FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
if (hFile == INVALID_HANDLE)
throw new Win32Exception();
try {
IO_STATUS_BLOCK iosb = new IO_STATUS_BLOCK();
FILE_CASE_SENSITIVE_INFORMATION caseSensitive = new FILE_CASE_SENSITIVE_INFORMATION();
NTSTATUS status = NtQueryInformationFile(hFile, ref iosb, ref caseSensitive,
Marshal.SizeOf<FILE_CASE_SENSITIVE_INFORMATION>(),
FILE_INFORMATION_CLASS.FileCaseSensitiveInformation);
switch (status) {
case NTSTATUS.SUCCESS:
return caseSensitive.Flags.HasFlag(CASE_SENSITIVITY_FLAGS.CaseSensitiveDirectory);
case NTSTATUS.NOT_IMPLEMENTED:
case NTSTATUS.NOT_SUPPORTED:
case NTSTATUS.INVALID_INFO_CLASS:
case NTSTATUS.INVALID_PARAMETER:
// Not supported, must be older version of windows.
// Directory case sensitivity is impossible.
return false;
default:
throw new Exception($"Unknown NTSTATUS: {(uint)status:X8}!");
}
}
finally {
CloseHandle(hFile);
}
}
}
Here is the implementation for setting the case sensitivity of a directory by implementing NTSetInformationFile
. (Which has a parameter list that is identical to NTQueryInformationFile
. Again, the problem was solved thanks to insight from @eryksun.
FILE_WRITE_ATTRIBUTES
is a FileAccess
flag that is not implemented in C#, so it needs to be defined and/or casted from the the value 0x100
.
partial class NativeMethods {
public const FileAccess FILE_WRITE_ATTRIBUTES = (FileAccess) 0x00000100;
// An override, specifically made for FileCaseSensitiveInformation, no IntPtr necessary.
[DllImport("ntdll.dll")]
[return: MarshalAs(UnmanagedType.U4)]
public static extern NTSTATUS NtSetInformationFile(
IntPtr FileHandle,
ref IO_STATUS_BLOCK IoStatusBlock,
ref FILE_CASE_SENSITIVE_INFORMATION FileInformation,
int Length,
FILE_INFORMATION_CLASS FileInformationClass);
// Require's elevated priviledges
public static void SetDirectoryCaseSensitive(string directory, bool enable) {
// FILE_WRITE_ATTRIBUTES access is the only requirement
IntPtr hFile = CreateFile(directory, FILE_WRITE_ATTRIBUTES, FileShare.ReadWrite,
IntPtr.Zero, FileMode.Open,
FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
if (hFile == INVALID_HANDLE)
throw new Win32Exception();
try {
IO_STATUS_BLOCK iosb = new IO_STATUS_BLOCK();
FILE_CASE_SENSITIVE_INFORMATION caseSensitive = new FILE_CASE_SENSITIVE_INFORMATION();
if (enable)
caseSensitive.Flags |= CASE_SENSITIVITY_FLAGS.CaseSensitiveDirectory;
NTSTATUS status = NtSetInformationFile(hFile, ref iosb, ref caseSensitive,
Marshal.SizeOf<FILE_CASE_SENSITIVE_INFORMATION>(),
FILE_INFORMATION_CLASS.FileCaseSensitiveInformation);
switch (status) {
case NTSTATUS.SUCCESS:
return;
case NTSTATUS.DIRECTORY_NOT_EMPTY:
throw new IOException($"Directory \"{directory}\" contains matching " +
$"case-insensitive files!");
case NTSTATUS.NOT_IMPLEMENTED:
case NTSTATUS.NOT_SUPPORTED:
case NTSTATUS.INVALID_INFO_CLASS:
case NTSTATUS.INVALID_PARAMETER:
// Not supported, must be older version of windows.
// Directory case sensitivity is impossible.
throw new NotSupportedException("This version of Windows does not support directory case sensitivity!");
default:
throw new Exception($"Unknown NTSTATUS: {(uint)status:X8}!");
}
}
finally {
CloseHandle(hFile);
}
}
}
Finally I have added a method to calculate once if the version of Windows supports case sensitive directories. This just creates a folder with a constant GUID
name in Temp and checks the NTSTATUS
result (so it can check a folder it knows it has access to).
partial class NativeMethods {
// Use the same directory so it does not need to be recreated when restarting the program
private static readonly string TempDirectory =
Path.Combine(Path.GetTempPath(), "88DEB13C-E516-46C3-97CA-46A8D0DDD8B2");
private static bool? isSupported;
public static bool IsDirectoryCaseSensitivitySupported() {
if (isSupported.HasValue)
return isSupported.Value;
// Make sure the directory exists
if (!Directory.Exists(TempDirectory))
Directory.CreateDirectory(TempDirectory);
IntPtr hFile = CreateFile(TempDirectory, 0, FileShare.ReadWrite,
IntPtr.Zero, FileMode.Open,
FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero);
if (hFile == INVALID_HANDLE)
throw new Exception("Failed to open file while checking case sensitivity support!");
try {
IO_STATUS_BLOCK iosb = new IO_STATUS_BLOCK();
FILE_CASE_SENSITIVE_INFORMATION caseSensitive = new FILE_CASE_SENSITIVE_INFORMATION();
// Strangely enough, this doesn't fail on files
NTSTATUS result = NtQueryInformationFile(hFile, ref iosb, ref caseSensitive,
Marshal.SizeOf<FILE_CASE_SENSITIVE_INFORMATION>(),
FILE_INFORMATION_CLASS.FileCaseSensitiveInformation);
switch (result) {
case NTSTATUS.SUCCESS:
return (isSupported = true).Value;
case NTSTATUS.NOT_IMPLEMENTED:
case NTSTATUS.INVALID_INFO_CLASS:
case NTSTATUS.INVALID_PARAMETER:
case NTSTATUS.NOT_SUPPORTED:
// Not supported, must be older version of windows.
// Directory case sensitivity is impossible.
return (isSupported = false).Value;
default:
throw new Exception($"Unknown NTSTATUS {(uint)result:X8} while checking case sensitivity support!");
}
}
finally {
CloseHandle(hFile);
try {
// CHOOSE: If you delete the folder, future calls to this will not be any faster
// Directory.Delete(TempDirectory);
}
catch { }
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With