Skip to content

Commit eeebc41

Browse files
author
Jussi Kukkonen
committed
ngclient: Don't use target path as local path
Doing so is not always safe and has various other issues (like target paths "a/../b" and "b" ending up as the same local path). Instead URL-encode the target path to make it a plain filename. This removes any opportunity for path trickery and removes the need to create the required sub directories (which we were not doing currently, leading to failed downloads). URL-encoding encodes much more than we really need but doing so should not hurt: the important thing is that it encodes all path separators. Return the actual filepath as return value. I would like to modify the arguments so caller could decide the filename if they want to. But I won't do it now because updated_targets() (the caching mechanism) relies on filenames being chosen by TUF. The plan is to make it possible for caller to choose the filename though. This is clearly a "filesystem API break" for anyone depending on the actual target file names, and does not make sense if we do not plan to go forward with other updated_targets()/download_target() changes listed in theupdateframework#1580. This is part of bigger plan in theupdateframework#1580 Fixes theupdateframework#1571 Signed-off-by: Jussi Kukkonen <[email protected]>
1 parent 82b0967 commit eeebc41

File tree

2 files changed

+46
-41
lines changed

2 files changed

+46
-41
lines changed

tests/test_updater_with_simulator.py

+26-21
Original file line numberDiff line numberDiff line change
@@ -80,32 +80,37 @@ def test_refresh(self):
8080
self._run_refresh()
8181

8282
def test_targets(self):
83-
# target does not exist yet
84-
updater = self._run_refresh()
85-
self.assertIsNone(updater.get_one_valid_targetinfo("file"))
83+
targets = {
84+
"targetpath": b"content",
85+
"åäö": b"more content"
86+
}
8687

88+
# Add targets to repository
8789
self.sim.targets.version += 1
88-
self.sim.add_target("targets", b"content", "file")
90+
for targetpath, content in targets.items():
91+
self.sim.add_target("targets", content, targetpath)
8992
self.sim.update_snapshot()
9093

91-
# target now exists, is not in cache yet
9294
updater = self._run_refresh()
93-
file_info = updater.get_one_valid_targetinfo("file")
94-
self.assertIsNotNone(file_info)
95-
self.assertEqual(
96-
updater.updated_targets([file_info], self.targets_dir), [file_info]
97-
)
98-
99-
# download target, assert it is in cache and content is correct
100-
updater.download_target(file_info, self.targets_dir)
101-
self.assertEqual(
102-
updater.updated_targets([file_info], self.targets_dir), []
103-
)
104-
with open(os.path.join(self.targets_dir, "file"), "rb") as f:
105-
self.assertEqual(f.read(), b"content")
106-
107-
# TODO: run the same download tests for
108-
# self.sim.add_target("targets", b"more content", "dir/file2")
95+
for targetpath, content in targets.items():
96+
# target now exists, is not in cache yet
97+
file_info = updater.get_one_valid_targetinfo(targetpath)
98+
self.assertIsNotNone(file_info)
99+
self.assertEqual(
100+
updater.updated_targets([file_info], self.targets_dir),
101+
[file_info]
102+
)
103+
104+
# download target, assert it is in cache and content is correct
105+
local_path = updater.download_target(file_info, self.targets_dir)
106+
self.assertEqual(
107+
updater.updated_targets([file_info], self.targets_dir), []
108+
)
109+
self.assertTrue(local_path.startswith(self.targets_dir))
110+
with open(local_path, "rb") as f:
111+
self.assertEqual(f.read(), content)
112+
113+
# TODO: run the same download tests for target paths like "dir/file2")
109114
# This currently fails because issue #1576
110115

111116
def test_keys_and_signatures(self):

tuf/ngclient/updater.py

+20-20
Original file line numberDiff line numberDiff line change
@@ -188,31 +188,27 @@ def updated_targets(
188188
returned in a list. The list items can be downloaded with
189189
'download_target()'.
190190
"""
191-
# Keep track of the target objects and filepaths of updated targets.
192-
# Return 'updated_targets' and use 'updated_targetpaths' to avoid
193-
# duplicates.
194-
updated_targets = []
195-
updated_targetpaths = []
191+
# Keep track of TargetFiles and local paths. Return 'updated_targets'
192+
# and use 'local_paths' to avoid duplicates.
193+
updated_targets: List[TargetFile] = []
194+
local_paths: List[str] = []
196195

197196
for target in targets:
198-
# Prepend 'destination_directory' to the target's relative filepath
199-
# (as stored in metadata.) Verify the hash of 'target_filepath'
200-
# against each hash listed for its fileinfo. Note: join() discards
201-
# 'destination_directory' if 'filepath' contains a leading path
202-
# separator (i.e., is treated as an absolute path).
203-
target_filepath = os.path.join(destination_directory, target.path)
204-
205-
if target_filepath in updated_targetpaths:
197+
# URL encode to get local filename like download_target() does
198+
filename = parse.quote(target.path, "")
199+
local_path = os.path.join(destination_directory, filename)
200+
201+
if local_path in local_paths:
206202
continue
207203

208204
try:
209-
with open(target_filepath, "rb") as target_file:
205+
with open(local_path, "rb") as target_file:
210206
target.verify_length_and_hashes(target_file)
211207
# If the file does not exist locally or length and hashes
212208
# do not match, append to updated targets.
213209
except (OSError, exceptions.LengthOrHashMismatchError):
214210
updated_targets.append(target)
215-
updated_targetpaths.append(target_filepath)
211+
local_paths.append(local_path)
216212

217213
return updated_targets
218214

@@ -221,7 +217,7 @@ def download_target(
221217
targetinfo: TargetFile,
222218
destination_directory: str,
223219
target_base_url: Optional[str] = None,
224-
) -> None:
220+
) -> str:
225221
"""Downloads the target file specified by 'targetinfo'.
226222
227223
Args:
@@ -236,6 +232,9 @@ def download_target(
236232
Raises:
237233
TODO: download-related errors
238234
TODO: file write errors
235+
236+
Returns:
237+
Path to downloaded file
239238
"""
240239

241240
if target_base_url is None:
@@ -266,12 +265,13 @@ def download_target(
266265
f"{target_filepath} length or hashes do not match"
267266
) from e
268267

269-
# Store the target file name without the HASH prefix.
270-
local_filepath = os.path.join(
271-
destination_directory, targetinfo.path
272-
)
268+
# Use a URL encoded targetpath as the local filename
269+
filename = parse.quote(targetinfo.path, "")
270+
local_filepath = os.path.join(destination_directory, filename)
273271
sslib_util.persist_temp_file(target_file, local_filepath)
274272

273+
return local_filepath
274+
275275
def _download_metadata(
276276
self, rolename: str, length: int, version: Optional[int] = None
277277
) -> bytes:

0 commit comments

Comments
 (0)