|
| 1 | +#!/usr/bin/env python3 |
| 2 | +""" |
| 3 | +set directory case sensitive on Windows directly with ctypes |
| 4 | +
|
| 5 | +**requires WSL to be enabled** |
| 6 | +
|
| 7 | +thus for Windows 10 1903 and up |
| 8 | +""" |
| 9 | + |
| 10 | +from ctypes import ( |
| 11 | + WinDLL, |
| 12 | + c_void_p, |
| 13 | + get_last_error, |
| 14 | + WinError, |
| 15 | + c_int, |
| 16 | + Structure, |
| 17 | + sizeof, |
| 18 | + byref |
| 19 | +) |
| 20 | +from ctypes.wintypes import ( |
| 21 | + HANDLE, |
| 22 | + LPCSTR, |
| 23 | + DWORD, |
| 24 | + BOOL, |
| 25 | + LPVOID, |
| 26 | + ULONG |
| 27 | +) |
| 28 | +try: |
| 29 | + from enum import IntEnum |
| 30 | +except ImportError: |
| 31 | + IntEnum = int |
| 32 | +import os.path |
| 33 | + |
| 34 | +# ================================================================== |
| 35 | +# ======================== WinApi Bindings ========================= |
| 36 | +# ================================================================== |
| 37 | + |
| 38 | +# https://stackoverflow.com/questions/29847679/get-error-message-from-ctypes-windll |
| 39 | +kernel32 = WinDLL('kernel32', use_last_error=True) |
| 40 | + |
| 41 | + |
| 42 | +def _check_handle(h, *_): |
| 43 | + if h == INVALID_HANDLE_VALUE: |
| 44 | + raise WinError(get_last_error()) |
| 45 | + |
| 46 | + return h |
| 47 | + |
| 48 | + |
| 49 | +def _expect_nonzero(res, *_): |
| 50 | + if not res: |
| 51 | + raise WinError(get_last_error()) |
| 52 | + |
| 53 | + |
| 54 | +# https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilea |
| 55 | +FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 |
| 56 | +FILE_FLAG_POSIX_SEMANTICS = 0x01000000 |
| 57 | + |
| 58 | +OPEN_EXISTING = 3 |
| 59 | + |
| 60 | +FILE_SHARE_READ = 0x00000001 |
| 61 | +FILE_SHARE_WRITE = 0x00000002 |
| 62 | +FILE_SHARE_DELETE = 0x00000004 |
| 63 | +FILE_SHARE_VALID_FLAGS = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE |
| 64 | + |
| 65 | +""" |
| 66 | + HANDLE CreateFileA( |
| 67 | + [in] LPCSTR lpFileName, |
| 68 | + [in] DWORD dwDesiredAccess, |
| 69 | + [in] DWORD dwShareMode, |
| 70 | + [in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes, |
| 71 | + [in] DWORD dwCreationDisposition, |
| 72 | + [in] DWORD dwFlagsAndAttributes, |
| 73 | + [in, optional] HANDLE hTemplateFile |
| 74 | + ); |
| 75 | +""" |
| 76 | +_CreateFileA = kernel32.CreateFileA |
| 77 | +_CreateFileA.argtypes = [ |
| 78 | + LPCSTR, DWORD, DWORD, c_void_p, DWORD, DWORD, HANDLE |
| 79 | +] |
| 80 | +_CreateFileA.restype = HANDLE |
| 81 | +_CreateFileA.errcheck = _check_handle |
| 82 | + |
| 83 | +# https://learn.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights |
| 84 | +GENERIC_READ = 0x80000000 |
| 85 | +GENERIC_WRITE = 0x40000000 |
| 86 | + |
| 87 | +# https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfilenamew |
| 88 | +INVALID_HANDLE_VALUE = HANDLE(-1).value |
| 89 | + |
| 90 | +# https://learn.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle |
| 91 | +""" |
| 92 | + BOOL CloseHandle( |
| 93 | + [in] HANDLE hObject |
| 94 | + ); |
| 95 | +""" |
| 96 | +_CloseHandle = kernel32.CloseHandle |
| 97 | +_CloseHandle.argtypes = [HANDLE] |
| 98 | +_CloseHandle.restype = BOOL |
| 99 | +_CloseHandle.errcheck = _expect_nonzero |
| 100 | + |
| 101 | +# https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getfileinformationbyhandleex |
| 102 | +""" |
| 103 | + BOOL GetFileInformationByHandleEx( |
| 104 | + [in] HANDLE hFile, |
| 105 | + [in] FILE_INFO_BY_HANDLE_CLASS FileInformationClass, |
| 106 | + [out] LPVOID lpFileInformation, |
| 107 | + [in] DWORD dwBufferSize |
| 108 | + ); |
| 109 | +""" |
| 110 | +_GetFileInformationByHandleEx = kernel32.GetFileInformationByHandleEx |
| 111 | +_GetFileInformationByHandleEx.argtypes = [HANDLE, c_int, LPVOID, DWORD] |
| 112 | +_GetFileInformationByHandleEx.restype = BOOL |
| 113 | +_GetFileInformationByHandleEx.errcheck = _expect_nonzero |
| 114 | + |
| 115 | + |
| 116 | +# ===== The start of the undocumented craps... ===== |
| 117 | + |
| 118 | +# /mingw64/include/winbase.h |
| 119 | +class FILE_CASE_SENSITIVE_INFO(Structure): |
| 120 | + _fields_ = [ |
| 121 | + ('Flags', ULONG) |
| 122 | + ] |
| 123 | + |
| 124 | + |
| 125 | +FILE_INFO_BY_HANDLE = FILE_CASE_SENSITIVE_INFO |
| 126 | + |
| 127 | + |
| 128 | +# https://learn.microsoft.com/en-us/windows/win32/api/minwinbase/ne-minwinbase-file_info_by_handle_class |
| 129 | +class FILE_INFO_BY_HANDLE_CLASS(IntEnum): |
| 130 | + FileCaseSensitiveInfo = 23 |
| 131 | + |
| 132 | + |
| 133 | +# /mingw64/include/winnt.h |
| 134 | +# https://github.com/DDoSolitary/LxRunOffline/blob/bdc6d7d77f886c6dcdab3b4c9c136557ca6694c4/src/lib/fs.cpp#L89 |
| 135 | +FILE_CS_FLAG_CASE_SENSITIVE_DIR = 0x00000001 |
| 136 | + |
| 137 | +# https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-setfileinformationbyhandle |
| 138 | +""" |
| 139 | + BOOL SetFileInformationByHandle( |
| 140 | + [in] HANDLE hFile, |
| 141 | + [in] FILE_INFO_BY_HANDLE_CLASS FileInformationClass, |
| 142 | + [in] LPVOID lpFileInformation, |
| 143 | + [in] DWORD dwBufferSize |
| 144 | + ); |
| 145 | +""" |
| 146 | +_SetFileInformationByHandle = kernel32.SetFileInformationByHandle |
| 147 | +_SetFileInformationByHandle.argtypes = [HANDLE, c_int, LPVOID, DWORD] |
| 148 | +_SetFileInformationByHandle.restype = BOOL |
| 149 | +_SetFileInformationByHandle.errcheck = _expect_nonzero |
| 150 | + |
| 151 | + |
| 152 | +# ================================================================== |
| 153 | +# ======================= Wrappers functions ======================= |
| 154 | +# ================================================================== |
| 155 | + |
| 156 | +def CreateFileA(path: str, access: int, share: int, |
| 157 | + oflag: int, flags: int) -> HANDLE: |
| 158 | + return _CreateFileA( |
| 159 | + path.encode(encoding='utf-8'), |
| 160 | + access, share, |
| 161 | + None, # default |
| 162 | + oflag, flags, |
| 163 | + None # unused |
| 164 | + ) |
| 165 | + |
| 166 | + |
| 167 | +def CloseHandle(h: HANDLE): |
| 168 | + _CloseHandle(h) |
| 169 | + |
| 170 | + |
| 171 | +def GetFileInformationByHandleEx( |
| 172 | + h: HANDLE, |
| 173 | + kind: FILE_INFO_BY_HANDLE_CLASS |
| 174 | +) -> FILE_INFO_BY_HANDLE: |
| 175 | + if kind == FILE_INFO_BY_HANDLE_CLASS.FileCaseSensitiveInfo: |
| 176 | + dtype = FILE_CASE_SENSITIVE_INFO |
| 177 | + else: |
| 178 | + raise ValueError('Invalid file info class') |
| 179 | + |
| 180 | + data = dtype() |
| 181 | + |
| 182 | + _GetFileInformationByHandleEx(h, kind, |
| 183 | + byref(data), sizeof(dtype)) |
| 184 | + |
| 185 | + return data |
| 186 | + |
| 187 | + |
| 188 | +def SetFileInformationByHandle( |
| 189 | + h: HANDLE, |
| 190 | + kind: FILE_INFO_BY_HANDLE_CLASS, |
| 191 | + data: FILE_INFO_BY_HANDLE |
| 192 | +): |
| 193 | + if kind == FILE_INFO_BY_HANDLE_CLASS.FileCaseSensitiveInfo: |
| 194 | + dtype = FILE_CASE_SENSITIVE_INFO |
| 195 | + else: |
| 196 | + raise ValueError('Invalid file info class') |
| 197 | + |
| 198 | + _SetFileInformationByHandle(h, kind, |
| 199 | + byref(data), sizeof(dtype)) |
| 200 | + |
| 201 | + |
| 202 | +# ================================================================== |
| 203 | +# ======================== Helper functions ======================== |
| 204 | +# ================================================================== |
| 205 | + |
| 206 | +def open_dir_handle(path: str, access: int) -> HANDLE: |
| 207 | + return CreateFileA( |
| 208 | + path, access, |
| 209 | + FILE_SHARE_VALID_FLAGS, OPEN_EXISTING, |
| 210 | + FILE_FLAG_POSIX_SEMANTICS | FILE_FLAG_BACKUP_SEMANTICS |
| 211 | + ) |
| 212 | + |
| 213 | + |
| 214 | +# ================================================================== |
| 215 | +# ======================= Exported functions ======================== |
| 216 | +# ================================================================== |
| 217 | + |
| 218 | +def ensure_dir_case_sensitive(path: str): |
| 219 | + if not os.path.isdir(path): |
| 220 | + raise NotADirectoryError( |
| 221 | + f'Cannot set case sensitive for non-directory: {path}' |
| 222 | + ) |
| 223 | + |
| 224 | + h = open_dir_handle(path, GENERIC_READ) |
| 225 | + try: |
| 226 | + info: FILE_CASE_SENSITIVE_INFO = GetFileInformationByHandleEx( |
| 227 | + h, FILE_INFO_BY_HANDLE_CLASS.FileCaseSensitiveInfo |
| 228 | + ) |
| 229 | + |
| 230 | + if info.Flags & FILE_CS_FLAG_CASE_SENSITIVE_DIR: |
| 231 | + print(f'{path!r} is already case sensitive') |
| 232 | + else: |
| 233 | + h2 = open_dir_handle(path, GENERIC_WRITE) |
| 234 | + |
| 235 | + try: |
| 236 | + info.Flags |= FILE_CS_FLAG_CASE_SENSITIVE_DIR |
| 237 | + |
| 238 | + SetFileInformationByHandle( |
| 239 | + h2, |
| 240 | + FILE_INFO_BY_HANDLE_CLASS.FileCaseSensitiveInfo, |
| 241 | + info |
| 242 | + ) |
| 243 | + |
| 244 | + print(f'set case sensitive state for {path!r}') |
| 245 | + finally: |
| 246 | + CloseHandle(h2) |
| 247 | + finally: |
| 248 | + CloseHandle(h) |
| 249 | + |
0 commit comments