Last active
May 20, 2026 12:10
-
-
Save ynkdir/9d52582df19b1580af6173e6f356f9be to your computer and use it in GitHub Desktop.
ntfs case sensitive
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # /// script | |
| # dependencies = ["win32more"] | |
| # /// | |
| # | |
| # Ntfs preserves file names as is. | |
| # Windows API compares them using NFC normalization and case-insensitive matching. | |
| # | |
| # Directory has case sensitive flag. | |
| # With the flag enabled, the directory can contain file names that differ only by case. | |
| # fsutil.exe file setCaseSensitiveInfo <dir> | |
| # fsutil.exe file queryCaseSensitiveInfo <dir> | |
| import unittest | |
| from ctypes import pointer, sizeof | |
| from pathlib import Path | |
| from tempfile import TemporaryDirectory | |
| from win32more import Char, Int32, WinError | |
| from win32more.Windows.Wdk.Storage.FileSystem import FileCaseSensitiveInformation, NtSetInformationFile | |
| from win32more.Windows.Win32.Foundation import ( | |
| INVALID_HANDLE_VALUE, | |
| STATUS_SUCCESS, | |
| CloseHandle, | |
| ) | |
| from win32more.Windows.Win32.Globalization import ( | |
| COMPARE_STRING, | |
| LCMAP_LOWERCASE, | |
| LOCALE_NAME_INVARIANT, | |
| NLSVERSIONINFO, | |
| NLSVERSIONINFOEX, | |
| GetNLSVersionEx, | |
| LCMapStringEx, | |
| NormalizationC, | |
| NormalizeString, | |
| ) | |
| from win32more.Windows.Win32.Storage.FileSystem import ( | |
| FILE_FLAG_BACKUP_SEMANTICS, | |
| FILE_SHARE_DELETE, | |
| FILE_SHARE_READ, | |
| FILE_SHARE_WRITE, | |
| FILE_WRITE_ATTRIBUTES, | |
| OPEN_EXISTING, | |
| CreateFile, | |
| FileCaseSensitiveByNameInfo, | |
| GetFileInformationByName, | |
| ) | |
| from win32more.Windows.Win32.System.IO import IO_STATUS_BLOCK | |
| from win32more.Windows.Win32.System.SystemServices import ( | |
| FILE_CASE_SENSITIVE_INFORMATION, | |
| FILE_CS_FLAG_CASE_SENSITIVE_DIR, | |
| ) | |
| def query_case_sensitive_info(dirname: str) -> bool: | |
| info = FILE_CASE_SENSITIVE_INFORMATION() | |
| r = GetFileInformationByName(dirname, FileCaseSensitiveByNameInfo, pointer(info), sizeof(info)) | |
| if not r: | |
| raise WinError() | |
| return bool(info.Flags & FILE_CS_FLAG_CASE_SENSITIVE_DIR) | |
| def set_case_sensitive_info(dirname: str, case_sensitive: bool) -> None: | |
| h = CreateFile( | |
| dirname, | |
| FILE_WRITE_ATTRIBUTES, | |
| FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, | |
| None, | |
| OPEN_EXISTING, | |
| FILE_FLAG_BACKUP_SEMANTICS, | |
| None, | |
| ) | |
| if h == INVALID_HANDLE_VALUE: | |
| raise WinError() | |
| try: | |
| iosb = IO_STATUS_BLOCK() | |
| info = FILE_CASE_SENSITIVE_INFORMATION() | |
| if case_sensitive: | |
| info.Flags = FILE_CS_FLAG_CASE_SENSITIVE_DIR | |
| status = NtSetInformationFile(h, iosb, pointer(info), sizeof(info), FileCaseSensitiveInformation) | |
| if status != STATUS_SUCCESS: | |
| raise WinError(status) | |
| finally: | |
| CloseHandle(h) | |
| def normalize_string(s: str) -> str: | |
| buflen = Int32(0) | |
| r = NormalizeString(NormalizationC, s, len(s), None, buflen) | |
| if r <= 0: | |
| raise WinError() | |
| buflen.value = r | |
| buf = (Char * buflen.value)() | |
| r = NormalizeString(NormalizationC, s, len(s), buf, buflen) | |
| if r <= 0: | |
| raise WinError() | |
| return buf[:] | |
| def get_nls_version_ex() -> NLSVERSIONINFO: | |
| nls_version_info_ex = NLSVERSIONINFOEX() | |
| nls_version_info_ex.dwNLSVersionInfoSize = sizeof(nls_version_info_ex) | |
| if not GetNLSVersionEx(COMPARE_STRING, LOCALE_NAME_INVARIANT, nls_version_info_ex): | |
| raise WinError() | |
| return NLSVERSIONINFO.from_buffer(nls_version_info_ex) | |
| def lowercase_string(s: str) -> str: | |
| nls_version_info = get_nls_version_ex() | |
| buflen = Int32(0) | |
| r = LCMapStringEx(LOCALE_NAME_INVARIANT, LCMAP_LOWERCASE, s, len(s), None, buflen, nls_version_info, None, 0) | |
| if r == 0: | |
| raise WinError() | |
| buflen.value = r | |
| buf = (Char * buflen.value)() | |
| r = LCMapStringEx(LOCALE_NAME_INVARIANT, LCMAP_LOWERCASE, s, len(s), buf, buflen, nls_version_info, None, 0) | |
| if r == 0: | |
| raise WinError() | |
| return buf[:] | |
| # maybe | |
| def ntfs_compare_string(a: str, b: str) -> bool: | |
| a_nfc_lower = lowercase_string(normalize_string(a)) | |
| b_nfc_lower = lowercase_string(normalize_string(b)) | |
| return a_nfc_lower == b_nfc_lower | |
| class TestNtfsCaseSensitive(unittest.TestCase): | |
| def test_compare(self): | |
| self.assertTrue(ntfs_compare_string("a", "A")) | |
| self.assertFalse(ntfs_compare_string("ß", "ẞ")) | |
| def test_ntfs_is_case_insensitive_normally(self): | |
| with TemporaryDirectory() as dirname: | |
| self.assertFalse(query_case_sensitive_info(dirname)) | |
| a_txt = Path(dirname) / "a.txt" | |
| A_txt = Path(dirname) / "A.txt" | |
| a_txt.write_text("a.txt") | |
| A_txt.write_text("A.txt") | |
| self.assertEqual(a_txt.read_text(), "A.txt") | |
| self.assertEqual(A_txt.read_text(), "A.txt") | |
| self.assertEqual({p.name for p in Path(dirname).glob("*.txt")}, {"a.txt"}) | |
| def test_ntfs_can_be_case_sensitive(self): | |
| with TemporaryDirectory() as dirname: | |
| self.assertFalse(query_case_sensitive_info(dirname)) | |
| set_case_sensitive_info(dirname, True) | |
| self.assertTrue(query_case_sensitive_info(dirname)) | |
| a_txt = Path(dirname) / "a.txt" | |
| A_txt = Path(dirname) / "A.txt" | |
| a_txt.write_text("a.txt") | |
| A_txt.write_text("A.txt") | |
| self.assertEqual(a_txt.read_text(), "a.txt") | |
| self.assertEqual(A_txt.read_text(), "A.txt") | |
| self.assertEqual({p.name for p in Path(dirname).glob("*.txt")}, {"a.txt", "A.txt"}) | |
| def test_new_directory_inherit_parents_case_sensitive_flag(self): | |
| with TemporaryDirectory() as dirname: | |
| set_case_sensitive_info(dirname, True) | |
| subdir = Path(dirname) / "subdir" | |
| subdir.mkdir() | |
| self.assertTrue(query_case_sensitive_info(str(subdir))) | |
| # it is possible to disable case sensitive flag of subdirectory under case sensitive directory | |
| set_case_sensitive_info(str(subdir), False) | |
| self.assertFalse(query_case_sensitive_info(str(subdir))) | |
| if __name__ == "__main__": | |
| unittest.main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment