Skip to content

Commit 8f940d4

Browse files
map_chars: deal invalid chars in paths on windows
1 parent d6d5ce5 commit 8f940d4

File tree

3 files changed

+48
-9
lines changed

3 files changed

+48
-9
lines changed

src/borg/helpers/fs.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ def make_path_safe(path):
252252
if "\\.." in path or "..\\" in path:
253253
raise ValueError(f"unexpected '..' element in path {path!r}")
254254

255-
path = percentify(path)
255+
path = map_chars(path)
256256

257257
path = path.lstrip("/")
258258
if path.startswith("../") or "/../" in path or path.endswith("/..") or path == "..":
@@ -270,15 +270,32 @@ def slashify(path):
270270
return path.replace("\\", "/") if is_win32 else path
271271

272272

273-
def percentify(path):
273+
# Bijective mapping to Unicode Private Use Area (like cifs mapchars)
274+
WINDOWS_MAP_CHARS = str.maketrans(
275+
{
276+
"<": "\uF03C",
277+
">": "\uF03E",
278+
":": "\uF03A",
279+
'"': "\uF022",
280+
"\\": "\uF05C",
281+
"|": "\uF07C",
282+
"?": "\uF03F",
283+
"*": "\uF02A",
284+
}
285+
)
286+
287+
288+
def map_chars(path):
274289
"""
275-
Replace backslashes with percent signs if running on Windows.
290+
Map reserved characters if running on Windows.
276291
277-
Use case: if an archived path contains backslashes (which is not a path separator on POSIX
278-
and could appear as a normal character in POSIX paths), we need to replace them with percent
279-
signs to make the path usable on Windows.
292+
Use case: if an archived path contains reserved characters (that are not reserved on POSIX)
293+
we need to replace them with replacements to make the path usable on Windows.
280294
"""
281-
return path.replace("\\", "%") if is_win32 else path
295+
if not is_win32:
296+
return path
297+
298+
return path.translate(WINDOWS_MAP_CHARS)
282299

283300

284301
def get_strip_prefix(path):

src/borg/item.pyx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ from cpython.bytes cimport PyBytes_AsStringAndSize
77
from .constants import ITEM_KEYS, ARCHIVE_KEYS
88
from .helpers import StableDict
99
from .helpers import format_file_size
10-
from .helpers.fs import assert_sanitized_path, to_sanitized_path, percentify, slashify
10+
from .helpers.fs import assert_sanitized_path, to_sanitized_path, map_chars, slashify
1111
from .helpers.msgpack import timestamp_to_int, int_to_timestamp, Timestamp
1212
from .helpers.time import OutputTimestamp, safe_timestamp
1313

@@ -265,7 +265,7 @@ cdef class Item(PropDict):
265265

266266
path = PropDictProperty(str, 'surrogate-escaped str', encode=assert_sanitized_path, decode=to_sanitized_path)
267267
source = PropDictProperty(str, 'surrogate-escaped str') # legacy borg 1.x. borg 2: see .target
268-
target = PropDictProperty(str, 'surrogate-escaped str', encode=slashify, decode=percentify)
268+
target = PropDictProperty(str, 'surrogate-escaped str', encode=slashify, decode=map_chars)
269269
user = PropDictProperty(str, 'surrogate-escaped str')
270270
group = PropDictProperty(str, 'surrogate-escaped str')
271271

src/borg/testsuite/helpers/fs_test.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
safe_unlink,
2121
remove_dotdot_prefixes,
2222
make_path_safe,
23+
map_chars,
2324
)
2425
from ...platform import is_win32, is_darwin, is_haiku
2526
from .. import are_hardlinks_supported
@@ -441,3 +442,24 @@ def open_dir(path):
441442
assert dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[".NOBACKUP"]) == [".NOBACKUP"]
442443
with open_dir(str(normal_dir)) as fd:
443444
assert dir_is_tagged(dir_fd=fd, exclude_caches=True, exclude_if_present=[".NOBACKUP"]) == []
445+
446+
447+
def test_map_chars(monkeypatch):
448+
# Test behavior on non-Windows (should return path unchanged)
449+
monkeypatch.setattr("borg.helpers.fs.is_win32", False)
450+
assert map_chars("foo/bar") == "foo/bar"
451+
assert map_chars("foo\\bar") == "foo\\bar"
452+
assert map_chars("foo:bar") == "foo:bar"
453+
454+
# Test behavior on Windows
455+
monkeypatch.setattr("borg.helpers.fs.is_win32", True)
456+
457+
# Reserved characters replacement
458+
assert map_chars("foo:bar") == "foo\uf03abar"
459+
assert map_chars("foo<bar") == "foo\uf03cbar"
460+
assert map_chars("foo>bar") == "foo\uf03ebar"
461+
assert map_chars('foo"bar') == "foo\uf022bar"
462+
assert map_chars("foo\\bar") == "foo\uf05cbar"
463+
assert map_chars("foo|bar") == "foo\uf07cbar"
464+
assert map_chars("foo?bar") == "foo\uf03fbar"
465+
assert map_chars("foo*bar") == "foo\uf02abar"

0 commit comments

Comments
 (0)