Skip to content

Commit

Permalink
Make winfsp an optional dependency (PR #958)
Browse files Browse the repository at this point in the history
  • Loading branch information
vxgmichel authored Feb 12, 2020
2 parents f5686a0 + 51beba1 commit bccd69f
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 93 deletions.
26 changes: 16 additions & 10 deletions parsec/core/fs/utils.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 ?
Expand Down
4 changes: 1 addition & 3 deletions parsec/core/mountpoint/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
MountpointNotMounted,
MountpointDisabled,
)
from parsec.core.mountpoint.winify import winify_entry_name


def get_mountpoint_runner():
Expand Down Expand Up @@ -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)

Expand Down
81 changes: 1 addition & 80 deletions parsec/core/mountpoint/winfsp_operations.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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()
Expand All @@ -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("\\", "/")))
Expand Down
81 changes: 81 additions & 0 deletions parsec/core/mountpoint/winify.py
Original file line number Diff line number Diff line change
@@ -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)
11 changes: 11 additions & 0 deletions tests/core/mountpoint/test_winfsp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import time
import threading

from parsec.core.fs.utils import ntstatus


@pytest.mark.win32
@pytest.mark.mountpoint
Expand Down Expand Up @@ -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

0 comments on commit bccd69f

Please sign in to comment.