aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-common
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-common')
-rw-r--r--packages/meshbay-common/src/meshbay_common/paths.py24
-rw-r--r--packages/meshbay-common/tests/test_paths.py49
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 = "invoice‮fdp.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]