""" 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"(? 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