fix: resolve per-torrent qb save paths

This commit is contained in:
2026-08-14 11:56:06 +00:00
parent ae7175719b
commit a1bf3e4315
17 changed files with 453 additions and 55 deletions
+21 -1
View File
@@ -23,6 +23,9 @@ class NormalizedResource:
files: tuple[resource_pb2.TorrentFile, ...]
metainfo: Metainfo
metainfo_bytes: bytes = b""
# qBittorrent's API-visible save path is intentionally local-only. It
# must never become part of inventory or placement protocol messages.
save_path: PurePosixPath | None = None
def build_content_tree(
@@ -137,6 +140,7 @@ def normalize_resource(
character not in "0123456789abcdef" for character in qb_torrent_id
):
raise ResourceError("torrent hash is invalid")
save_path = _save_path(torrent.get("save_path"))
summary = resource_pb2.ResourceSummary(
qb_torrent_id=qb_torrent_id,
display_name=_string(torrent.get("name"), "torrent name"),
@@ -172,7 +176,9 @@ def normalize_resource(
revision_data, sort_keys=True, separators=(",", ":"),
).encode("utf-8")).hexdigest()
summary.observed_at.FromDatetime(observed_at)
return NormalizedResource(summary, tuple(files), metainfo, metainfo_bytes)
return NormalizedResource(
summary, tuple(files), metainfo, metainfo_bytes, save_path
)
def _set_selection(target: Any, indices: list[int]) -> None:
@@ -217,6 +223,20 @@ def _path(value: Any) -> str:
return candidate.as_posix()
def _save_path(value: Any) -> PurePosixPath:
"""Validate qBittorrent's API-visible per-torrent content root."""
path = _string(value, "torrent save path")
candidate = PurePosixPath(path)
if (
not candidate.is_absolute()
or ".." in candidate.parts
or "." in candidate.parts
):
raise ResourceError("qBittorrent torrent save path is unsafe")
return candidate
def _integer(value: Any, name: str) -> int:
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
raise ResourceError(f"{name} is invalid")