diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-01 11:47:39 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-01 11:47:39 +0200 |
| commit | 760ac421b1944cd69a80e3a92127a1a966f15938 (patch) | |
| tree | c894b655d4f21698ebb91c0865ddf3e04985586d /packages/meshbay-common | |
| parent | 752b160c7c5e671e0db8f402a52fac27bb85ab06 (diff) | |
| download | meshbay-760ac421b1944cd69a80e3a92127a1a966f15938.tar.gz | |
fix: downloads are marked and keep their extension; Explorer files are refused
The desktop app writes the Mark-of-the-Web on each file it saves on Windows, as
a browser does. Bidirectional controls are reserved characters in a saved name
(portable-name.js and paths.sanitize_for_download, and again in the main
process), so a name cannot display one extension and carry another. The node
refuses uploads of files Windows Explorer acts on by itself: desktop.ini,
.lnk, .url, .scf, .library-ms, .searchConnector-ms (F-19).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-common')
| -rw-r--r-- | packages/meshbay-common/src/meshbay_common/paths.py | 8 | ||||
| -rw-r--r-- | packages/meshbay-common/tests/test_paths.py | 7 | ||||
| -rw-r--r-- | packages/meshbay-common/tests/test_portable_name_parity.py | 1 |
3 files changed, 14 insertions, 2 deletions
diff --git a/packages/meshbay-common/src/meshbay_common/paths.py b/packages/meshbay-common/src/meshbay_common/paths.py index b673649..8be4680 100644 --- a/packages/meshbay-common/src/meshbay_common/paths.py +++ b/packages/meshbay-common/src/meshbay_common/paths.py @@ -30,8 +30,12 @@ WINDOWS_RESERVED = frozenset({ *(f"LPT{i}" for i in range(1, 10)), }) -# Reserved on Windows; `/` is reserved everywhere. Control characters go too. -_RESERVED_CHARS = set('<>:"/\\|?*') | {chr(c) for c in range(32)} +# Reserved on Windows; `/` is reserved everywhere. Control characters go too, +# and so do the bidirectional controls: "invoice\u202efdp.exe" displays as +# "invoiceexe.pdf", and a saved name must say what the file is. +BIDI_CONTROLS = frozenset("\u061c\u200e\u200f\u202a\u202b\u202c\u202d\u202e" + "\u2066\u2067\u2068\u2069") +_RESERVED_CHARS = set('<>:"/\\|?*') | {chr(c) for c in range(32)} | BIDI_CONTROLS # Windows without long-path support. A deep media library reaches this. MAX_PATH_WINDOWS = 260 diff --git a/packages/meshbay-common/tests/test_paths.py b/packages/meshbay-common/tests/test_paths.py index 223a2a6..012a32e 100644 --- a/packages/meshbay-common/tests/test_paths.py +++ b/packages/meshbay-common/tests/test_paths.py @@ -119,3 +119,10 @@ def test_sanitizing_produces_something_writable(): def test_sanitizing_never_returns_nothing(): assert sanitize_for_download("...") not in ("", None) assert sanitize_for_download("???") not in ("", None) + + +def test_a_bidi_override_cannot_hide_an_extension(): + from meshbay_common.paths import portable_name_problem, sanitize_for_download + disguised = "invoicefdp.exe" # displays as "invoiceexe.pdf" + assert sanitize_for_download(disguised) == "invoice_fdp.exe" + assert portable_name_problem(disguised) diff --git a/packages/meshbay-common/tests/test_portable_name_parity.py b/packages/meshbay-common/tests/test_portable_name_parity.py index 3a491a7..d7df710 100644 --- a/packages/meshbay-common/tests/test_portable_name_parity.py +++ b/packages/meshbay-common/tests/test_portable_name_parity.py @@ -26,6 +26,7 @@ pytestmark = pytest.mark.skipif( ) NAMES = [ + "invoice\u202efdp.exe", "a\u2066b\u2069.txt", "mark\u200f.txt", "plain.txt", "Réunion 12:30.pdf", 'a<b>c:d"e/f\\g|h?i*j.txt', "tab\tnew\nline", "ends with dot.", "ends with space ", "trailing . . ", "CON", "con.txt", "aux.tar.gz", "COM1", "com10.txt", "LPT9.log", "nul.", ".", "..", "", " ", ".bashrc", "...", |