Skip to content

Commit 41d35ee

Browse files
implement plan
1 parent faf00c3 commit 41d35ee

File tree

8 files changed

+83
-48
lines changed

8 files changed

+83
-48
lines changed

src/borg/archive.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import errno
33
import json
44
import os
5+
import posixpath
56
import stat
67
import sys
78
import time
@@ -1243,8 +1244,8 @@ def __init__(
12431244
@contextmanager
12441245
def create_helper(self, path, st, status=None, hardlinkable=True, strip_prefix=None):
12451246
if strip_prefix is not None:
1246-
assert not path.endswith(os.sep)
1247-
if strip_prefix.startswith(path + os.sep):
1247+
assert not path.endswith("/")
1248+
if strip_prefix.startswith(path + "/"):
12481249
# still on a directory level that shall be stripped - do not create an item for this!
12491250
yield None, "x", False, None
12501251
return
@@ -1547,7 +1548,7 @@ def s_to_ns(s):
15471548

15481549
# if the tar has names starting with "./", normalize them like borg create also does.
15491550
# ./dir/file must become dir/file in the borg archive.
1550-
normalized_path = os.path.normpath(tarinfo.name)
1551+
normalized_path = posixpath.normpath(tarinfo.name)
15511552
item = Item(
15521553
path=make_path_safe(normalized_path),
15531554
mode=tarinfo.mode | type,
@@ -1608,7 +1609,7 @@ def process_symlink(self, *, tarinfo, status, type):
16081609
def process_hardlink(self, *, tarinfo, status, type):
16091610
with self.create_helper(tarinfo, status, type) as (item, status):
16101611
# create a not hardlinked borg item, reusing the chunks, see HardLinkManager.__doc__
1611-
normalized_path = os.path.normpath(tarinfo.linkname)
1612+
normalized_path = posixpath.normpath(tarinfo.linkname)
16121613
safe_path = make_path_safe(normalized_path)
16131614
chunks = self.hlm.retrieve(safe_path)
16141615
if chunks is not None:

src/borg/archiver/create_cmd.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import argparse
44
import logging
55
import os
6+
import posixpath
67
import stat
78
import subprocess
89
import time
@@ -16,11 +17,11 @@
1617
from ..cache import Cache
1718
from ..constants import * # NOQA
1819
from ..compress import CompressionSpec
19-
from ..helpers import comment_validator, ChunkerParams, PathSpec
20+
from ..helpers import comment_validator, ChunkerParams, FilesystemPathSpec
2021
from ..helpers import archivename_validator, FilesCacheMode
2122
from ..helpers import eval_escapes
2223
from ..helpers import timestamp, archive_ts_now
23-
from ..helpers import get_cache_dir, os_stat, get_strip_prefix
24+
from ..helpers import get_cache_dir, os_stat, get_strip_prefix, slashify
2425
from ..helpers import dir_is_tagged
2526
from ..helpers import log_multi
2627
from ..helpers import basic_json_data, json_print
@@ -106,8 +107,9 @@ def create_inner(archive, cache, fso):
106107
pipe_bin = sys.stdin.buffer
107108
pipe = TextIOWrapper(pipe_bin, errors="surrogateescape")
108109
for path in iter_separated(pipe, paths_sep):
110+
path = slashify(path)
109111
strip_prefix = get_strip_prefix(path)
110-
path = os.path.normpath(path)
112+
path = posixpath.normpath(path)
111113
try:
112114
with backup_io("stat"):
113115
st = os_stat(path=path, parent_fd=None, name=None, follow_symlinks=False)
@@ -160,7 +162,7 @@ def create_inner(archive, cache, fso):
160162
continue
161163

162164
strip_prefix = get_strip_prefix(path)
163-
path = os.path.normpath(path)
165+
path = posixpath.normpath(path)
164166
try:
165167
with backup_io("stat"):
166168
st = os_stat(path=path, parent_fd=None, name=None, follow_symlinks=False)
@@ -489,7 +491,7 @@ def _rec_walk(
489491
path=path, fd=child_fd, st=st, strip_prefix=strip_prefix
490492
)
491493
for tag_name in tag_names:
492-
tag_path = os.path.join(path, tag_name)
494+
tag_path = posixpath.join(path, tag_name)
493495
self._rec_walk(
494496
path=tag_path,
495497
parent_fd=child_fd,
@@ -523,7 +525,7 @@ def _rec_walk(
523525
with backup_io("scandir"):
524526
entries = helpers.scandir_inorder(path=path, fd=child_fd)
525527
for dirent in entries:
526-
normpath = os.path.normpath(os.path.join(path, dirent.name))
528+
normpath = posixpath.normpath(posixpath.join(path, dirent.name))
527529
self._rec_walk(
528530
path=normpath,
529531
parent_fd=child_fd,
@@ -962,5 +964,5 @@ def build_parser_create(self, subparsers, common_parser, mid_common_parser):
962964

963965
subparser.add_argument("name", metavar="NAME", type=archivename_validator, help="specify the archive name")
964966
subparser.add_argument(
965-
"paths", metavar="PATH", nargs="*", type=PathSpec, action="extend", help="paths to archive"
967+
"paths", metavar="PATH", nargs="*", type=FilesystemPathSpec, action="extend", help="paths to archive"
966968
)

src/borg/archiver/extract_cmd.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import sys
22
import argparse
33
import logging
4-
import os
54
import stat
65

76
from ._common import with_repository, with_archive
@@ -60,7 +59,7 @@ def do_extract(self, args, repository, manifest, archive):
6059
for item in archive.iter_items():
6160
orig_path = item.path
6261
if strip_components:
63-
stripped_path = os.sep.join(orig_path.split(os.sep)[strip_components:])
62+
stripped_path = "/".join(orig_path.split("/")[strip_components:])
6463
if not stripped_path:
6564
continue
6665
item.path = stripped_path

src/borg/helpers/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,23 @@
2020
from .fs import ensure_dir, join_base_dir, get_socket_filename
2121
from .fs import get_security_dir, get_keys_dir, get_base_dir, get_cache_dir, get_config_dir, get_runtime_dir
2222
from .fs import dir_is_tagged, dir_is_cachedir, remove_dotdot_prefixes, make_path_safe, scandir_inorder
23-
from .fs import secure_erase, safe_unlink, dash_open, os_open, os_stat, get_strip_prefix, umount
23+
from .fs import secure_erase, safe_unlink, dash_open, os_open, os_stat, get_strip_prefix, umount, slashify
2424
from .fs import O_, flags_dir, flags_special_follow, flags_special, flags_base, flags_normal, flags_noatime
2525
from .fs import HardLinkManager
2626
from .misc import sysinfo, log_multi, consume
2727
from .misc import ChunkIteratorFileWrapper, open_item, chunkit, iter_separated, ErrorIgnoringTextIOWrapper
2828
from .parseformat import bin_to_hex, hex_to_bin, safe_encode, safe_decode
2929
from .parseformat import text_to_json, binary_to_json, remove_surrogates, join_cmd
3030
from .parseformat import eval_escapes, decode_dict, positive_int_validator, interval
31-
from .parseformat import PathSpec, SortBySpec, ChunkerParams, FilesCacheMode, partial_format, DatetimeWrapper
31+
from .parseformat import (
32+
PathSpec,
33+
FilesystemPathSpec,
34+
SortBySpec,
35+
ChunkerParams,
36+
FilesCacheMode,
37+
partial_format,
38+
DatetimeWrapper,
39+
)
3240
from .parseformat import format_file_size, parse_file_size, FileSize
3341
from .parseformat import sizeof_fmt, sizeof_fmt_iec, sizeof_fmt_decimal, Location, text_validator
3442
from .parseformat import format_line, replace_placeholders, PlaceholderError, relative_time_marker_validator

src/borg/helpers/fs.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,13 +249,38 @@ def make_path_safe(path):
249249
For reasons of security, a ValueError is raised should
250250
`path` contain any '..' elements.
251251
"""
252+
if "\\.." in path or "..\\" in path:
253+
raise ValueError(f"unexpected '..' element in path {path!r}")
254+
255+
path = percentify(path)
256+
252257
path = path.lstrip("/")
253258
if path.startswith("../") or "/../" in path or path.endswith("/..") or path == "..":
254259
raise ValueError(f"unexpected '..' element in path {path!r}")
255260
path = posixpath.normpath(path)
256261
return path
257262

258263

264+
def slashify(path):
265+
"""
266+
Replace backslashes with forward slashes if running on Windows.
267+
268+
Use case: we always want to use forward slashes, even on Windows.
269+
"""
270+
return path.replace("\\", "/") if is_win32 else path
271+
272+
273+
def percentify(path):
274+
"""
275+
Replace backslashes with percent signs if running on Windows.
276+
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.
280+
"""
281+
return path.replace("\\", "%") if is_win32 else path
282+
283+
259284
def get_strip_prefix(path):
260285
# similar to how rsync does it, we allow users to give paths like:
261286
# /this/gets/stripped/./this/is/kept
@@ -265,7 +290,7 @@ def get_strip_prefix(path):
265290
pos = path.find("/./") # detect slashdot hack
266291
if pos > 0:
267292
# found a prefix to strip! make sure it ends with one "/"!
268-
return os.path.normpath(path[:pos]) + os.sep
293+
return posixpath.normpath(path[:pos]) + "/"
269294
else:
270295
# no or empty prefix, nothing to strip!
271296
return None
@@ -276,15 +301,14 @@ def get_strip_prefix(path):
276301

277302
def remove_dotdot_prefixes(path):
278303
"""
279-
Remove '../'s at the beginning of `path`. Additionally,
280-
the path is made relative.
304+
Remove '../'s at the beginning of `path`. Additionally, the path is made relative.
281305
282-
`path` is expected to be normalized already (e.g. via `os.path.normpath()`).
306+
`path` is expected to be normalized already (e.g. via `posixpath.normpath()`).
283307
"""
308+
assert "\\" not in path
284309
if is_win32:
285310
if len(path) > 1 and path[1] == ":":
286311
path = path.replace(":", "", 1)
287-
path = path.replace("\\", "/")
288312

289313
path = path.lstrip("/")
290314
path = _dotdot_re.sub("", path)

src/borg/helpers/parseformat.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@
2323
logger = create_logger()
2424

2525
from .errors import Error
26-
from .fs import get_keys_dir, make_path_safe
26+
from .fs import get_keys_dir, make_path_safe, slashify
2727
from .msgpack import Timestamp
2828
from .time import OutputTimestamp, format_time, safe_timestamp
2929
from .. import __version__ as borg_version
3030
from .. import __version_tuple__ as borg_version_tuple
3131
from ..constants import * # NOQA
32+
from ..platformflags import is_win32
3233

3334
if TYPE_CHECKING:
3435
from ..item import ItemDiff
@@ -335,6 +336,12 @@ def PathSpec(text):
335336
return text
336337

337338

339+
def FilesystemPathSpec(text):
340+
if not text:
341+
raise argparse.ArgumentTypeError("Empty strings are not accepted as paths.")
342+
return slashify(text)
343+
344+
338345
def SortBySpec(text):
339346
from ..manifest import AI_HUMAN_SORT_KEYS
340347

@@ -558,7 +565,8 @@ def _parse(self, text):
558565
m = self.local_re.match(text)
559566
if m:
560567
self.proto = "file"
561-
self.path = os.path.abspath(os.path.normpath(m.group("path")))
568+
path = m.group("path")
569+
self.path = slashify(os.path.abspath(path)) if is_win32 else os.path.abspath(path)
562570
return True
563571
return False
564572

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
10+
from .helpers.fs import assert_sanitized_path, to_sanitized_path, percentify, 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')
268+
target = PropDictProperty(str, 'surrogate-escaped str', encode=slashify, decode=percentify)
269269
user = PropDictProperty(str, 'surrogate-escaped str')
270270
group = PropDictProperty(str, 'surrogate-escaped str')
271271

src/borg/patterns.py

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import argparse
22
import fnmatch
3-
import os.path
3+
import posixpath
44
import re
55
import sys
66
import unicodedata
@@ -142,7 +142,7 @@ def match(self, path):
142142
in self.fallback is returned (defaults to None).
143143
144144
"""
145-
path = normalize_path(path).lstrip(os.path.sep)
145+
path = normalize_path(path).lstrip("/")
146146
# do a fast lookup for full path matches (note: we do not count such matches):
147147
non_existent = object()
148148
value = self._path_full_patterns.get(path, non_existent)
@@ -215,7 +215,7 @@ class PathFullPattern(PatternBase):
215215
PREFIX = "pf"
216216

217217
def _prepare(self, pattern):
218-
self.pattern = os.path.normpath(pattern).lstrip(os.path.sep) # sep at beginning is removed
218+
self.pattern = posixpath.normpath(pattern).lstrip("/") # / at beginning is removed
219219

220220
def _match(self, path):
221221
return path == self.pattern
@@ -236,12 +236,10 @@ class PathPrefixPattern(PatternBase):
236236
PREFIX = "pp"
237237

238238
def _prepare(self, pattern):
239-
sep = os.path.sep
240-
241-
self.pattern = (os.path.normpath(pattern).rstrip(sep) + sep).lstrip(sep) # sep at beginning is removed
239+
self.pattern = (posixpath.normpath(pattern).rstrip("/") + "/").lstrip("/") # / at beginning is removed
242240

243241
def _match(self, path):
244-
return (path + os.path.sep).startswith(self.pattern)
242+
return (path + "/").startswith(self.pattern)
245243

246244

247245
class FnmatchPattern(PatternBase):
@@ -252,19 +250,19 @@ class FnmatchPattern(PatternBase):
252250
PREFIX = "fm"
253251

254252
def _prepare(self, pattern):
255-
if pattern.endswith(os.path.sep):
256-
pattern = os.path.normpath(pattern).rstrip(os.path.sep) + os.path.sep + "*" + os.path.sep
253+
if pattern.endswith("/"):
254+
pattern = posixpath.normpath(pattern).rstrip("/") + "/*/"
257255
else:
258-
pattern = os.path.normpath(pattern) + os.path.sep + "*"
256+
pattern = posixpath.normpath(pattern) + "/*"
259257

260-
self.pattern = pattern.lstrip(os.path.sep) # sep at beginning is removed
258+
self.pattern = pattern.lstrip("/") # / at beginning is removed
261259

262260
# fnmatch and re.match both cache compiled regular expressions.
263261
# Nevertheless, this is about 10 times faster.
264262
self.regex = re.compile(fnmatch.translate(self.pattern))
265263

266264
def _match(self, path):
267-
return self.regex.match(path + os.path.sep) is not None
265+
return self.regex.match(path + "/") is not None
268266

269267

270268
class ShellPattern(PatternBase):
@@ -275,18 +273,16 @@ class ShellPattern(PatternBase):
275273
PREFIX = "sh"
276274

277275
def _prepare(self, pattern):
278-
sep = os.path.sep
279-
280-
if pattern.endswith(sep):
281-
pattern = os.path.normpath(pattern).rstrip(sep) + sep + "**" + sep + "*" + sep
276+
if pattern.endswith("/"):
277+
pattern = posixpath.normpath(pattern).rstrip("/") + "/**/*/"
282278
else:
283-
pattern = os.path.normpath(pattern) + sep + "**" + sep + "*"
279+
pattern = posixpath.normpath(pattern) + "/**/*"
284280

285-
self.pattern = pattern.lstrip(sep) # sep at beginning is removed
281+
self.pattern = pattern.lstrip("/") # / at beginning is removed
286282
self.regex = re.compile(shellpattern.translate(self.pattern))
287283

288284
def _match(self, path):
289-
return self.regex.match(path + os.path.sep) is not None
285+
return self.regex.match(path + "/") is not None
290286

291287

292288
class RegexPattern(PatternBase):
@@ -295,14 +291,11 @@ class RegexPattern(PatternBase):
295291
PREFIX = "re"
296292

297293
def _prepare(self, pattern):
298-
self.pattern = pattern # sep at beginning is NOT removed
294+
self.pattern = pattern # / at beginning is NOT removed
299295
self.regex = re.compile(pattern)
300296

301297
def _match(self, path):
302-
# Normalize path separators
303-
if os.path.sep != "/":
304-
path = path.replace(os.path.sep, "/")
305-
298+
assert "\\" not in path
306299
return self.regex.search(path) is not None
307300

308301

0 commit comments

Comments
 (0)