Skip to content

Commit da864ea

Browse files
committed
Metadata API: include target name in targetfile
Currently, TargetFile instances do not contain the filename of the file they represent. The API itself does not need it but it could be useful for users of the API. As an example, the current client returns a dict for get_one_valid_targetinfo(): that dict contains a filepath field and a targetinfo field (essentially TargetFile). We would like to keep a similar API, but avoid hand-crafted dicts. It would be much nicer to return a TargetFile that would contain the full "metadata" of the targetfile. Signed-off-by: Martin Vrachev <[email protected]>
1 parent d3441f0 commit da864ea

File tree

5 files changed

+33
-31
lines changed

5 files changed

+33
-31
lines changed

tests/test_api.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,7 @@ def test_metadata_targets(self):
539539
"sha512": "ef5beafa16041bcdd2937140afebd485296cd54f7348ecd5a4d035c09759608de467a7ac0eb58753d0242df873c305e8bffad2454aa48f44480f15efae1cacd0"
540540
}
541541

542-
fileinfo = TargetFile(length=28, hashes=hashes)
542+
fileinfo = TargetFile(length=28, hashes=hashes, path=filename)
543543

544544
# Assert that data is not aleady equal
545545
self.assertNotEqual(

tests/test_metadata_serialization.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def test_delegation_serialization(self, test_case_data: str):
296296
def test_invalid_targetfile_serialization(self, test_case_data: Dict[str, str]):
297297
case_dict = json.loads(test_case_data)
298298
with self.assertRaises(KeyError):
299-
TargetFile.from_dict(copy.deepcopy(case_dict))
299+
TargetFile.from_dict(copy.deepcopy(case_dict), "file1.txt")
300300

301301

302302
valid_targetfiles: DataSet = {
@@ -310,7 +310,7 @@ def test_invalid_targetfile_serialization(self, test_case_data: Dict[str, str]):
310310
@run_sub_tests_with_dataset(valid_targetfiles)
311311
def test_targetfile_serialization(self, test_case_data: str):
312312
case_dict = json.loads(test_case_data)
313-
target_file = TargetFile.from_dict(copy.copy(case_dict))
313+
target_file = TargetFile.from_dict(copy.copy(case_dict), "file1.txt")
314314
self.assertDictEqual(case_dict, target_file.to_dict())
315315

316316

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -1138,13 +1138,15 @@ class TargetFile(BaseFile):
11381138
Attributes:
11391139
length: An integer indicating the length of the target file.
11401140
hashes: A dictionary of hash algorithm names to hash values.
1141+
path: A string denoting the target file path.
11411142
unrecognized_fields: Dictionary of all unrecognized fields.
11421143
"""
11431144

11441145
def __init__(
11451146
self,
11461147
length: int,
11471148
hashes: Dict[str, str],
1149+
path: str,
11481150
unrecognized_fields: Optional[Mapping[str, Any]] = None,
11491151
) -> None:
11501152

@@ -1153,20 +1155,21 @@ def __init__(
11531155

11541156
self.length = length
11551157
self.hashes = hashes
1158+
self.path = path
11561159
self.unrecognized_fields = unrecognized_fields or {}
11571160

11581161
@property
11591162
def custom(self) -> Any:
11601163
return self.unrecognized_fields.get("custom", None)
11611164

11621165
@classmethod
1163-
def from_dict(cls, target_dict: Dict[str, Any]) -> "TargetFile":
1166+
def from_dict(cls, target_dict: Dict[str, Any], path: str) -> "TargetFile":
11641167
"""Creates TargetFile object from its dict representation."""
11651168
length = target_dict.pop("length")
11661169
hashes = target_dict.pop("hashes")
11671170

11681171
# All fields left in the target_dict are unrecognized.
1169-
return cls(length, hashes, target_dict)
1172+
return cls(length, hashes, path, target_dict)
11701173

11711174
def to_dict(self) -> Dict[str, Any]:
11721175
"""Returns the JSON-serializable dictionary representation of self."""
@@ -1232,7 +1235,9 @@ def from_dict(cls, signed_dict: Dict[str, Any]) -> "Targets":
12321235
delegations = Delegations.from_dict(delegations_dict)
12331236
res_targets = {}
12341237
for target_path, target_info in targets.items():
1235-
res_targets[target_path] = TargetFile.from_dict(target_info)
1238+
res_targets[target_path] = TargetFile.from_dict(
1239+
target_info, target_path
1240+
)
12361241
# All fields left in the targets_dict are unrecognized.
12371242
return cls(*common_args, res_targets, delegations, signed_dict)
12381243

tuf/ngclient/updater.py

+20-23
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,13 @@
6060

6161
import logging
6262
import os
63-
from typing import Any, Dict, List, Optional, Set, Tuple
63+
from typing import List, Optional, Set, Tuple
6464
from urllib import parse
6565

6666
from securesystemslib import util as sslib_util
6767

6868
from tuf import exceptions
69-
from tuf.api.metadata import Targets
69+
from tuf.api.metadata import TargetFile, Targets
7070
from tuf.ngclient._internal import requests_fetcher, trusted_metadata_set
7171
from tuf.ngclient.config import UpdaterConfig
7272
from tuf.ngclient.fetcher import FetcherInterface
@@ -144,8 +144,8 @@ def refresh(self) -> None:
144144

145145
def get_one_valid_targetinfo(
146146
self, target_path: str
147-
) -> Optional[Dict[str, Any]]:
148-
"""Returns target information for 'target_path'.
147+
) -> Optional[TargetFile]:
148+
"""Returns TargetFile instance with information for 'target_path'.
149149
150150
The return value can be used as an argument to
151151
:func:`download_target()` and :func:`updated_targets()`.
@@ -172,14 +172,14 @@ def get_one_valid_targetinfo(
172172
TODO: download-related errors
173173
174174
Returns:
175-
A targetinfo dictionary or None
175+
A TargetFile instance or None.
176176
"""
177177
return self._preorder_depth_first_walk(target_path)
178178

179179
@staticmethod
180180
def updated_targets(
181-
targets: List[Dict[str, Any]], destination_directory: str
182-
) -> List[Dict[str, Any]]:
181+
targets: List[TargetFile], destination_directory: str
182+
) -> List[TargetFile]:
183183
"""Checks whether local cached target files are up to date
184184
185185
After retrieving the target information for the targets that should be
@@ -202,17 +202,14 @@ def updated_targets(
202202
# against each hash listed for its fileinfo. Note: join() discards
203203
# 'destination_directory' if 'filepath' contains a leading path
204204
# separator (i.e., is treated as an absolute path).
205-
filepath = target["filepath"]
206-
target_fileinfo: "TargetFile" = target["fileinfo"]
207-
208-
target_filepath = os.path.join(destination_directory, filepath)
205+
target_filepath = os.path.join(destination_directory, target.path)
209206

210207
if target_filepath in updated_targetpaths:
211208
continue
212209

213210
try:
214211
with open(target_filepath, "rb") as target_file:
215-
target_fileinfo.verify_length_and_hashes(target_file)
212+
target.verify_length_and_hashes(target_file)
216213
# If the file does not exist locally or length and hashes
217214
# do not match, append to updated targets.
218215
except (OSError, exceptions.LengthOrHashMismatchError):
@@ -223,15 +220,15 @@ def updated_targets(
223220

224221
def download_target(
225222
self,
226-
targetinfo: Dict,
223+
targetinfo: TargetFile,
227224
destination_directory: str,
228225
target_base_url: Optional[str] = None,
229226
):
230227
"""Downloads the target file specified by 'targetinfo'.
231228
232229
Args:
233-
targetinfo: data received from get_one_valid_targetinfo() or
234-
updated_targets().
230+
targetinfo: TargetFile instance received from
231+
get_one_valid_targetinfo() or updated_targets().
235232
destination_directory: existing local directory to download into.
236233
Note that new directories may be created inside
237234
destination_directory as required.
@@ -252,27 +249,26 @@ def download_target(
252249
else:
253250
target_base_url = _ensure_trailing_slash(target_base_url)
254251

255-
target_fileinfo: "TargetFile" = targetinfo["fileinfo"]
256-
target_filepath = targetinfo["filepath"]
252+
target_filepath = targetinfo.path
257253
consistent_snapshot = self._trusted_set.root.signed.consistent_snapshot
258254
if consistent_snapshot and self.config.prefix_targets_with_hash:
259-
hashes = list(target_fileinfo.hashes.values())
255+
hashes = list(targetinfo.hashes.values())
260256
target_filepath = f"{hashes[0]}.{target_filepath}"
261257
full_url = parse.urljoin(target_base_url, target_filepath)
262258

263259
with self._fetcher.download_file(
264-
full_url, target_fileinfo.length
260+
full_url, targetinfo.length
265261
) as target_file:
266262
try:
267-
target_fileinfo.verify_length_and_hashes(target_file)
263+
targetinfo.verify_length_and_hashes(target_file)
268264
except exceptions.LengthOrHashMismatchError as e:
269265
raise exceptions.RepositoryError(
270266
f"{target_filepath} length or hashes do not match"
271267
) from e
272268

273269
# Store the target file name without the HASH prefix.
274270
local_filepath = os.path.join(
275-
destination_directory, targetinfo["filepath"]
271+
destination_directory, targetinfo.path
276272
)
277273
sslib_util.persist_temp_file(target_file, local_filepath)
278274

@@ -381,7 +377,7 @@ def _load_targets(self, role: str, parent_role: str) -> None:
381377

382378
def _preorder_depth_first_walk(
383379
self, target_filepath: str
384-
) -> Optional[Dict[str, Any]]:
380+
) -> Optional[TargetFile]:
385381
"""
386382
Interrogates the tree of target delegations in order of appearance
387383
(which implicitly order trustworthiness), and returns the matching
@@ -414,7 +410,8 @@ def _preorder_depth_first_walk(
414410

415411
if target is not None:
416412
logger.debug("Found target in current role %s", role_name)
417-
return {"filepath": target_filepath, "fileinfo": target}
413+
target.targetname = target_filepath
414+
return target
418415

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

0 commit comments

Comments
 (0)