codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import os4import sys5from contextlib import suppress6from errno import EACCES7from pathlib import Path8from typing import cast9 10from ._api import BaseFileLock11from ._util import ensure_directory_exists, raise_on_not_writable_file12 13if sys.platform == "win32": # pragma: win32 cover14 import ctypes15 import msvcrt16 from ctypes import wintypes17 18 # Windows API constants for reparse point detection19 FILE_ATTRIBUTE_REPARSE_POINT = 0x0000040020 INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF21 22 # Load kernel32.dll23 _kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)24 _kernel32.GetFileAttributesW.argtypes = [wintypes.LPCWSTR]25 _kernel32.GetFileAttributesW.restype = wintypes.DWORD26 27 def _is_reparse_point(path: str) -> bool:28 """29 Check if a path is a reparse point (symlink, junction, etc.) on Windows.30 31 :param path: Path to check32 33 :returns: True if path is a reparse point, False otherwise34 35 :raises OSError: If GetFileAttributesW fails for reasons other than file-not-found36 37 """38 attrs = _kernel32.GetFileAttributesW(path)39 if attrs == INVALID_FILE_ATTRIBUTES:40 # File doesn't exist yet - that's fine, we'll create it41 err = ctypes.get_last_error()42 if err == 2: # noqa: PLR2004 # ERROR_FILE_NOT_FOUND43 return False44 if err == 3: # noqa: PLR2004 # ERROR_PATH_NOT_FOUND45 return False46 # Some other error - let caller handle it47 return False48 return bool(attrs & FILE_ATTRIBUTE_REPARSE_POINT)49 50 class WindowsFileLock(BaseFileLock):51 """52 Uses the :func:`msvcrt.locking` function to hard lock the lock file on Windows systems.53 54 Lock file cleanup: Windows attempts to delete the lock file after release, but deletion is55 not guaranteed in multi-threaded scenarios where another thread holds an open handle. The lock56 file may persist on disk, which does not affect lock correctness.57 """58 59 def _acquire(self) -> None:60 raise_on_not_writable_file(self.lock_file)61 ensure_directory_exists(self.lock_file)62 63 # Security check: Refuse to open reparse points (symlinks, junctions)64 # This prevents TOCTOU symlink attacks (CVE-TBD)65 if _is_reparse_point(self.lock_file):66 msg = f"Lock file is a reparse point (symlink/junction): {self.lock_file}"67 raise OSError(msg)68 69 flags = (70 os.O_RDWR # open for read and write71 | os.O_CREAT # create file if not exists72 )73 try:74 fd = os.open(self.lock_file, flags, self._open_mode())75 except OSError as exception:76 if exception.errno != EACCES: # has no access to this lock77 raise78 else:79 try:80 msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)81 except OSError as exception:82 os.close(fd) # close file first83 if exception.errno != EACCES: # file is already locked84 raise85 else:86 self._context.lock_file_fd = fd87 88 def _release(self) -> None:89 fd = cast("int", self._context.lock_file_fd)90 self._context.lock_file_fd = None91 msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)92 os.close(fd)93 94 with suppress(OSError):95 Path(self.lock_file).unlink()96 97else: # pragma: win32 no cover98 99 class WindowsFileLock(BaseFileLock):100 """Uses the :func:`msvcrt.locking` function to hard lock the lock file on Windows systems."""101 102 def _acquire(self) -> None:103 raise NotImplementedError104 105 def _release(self) -> None:106 raise NotImplementedError107 108 109__all__ = [110 "WindowsFileLock",111]112 