diff --git a/parsec/core/fs/utils.py b/parsec/core/fs/utils.py index c72939a8832..ae8213c4577 100644 --- a/parsec/core/fs/utils.py +++ b/parsec/core/fs/utils.py @@ -1,6 +1,7 @@ # Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2019 Scille SAS -import os +import enum + from parsec.api.data import WorkspaceManifest, FileManifest, FolderManifest, Manifest from parsec.core.types import ( LocalUserManifest, @@ -10,18 +11,23 @@ LocalManifest, ) -# Cross-plateform windows error enumeration -if os.name == "nt": - from winfspy.plumbing.winstuff import NTSTATUS as ntstatus -else: +# Cross-plateform windows error enumeration - class NTSTATUS: - def __getattr__(self, key): - assert key.startswith("STATUS_") - return None - ntstatus = NTSTATUS() +class ntstatus(enum.IntEnum): + STATUS_INVALID_HANDLE = 0xC0000008 + STATUS_INVALID_PARAMETER = 0xC000000D + STATUS_END_OF_FILE = 0xC0000011 + STATUS_ACCESS_DENIED = 0xC0000022 + STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034 + STATUS_OBJECT_NAME_COLLISION = 0xC0000035 + STATUS_MEDIA_WRITE_PROTECTED = 0xC00000A2 + STATUS_FILE_IS_A_DIRECTORY = 0xC00000BA + STATUS_NOT_SAME_DEVICE = 0xC00000D4 + STATUS_DIRECTORY_NOT_EMPTY = 0xC0000101 + STATUS_NOT_A_DIRECTORY = 0xC0000103 + STATUS_HOST_UNREACHABLE = 0xC000023D # TODO: remove those methods ? diff --git a/parsec/core/mountpoint/manager.py b/parsec/core/mountpoint/manager.py index c7e9510fb24..e82b180abbc 100644 --- a/parsec/core/mountpoint/manager.py +++ b/parsec/core/mountpoint/manager.py @@ -21,6 +21,7 @@ MountpointNotMounted, MountpointDisabled, ) +from parsec.core.mountpoint.winify import winify_entry_name def get_mountpoint_runner(): @@ -71,12 +72,9 @@ def __init__(self, user_fs, event_bus, base_mountpoint_path, config, runner, nur self._timestamped_workspacefs = {} if os.name == "nt": - from parsec.core.mountpoint.winfsp_operations import winify_entry_name - self._build_mountpoint_path = lambda base_path, parts: base_path / "\\".join( winify_entry_name(x) for x in parts ) - else: self._build_mountpoint_path = lambda base_path, parts: base_path / "/".join(parts) diff --git a/parsec/core/mountpoint/winfsp_operations.py b/parsec/core/mountpoint/winfsp_operations.py index df0b2764799..bab236f88db 100644 --- a/parsec/core/mountpoint/winfsp_operations.py +++ b/parsec/core/mountpoint/winfsp_operations.py @@ -1,6 +1,5 @@ # Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2019 Scille SAS -import re import functools from contextlib import contextmanager from trio import Cancelled, RunFinishedError @@ -16,6 +15,7 @@ from parsec.core.types import FsPath from parsec.core.fs import FSLocalOperationError, FSRemoteOperationError from parsec.core.fs.workspacefs.sync_transactions import DEFAULT_BLOCK_SIZE +from parsec.core.mountpoint.winify import winify_entry_name, unwinify_entry_name logger = get_logger() @@ -25,85 +25,6 @@ FILE_WRITE_DATA = 1 << 1 -# https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file -# tl;dr: https://twitter.com/foone/status/1058676834940776450 -_WIN32_RES_CHARS = tuple(chr(x) for x in range(1, 32)) + ( - "<", - ">", - ":", - '"', - "\\", - "|", - "?", - "*", -) # Ignore `\` -_WIN32_RES_NAMES = ( - "CON", - "PRN", - "AUX", - "NUL", - "COM1", - "COM2", - "COM3", - "COM4", - "COM5", - "COM6", - "COM7", - "COM8", - "COM9", - "LPT1", - "LPT2", - "LPT3", - "LPT4", - "LPT5", - "LPT6", - "LPT7", - "LPT8", - "LPT9", -) - - -def winify_entry_name(name: str) -> str: - prefix, *suffixes = name.split(".", 1) - if prefix in _WIN32_RES_NAMES: - full_suffix = f".{'.'.join(suffixes)}" if suffixes else "" - name = f"{prefix[:-1]}~{ord(prefix[-1]):02x}{full_suffix}" - - else: - for reserved in _WIN32_RES_CHARS: - name = name.replace(reserved, f"~{ord(reserved):02x}") - - if name[-1] in (".", " "): - name = f"{name[:-1]}~{ord(name[-1]):02x}" - - return name - - -def unwinify_entry_name(name: str) -> str: - # Given / is not allowed, no need to check if path already contains it - if "~" not in name: - return name - - else: - *to_convert_parts, last_part = re.split(r"(~[0-9A-Fa-f]{2})", name) - converted_parts = [] - is_escape = False - for part in to_convert_parts: - if is_escape: - converted_chr = chr(int(part[1:], 16)) - if converted_chr in ("/", "\x00"): - raise ValueError("Invalid escaped value") - converted_parts.append(converted_chr) - - else: - converted_parts.append(part) - - is_escape = not is_escape - - converted_parts.append(last_part) - return "".join(converted_parts) - - def _winpath_to_parsec(path: str) -> FsPath: # Given / is not allowed, no need to check if path already contains it return FsPath(unwinify_entry_name(path.replace("\\", "/"))) diff --git a/parsec/core/mountpoint/winify.py b/parsec/core/mountpoint/winify.py new file mode 100644 index 00000000000..a2861332fa4 --- /dev/null +++ b/parsec/core/mountpoint/winify.py @@ -0,0 +1,81 @@ +# Parsec Cloud (https://parsec.cloud) Copyright (c) AGPLv3 2019 Scille SAS + +import re + +# https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file +# tl;dr: https://twitter.com/foone/status/1058676834940776450 +_WIN32_RES_CHARS = tuple(chr(x) for x in range(1, 32)) + ( + "<", + ">", + ":", + '"', + "\\", + "|", + "?", + "*", +) # Ignore `\` +_WIN32_RES_NAMES = ( + "CON", + "PRN", + "AUX", + "NUL", + "COM1", + "COM2", + "COM3", + "COM4", + "COM5", + "COM6", + "COM7", + "COM8", + "COM9", + "LPT1", + "LPT2", + "LPT3", + "LPT4", + "LPT5", + "LPT6", + "LPT7", + "LPT8", + "LPT9", +) + + +def winify_entry_name(name: str) -> str: + prefix, *suffixes = name.split(".", 1) + if prefix in _WIN32_RES_NAMES: + full_suffix = f".{'.'.join(suffixes)}" if suffixes else "" + name = f"{prefix[:-1]}~{ord(prefix[-1]):02x}{full_suffix}" + + else: + for reserved in _WIN32_RES_CHARS: + name = name.replace(reserved, f"~{ord(reserved):02x}") + + if name[-1] in (".", " "): + name = f"{name[:-1]}~{ord(name[-1]):02x}" + + return name + + +def unwinify_entry_name(name: str) -> str: + # Given / is not allowed, no need to check if path already contains it + if "~" not in name: + return name + + else: + *to_convert_parts, last_part = re.split(r"(~[0-9A-Fa-f]{2})", name) + converted_parts = [] + is_escape = False + for part in to_convert_parts: + if is_escape: + converted_chr = chr(int(part[1:], 16)) + if converted_chr in ("/", "\x00"): + raise ValueError("Invalid escaped value") + converted_parts.append(converted_chr) + + else: + converted_parts.append(part) + + is_escape = not is_escape + + converted_parts.append(last_part) + return "".join(converted_parts) diff --git a/tests/core/mountpoint/test_winfsp.py b/tests/core/mountpoint/test_winfsp.py index 17bf03ba651..f307010b2ce 100644 --- a/tests/core/mountpoint/test_winfsp.py +++ b/tests/core/mountpoint/test_winfsp.py @@ -5,6 +5,8 @@ import time import threading +from parsec.core.fs.utils import ntstatus + @pytest.mark.win32 @pytest.mark.mountpoint @@ -219,3 +221,12 @@ async def _bootstrap(user_fs, mountpoint_manager): # Note `os.listdir()` ignores `.` and `..` entries entries_names = os.listdir(mountpoint_service.wpath) assert entries_names == expected_entries_names + + +@pytest.mark.win32 +@pytest.mark.mountpoint +def test_ntstatus_in_fs_errors(): + from winfspy.plumbing.winstuff import NTSTATUS + + for status in ntstatus: + assert getattr(NTSTATUS, status.name) == status