【问题标题】:Programmatically check Windows 10's case sensitive directory attribute以编程方式检查 Windows 10 的区分大小写的目录属性
【发布时间】:2018-09-06 14:20:30
【问题描述】:

自 2018 年 4 月以来,Windows 10 已经能够获取或设置使用 fsutil.exe 将目录标记为区分大小写。

有没有办法以编程方式查询目录的区分大小写,而无需运行 fsutil.exe 或不经意地创建具有不同大小写的文件以查看它们是否发生冲突?

我还没有真正找到任何方法来通过研究来测试这一点。我读过这是一个实际的 NTFS 属性,但在获取文件的属性时它没有显示出来。我还注意到,如果存在两个不同的大小写,FindFirstFile 将返回正确文件的大小写。除此之外,我不知道去哪里,因为这方面的信息真的不多。这东西还是很新的。

正如其他人所提到的,由于可比性问题,在 Windows 中区分大小写并不是一个好主意。我知道这一点,我的目标是扫描和使用文件系统中现有的区分大小写的目录。

进展:

我发现即使不使用FIND_FIRST_EX_CASE_SENSITIVE,Windows 的FindFirstFile 和朋友功能也会尊重目录的大小写敏感性。它不会返回大小写无效的文件。现在我想弄清楚是否有一个很好的方法来利用它。

【问题讨论】:

  • C++ 和 C# 是非常不同的语言。请选择一个。另外,到目前为止,您尝试过什么?
  • 我已经列出了 C# 和 C++,因为我正在使用 C#,但如果需要,我可以轻松地 P/Invoke C++ 中的某些内容。 (无论如何,我已经删除了 C++ 标签) 至于尝试,我还没有真正找到任何方法来测试它。我读过这是一个实际的 NTFS 属性,但在获取文件的属性时它没有显示出来。我还注意到,如果存在两个不同的大小写,FindFirstFile 将返回正确文件的大小写。除此之外,我不知道去哪里,因为这方面的信息真的不多。这东西还是很新的。
  • 在 Windows 中启用区分大小写 -> 98% 的程序损坏...
  • @trigger_death 哦,好的,感谢您的澄清。您可能可以edit 这个问题包含您在评论中发布的信息。
  • 它还没有在 Windows API 中公开,至少在 10.0.17134 标头中没有。您必须打开目录并调用本机 NT 函数 NtQueryInformationFile: FileCaseSensitiveInformation 才能读取目录的 FILE_CASE_SENSITIVE_INFORMATION

标签: c# windows winapi


【解决方案1】:

感谢@eryksun 的评论,这是我的 P/Invoke 解决方案。

编辑2:添加SetDirectoryCaseSensitive()

编辑 3: 添加了IsDirectoryCaseSensitivitySupported()

我在使用FILE_INFORMATION_CLASS FileCaseSensitiveInformation 读取FILE_CASE_SENSITIVE_INFORMATION 结构时实现了本机方法NtQueryInformationFile

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);
        }
    }
}

这里是通过实现NTSetInformationFile来设置目录区分大小写的实现。 (它的参数列表与NTQueryInformationFile 相同。再次感谢@eryksun 的见解,问题得到了解决。

FILE_WRITE_ATTRIBUTES 是一个未在 C# 中实现的 FileAccess 标志,因此需要从值 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);
        }
    }
}

最后,如果 Windows 版本支持区分大小写的目录,我添加了一种计算一次的方法。这只是在 Temp 中创建一个具有常量 GUID 名称的文件夹并检查 NTSTATUS 结果(因此它可以检查它知道它可以访问的文件夹)。

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 { }
        }
    }
}

【讨论】:

  • 一个不受支持的信息类更有可能以STATUS_INVALID_PARAMETER 失败。例如,请参阅 FatCommonQueryInformationCdCommonQueryInfo
  • 对于旧的Windows版本,返回值应该是STATUS_INVALID_INFO_CLASS,如果操作系统版本的信息类大于或等于FileMaximumInformation,则由I/O管理器自己立即返回.此外,我通过“\\localhost\C$\”尝试了重定向器,它返回 STATUS_NOT_SUPPORTED 以获得不受支持的信息类而不是 STATUS_INVALID_PARAMETER。其他人可能会返回STATUS_NOT_IMPLEMENTED。对于所有这些情况,我都会返回 false,因为这不是意外错误。
  • 在内核调试器(例如 kd.exe)中,您可以检查查询/设置 FileCaseSensitiveInformation 所需的访问权限。 ?? ((unsigned long *)@@(nt!IopQueryOperationAccess))[71]FILE_READ_ATTRIBUTES(即 0x80),?? ((unsigned long *)@@(nt!IopSetOperationAccess))[71]FILE_WRITE_ATTRIBUTES(即 0x100)。对于 0 期望访问,CreateFile 实际上使用了SYNCHRONIZE | FILE_READ_ATTRIBUTES,这就解释了为什么查询会成功。您需要请求FILE_WRITE_ATTRIBUTES 访问权限才能设置FileCaseSensitiveInformation
  • 仅供参考,为了在对象目录中创建 Section(即文件映射)和 SymbolicLink 对象,需要 SeCreateGlobalPrivilege 仅当它由不是当前会话的会话拥有时,除非该名称不受此限制检查因为它在ObpUnsecureGlobalNamesBuffer 中列出。特别是,全局“\BaseNamedObjects”归会话 0(即系统服务会话)所有,因此您需要 SeCreateGlobalPrivilege 通过CreateFileMapping 在此处创建一个具有全局名称(例如“Global\SharedMemory”)的 Section 对象。跨度>
  • 您在尝试设置FileCaseSensitiveInformation 时是否观察到STATUS_DIRECTORY_NOT_EMPTY?我还没有看到这个问题。在秋季更新中,我们甚至应该能够在单个系统调用中为所有子目录设置此标志。
猜你喜欢
  • 2019-08-17
  • 2019-10-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-07-11
  • 2014-08-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多