diff options
Diffstat (limited to 'packages/meshbay-common')
| -rw-r--r-- | packages/meshbay-common/src/meshbay_common/paths.py | 24 | ||||
| -rw-r--r-- | packages/meshbay-common/tests/test_paths.py | 49 |
2 files changed, 64 insertions, 9 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/paths.py b/packages/meshbay-common/src/meshbay_common/paths.py index 8be4680..059bf09 100644 --- a/packages/meshbay-common/src/meshbay_common/paths.py +++ b/packages/meshbay-common/src/meshbay_common/paths.py @@ -19,7 +19,7 @@ from __future__ import annotations import os import re import unicodedata -from pathlib import Path, PurePath +from pathlib import Path # Windows refuses these as a basename, with or without an extension: `AUX.txt` # is as impossible as `AUX`. A group indexed on Linux can hold them, and a @@ -39,6 +39,9 @@ _RESERVED_CHARS = set('<>:"/\\|?*') | {chr(c) for c in range(32)} | BIDI_CONTROL # Windows without long-path support. A deep media library reaches this. MAX_PATH_WINDOWS = 260 +# Creating a directory is refused twelve characters earlier (room for an 8.3 +# name), and listing one adds `\*`: the extended form starts there. +_EXTENDED_FROM = MAX_PATH_WINDOWS - 12 def nfc(text: str) -> str: @@ -102,19 +105,22 @@ def long_path(path: Path | str) -> str: """ A path string safe to hand to the OS. - On Windows, prefix with `\\\\?\\` so `MAX_PATH` does not truncate a deep - library. The prefix requires an absolute, already-normalized path, and it is - a no-op everywhere else. + On Windows, a path that reaches `MAX_PATH` is refused by every call unless + the system's `LongPathsEnabled` is set, which it is not by default and which + a program cannot set for itself. The extended form (`\\\\?\\`) is accepted + whatever the setting, so a long path gets it. The form skips the system's + normalisation, so the path is normalised here first. A no-op everywhere + else, and for a path short enough to need nothing. """ text = str(path) if os.name != "nt" or text.startswith("\\\\?\\"): return text - resolved = str(PurePath(text)) - if len(resolved) < MAX_PATH_WINDOWS and not text.startswith("\\\\"): + full = os.path.normpath(text) if os.path.isabs(text) else os.path.abspath(text) + if len(full) < _EXTENDED_FROM: return text - if text.startswith("\\\\"): # UNC: \\server\share → \\?\UNC\server\share - return "\\\\?\\UNC" + text[1:] - return "\\\\?\\" + text + if full.startswith("\\\\"): # UNC: \\server\share → \\?\UNC\server\share + return "\\\\?\\UNC" + full[1:] + return "\\\\?\\" + full def find_fold_collisions(names: list[str]) -> dict[str, list[str]]: diff --git a/packages/meshbay-common/tests/test_paths.py b/packages/meshbay-common/tests/test_paths.py index 012a32e..cbaf1f5 100644 --- a/packages/meshbay-common/tests/test_paths.py +++ b/packages/meshbay-common/tests/test_paths.py @@ -6,12 +6,15 @@ these are not compatibility niceties — they decide whether two index entries a one file, and whether a member can save what they downloaded. """ +import os + import pytest from meshbay_common.paths import ( find_fold_collisions, fold, fold_path, is_portable_name, + long_path, nfc, portable_name_problem, sanitize_for_download, @@ -126,3 +129,49 @@ def test_a_bidi_override_cannot_hide_an_extension(): disguised = "invoicefdp.exe" # displays as "invoiceexe.pdf" assert sanitize_for_download(disguised) == "invoice_fdp.exe" assert portable_name_problem(disguised) + + +# ── Long paths on Windows ──────────────────────────────────────────────────── + +on_windows = pytest.mark.skipif(os.name != "nt", reason="the extended form is Windows'") +BS = chr(92) +EXT = BS * 2 + "?" + BS +DEEP = BS.join(["C:", *["a folder name of some length"] * 9, "track.flac"]) + + +@on_windows +def test_a_short_path_is_left_as_it_is(): + assert long_path(f"C:{BS}Music{BS}track.flac") == f"C:{BS}Music{BS}track.flac" + + +@on_windows +def test_a_long_path_gets_the_extended_form(): + assert long_path(DEEP) == EXT + DEEP + + +@on_windows +def test_the_extended_form_is_normalised_first(): + """The system normalises nothing behind the prefix: a forward slash or a + `..` left in would name a file that does not exist.""" + messy = DEEP.replace(BS, "/").replace("/track.flac", "/x/../track.flac") + assert long_path(messy) == EXT + DEEP + + +@on_windows +def test_a_long_share_path_gets_the_unc_form(): + share = BS * 2 + "server" + BS + "share" + DEEP[2:] + assert long_path(share) == EXT + "UNC" + BS + share[2:] + + +@on_windows +def test_a_path_in_the_extended_form_is_left_alone(): + assert long_path(EXT + DEEP) == EXT + DEEP + + +@on_windows +def test_a_directory_gets_it_before_max_path(): + """Creating a directory is refused twelve characters before MAX_PATH.""" + at_248 = "C:" + BS + "d" * 245 + assert len(at_248) == 248 + assert long_path(at_248) == EXT + at_248 + assert long_path(at_248[:-1]) == at_248[:-1] |