1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
"""
Names crossing between a group and this machine, in both directions.
**Uploading**, the node decides: `SAFE_UPLOAD_NAME` and the Explorer-active types
in `meshbay_node/roots.py`. This is a copy of that rule, checked before anything
is sent so a folder holding one refused name is refused whole instead of
arriving with holes in it — as the Files tab does with its own copy.
`test_names.py` holds this copy to the node's answers.
**Downloading**, a name from the index is someone else's text. It is made
writable on this platform (`meshbay_common.paths.sanitize_for_download`, the
same rule the browser applies), and the resolved path must stay under the
destination: checked after resolution, never by comparing strings.
"""
import re
from pathlib import Path
from meshbay_common.paths import fold, sanitize_for_download
UPLOAD_NAME = re.compile(
r"^[^\W_]"
r"[\w .\-()\[\]'’,&+#@]{0,127}"
r"(?<![ .])$",
re.UNICODE)
SHELL_ACTIVE_NAMES = frozenset({"desktop.ini"})
SHELL_ACTIVE_SUFFIXES = (".lnk", ".url", ".scf", ".library-ms", ".searchconnector-ms")
def upload_name_problem(name: str, *, is_dir: bool = False) -> str | None:
if not UPLOAD_NAME.match(name):
return ("starts with something other than a letter or digit, ends with a space "
"or a dot, holds a character the node does not accept, or is longer "
"than 128 characters")
lower = name.lower()
if not is_dir and (lower in SHELL_ACTIVE_NAMES or lower.endswith(SHELL_ACTIVE_SUFFIXES)):
return "a file type Windows Explorer acts on by itself; no node accepts it"
return None
def same_name(a: str, b: str) -> bool:
"""As the node and the Files tab compare names in a folder: without case."""
return fold(a) == fold(b)
class UnsafeName(Exception):
pass
def local_path(dest: Path, parts: list[str]) -> tuple[Path, bool]:
"""
Where a remote file or folder lands under `dest`, and whether a name had to
be changed to be written here.
"""
renamed = False
out = dest
for part in parts:
safe = sanitize_for_download(part)
if safe in (".", ".."):
safe = "unnamed"
renamed |= safe != part
out = out / safe
base = dest.resolve()
try:
out.resolve().relative_to(base)
except ValueError:
raise UnsafeName(f"{'/'.join(parts)!r} would land outside {dest}") from None
return out, renamed
|