Skip to content

Instantly share code, notes, and snippets.

@ynkdir
Last active May 20, 2026 12:10
Show Gist options
  • Select an option

  • Save ynkdir/9d52582df19b1580af6173e6f356f9be to your computer and use it in GitHub Desktop.

Select an option

Save ynkdir/9d52582df19b1580af6173e6f356f9be to your computer and use it in GitHub Desktop.
ntfs case sensitive
# /// 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