Skip to content

Commit 7731738

Browse files
author
Jussi Kukkonen
authored
Merge pull request #1514 from MVrachev/filename-in-targetfile
Metadata API: include target target name in TargetFile
2 parents 5a3ac9c + 9229a40 commit 7731738

File tree

5 files changed

+36
-34
lines changed

5 files changed

+36
-34
lines changed

tests/test_api.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -495,14 +495,14 @@ def test_metadata_targets(self):
495495
"sha512": "ef5beafa16041bcdd2937140afebd485296cd54f7348ecd5a4d035c09759608de467a7ac0eb58753d0242df873c305e8bffad2454aa48f44480f15efae1cacd0"
496496
}
497497

498-
fileinfo = TargetFile(length=28, hashes=hashes)
498+
fileinfo = TargetFile(length=28, hashes=hashes, path=filename)
499499

500500
# Assert that data is not aleady equal
501501
self.assertNotEqual(
502502
targets.signed.targets[filename].to_dict(), fileinfo.to_dict()
503503
)
504504
# Update an already existing fileinfo
505-
targets.signed.update(filename, fileinfo)
505+
targets.signed.update(fileinfo)
506506
# Verify that data is updated
507507
self.assertEqual(
508508
targets.signed.targets[filename].to_dict(), fileinfo.to_dict()

tests/test_metadata_serialization.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,7 @@ def test_delegation_serialization(self, test_case_data: str):
340340
def test_invalid_targetfile_serialization(self, test_case_data: Dict[str, str]):
341341
case_dict = json.loads(test_case_data)
342342
with self.assertRaises(KeyError):
343-
TargetFile.from_dict(copy.deepcopy(case_dict))
343+
TargetFile.from_dict(copy.deepcopy(case_dict), "file1.txt")
344344

345345

346346
valid_targetfiles: DataSet = {
@@ -354,7 +354,7 @@ def test_invalid_targetfile_serialization(self, test_case_data: Dict[str, str]):
354354
@run_sub_tests_with_dataset(valid_targetfiles)
355355
def test_targetfile_serialization(self, test_case_data: str):
356356
case_dict = json.loads(test_case_data)
357-
target_file = TargetFile.from_dict(copy.copy(case_dict))
357+
target_file = TargetFile.from_dict(copy.copy(case_dict), "file1.txt")
358358
self.assertDictEqual(case_dict, target_file.to_dict())
359359

360360

tests/test_updater_ng.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,8 @@ def test_refresh_on_consistent_targets(self):
162162
targetinfo3 = self.repository_updater.get_one_valid_targetinfo("file3.txt")
163163

164164
# Create consistent targets with file path HASH.FILENAME.EXT
165-
target1_hash = list(targetinfo1["fileinfo"].hashes.values())[0]
166-
target3_hash = list(targetinfo3["fileinfo"].hashes.values())[0]
165+
target1_hash = list(targetinfo1.hashes.values())[0]
166+
target3_hash = list(targetinfo3.hashes.values())[0]
167167
self._create_consistent_target("file1.txt", target1_hash)
168168
self._create_consistent_target("file3.txt", target3_hash)
169169

tuf/api/metadata.py

+11-5
Original file line numberDiff line numberDiff line change
@@ -1140,13 +1140,16 @@ class TargetFile(BaseFile):
11401140
Attributes:
11411141
length: An integer indicating the length of the target file.
11421142
hashes: A dictionary of hash algorithm names to hash values.
1143+
path: A string denoting the path to a target file relative to a base
1144+
URL of targets.
11431145
unrecognized_fields: Dictionary of all unrecognized fields.
11441146
"""
11451147

11461148
def __init__(
11471149
self,
11481150
length: int,
11491151
hashes: Dict[str, str],
1152+
path: str,
11501153
unrecognized_fields: Optional[Mapping[str, Any]] = None,
11511154
) -> None:
11521155

@@ -1155,20 +1158,21 @@ def __init__(
11551158

11561159
self.length = length
11571160
self.hashes = hashes
1161+
self.path = path
11581162
self.unrecognized_fields = unrecognized_fields or {}
11591163

11601164
@property
11611165
def custom(self) -> Any:
11621166
return self.unrecognized_fields.get("custom", None)
11631167

11641168
@classmethod
1165-
def from_dict(cls, target_dict: Dict[str, Any]) -> "TargetFile":
1169+
def from_dict(cls, target_dict: Dict[str, Any], path: str) -> "TargetFile":
11661170
"""Creates TargetFile object from its dict representation."""
11671171
length = target_dict.pop("length")
11681172
hashes = target_dict.pop("hashes")
11691173

11701174
# All fields left in the target_dict are unrecognized.
1171-
return cls(length, hashes, target_dict)
1175+
return cls(length, hashes, path, target_dict)
11721176

11731177
def to_dict(self) -> Dict[str, Any]:
11741178
"""Returns the JSON-serializable dictionary representation of self."""
@@ -1234,7 +1238,9 @@ def from_dict(cls, signed_dict: Dict[str, Any]) -> "Targets":
12341238
delegations = Delegations.from_dict(delegations_dict)
12351239
res_targets = {}
12361240
for target_path, target_info in targets.items():
1237-
res_targets[target_path] = TargetFile.from_dict(target_info)
1241+
res_targets[target_path] = TargetFile.from_dict(
1242+
target_info, target_path
1243+
)
12381244
# All fields left in the targets_dict are unrecognized.
12391245
return cls(*common_args, res_targets, delegations, signed_dict)
12401246

@@ -1250,6 +1256,6 @@ def to_dict(self) -> Dict[str, Any]:
12501256
return targets_dict
12511257

12521258
# Modification.
1253-
def update(self, filename: str, fileinfo: TargetFile) -> None:
1259+
def update(self, fileinfo: TargetFile) -> None:
12541260
"""Assigns passed target file info to meta dict."""
1255-
self.targets[filename] = fileinfo
1261+
self.targets[fileinfo.path] = fileinfo

tuf/ngclient/updater.py

+19-23
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@
6262

6363
import logging
6464
import os
65-
from typing import Any, Dict, List, Optional, Set, Tuple
65+
from typing import List, Optional, Set, Tuple
6666
from urllib import parse
6767

6868
from securesystemslib import util as sslib_util
6969

7070
from tuf import exceptions
71-
from tuf.api.metadata import Targets
71+
from tuf.api.metadata import TargetFile, Targets
7272
from tuf.ngclient._internal import requests_fetcher, trusted_metadata_set
7373
from tuf.ngclient.config import UpdaterConfig
7474
from tuf.ngclient.fetcher import FetcherInterface
@@ -146,8 +146,8 @@ def refresh(self) -> None:
146146

147147
def get_one_valid_targetinfo(
148148
self, target_path: str
149-
) -> Optional[Dict[str, Any]]:
150-
"""Returns target information for 'target_path'.
149+
) -> Optional[TargetFile]:
150+
"""Returns TargetFile instance with information for 'target_path'.
151151
152152
The return value can be used as an argument to
153153
:func:`download_target()` and :func:`updated_targets()`.
@@ -174,14 +174,14 @@ def get_one_valid_targetinfo(
174174
TODO: download-related errors
175175
176176
Returns:
177-
A targetinfo dictionary or None
177+
A TargetFile instance or None.
178178
"""
179179
return self._preorder_depth_first_walk(target_path)
180180

181181
@staticmethod
182182
def updated_targets(
183-
targets: List[Dict[str, Any]], destination_directory: str
184-
) -> List[Dict[str, Any]]:
183+
targets: List[TargetFile], destination_directory: str
184+
) -> List[TargetFile]:
185185
"""Checks whether local cached target files are up to date
186186
187187
After retrieving the target information for the targets that should be
@@ -204,17 +204,14 @@ def updated_targets(
204204
# against each hash listed for its fileinfo. Note: join() discards
205205
# 'destination_directory' if 'filepath' contains a leading path
206206
# separator (i.e., is treated as an absolute path).
207-
filepath = target["filepath"]
208-
target_fileinfo: "TargetFile" = target["fileinfo"]
209-
210-
target_filepath = os.path.join(destination_directory, filepath)
207+
target_filepath = os.path.join(destination_directory, target.path)
211208

212209
if target_filepath in updated_targetpaths:
213210
continue
214211

215212
try:
216213
with open(target_filepath, "rb") as target_file:
217-
target_fileinfo.verify_length_and_hashes(target_file)
214+
target.verify_length_and_hashes(target_file)
218215
# If the file does not exist locally or length and hashes
219216
# do not match, append to updated targets.
220217
except (OSError, exceptions.LengthOrHashMismatchError):
@@ -225,15 +222,15 @@ def updated_targets(
225222

226223
def download_target(
227224
self,
228-
targetinfo: Dict,
225+
targetinfo: TargetFile,
229226
destination_directory: str,
230227
target_base_url: Optional[str] = None,
231228
):
232229
"""Downloads the target file specified by 'targetinfo'.
233230
234231
Args:
235-
targetinfo: data received from get_one_valid_targetinfo() or
236-
updated_targets().
232+
targetinfo: TargetFile instance received from
233+
get_one_valid_targetinfo() or updated_targets().
237234
destination_directory: existing local directory to download into.
238235
Note that new directories may be created inside
239236
destination_directory as required.
@@ -254,27 +251,26 @@ def download_target(
254251
else:
255252
target_base_url = _ensure_trailing_slash(target_base_url)
256253

257-
target_fileinfo: "TargetFile" = targetinfo["fileinfo"]
258-
target_filepath = targetinfo["filepath"]
254+
target_filepath = targetinfo.path
259255
consistent_snapshot = self._trusted_set.root.signed.consistent_snapshot
260256
if consistent_snapshot and self.config.prefix_targets_with_hash:
261-
hashes = list(target_fileinfo.hashes.values())
257+
hashes = list(targetinfo.hashes.values())
262258
target_filepath = f"{hashes[0]}.{target_filepath}"
263259
full_url = parse.urljoin(target_base_url, target_filepath)
264260

265261
with self._fetcher.download_file(
266-
full_url, target_fileinfo.length
262+
full_url, targetinfo.length
267263
) as target_file:
268264
try:
269-
target_fileinfo.verify_length_and_hashes(target_file)
265+
targetinfo.verify_length_and_hashes(target_file)
270266
except exceptions.LengthOrHashMismatchError as e:
271267
raise exceptions.RepositoryError(
272268
f"{target_filepath} length or hashes do not match"
273269
) from e
274270

275271
# Store the target file name without the HASH prefix.
276272
local_filepath = os.path.join(
277-
destination_directory, targetinfo["filepath"]
273+
destination_directory, targetinfo.path
278274
)
279275
sslib_util.persist_temp_file(target_file, local_filepath)
280276

@@ -380,7 +376,7 @@ def _load_targets(self, role: str, parent_role: str) -> None:
380376

381377
def _preorder_depth_first_walk(
382378
self, target_filepath: str
383-
) -> Optional[Dict[str, Any]]:
379+
) -> Optional[TargetFile]:
384380
"""
385381
Interrogates the tree of target delegations in order of appearance
386382
(which implicitly order trustworthiness), and returns the matching
@@ -413,7 +409,7 @@ def _preorder_depth_first_walk(
413409

414410
if target is not None:
415411
logger.debug("Found target in current role %s", role_name)
416-
return {"filepath": target_filepath, "fileinfo": target}
412+
return target
417413

418414
# After preorder check, add current role to set of visited roles.
419415
visited_role_names.add((role_name, parent_role))

0 commit comments

Comments
 (0)