Skip to content

Commit bb0dc23

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 01e2308 commit bb0dc23

File tree

2 files changed

+26
-21
lines changed

2 files changed

+26
-21
lines changed

tuf/api/metadata.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -1085,13 +1085,15 @@ class TargetFile(BaseFile):
10851085
Attributes:
10861086
length: An integer indicating the length of the target file.
10871087
hashes: A dictionary of hash algorithm names to hash values.
1088+
targetname: An optional string denoting the target file name.
10881089
unrecognized_fields: Dictionary of all unrecognized fields.
10891090
"""
10901091

10911092
def __init__(
10921093
self,
10931094
length: int,
10941095
hashes: Dict[str, str],
1096+
targetname: Optional[str] = None,
10951097
unrecognized_fields: Optional[Mapping[str, Any]] = None,
10961098
) -> None:
10971099

@@ -1100,20 +1102,23 @@ def __init__(
11001102

11011103
self.length = length
11021104
self.hashes = hashes
1105+
self.targetname = targetname
11031106
self.unrecognized_fields = unrecognized_fields or {}
11041107

11051108
@property
11061109
def custom(self) -> Any:
11071110
return self.unrecognized_fields.get("custom", None)
11081111

11091112
@classmethod
1110-
def from_dict(cls, target_dict: Dict[str, Any]) -> "TargetFile":
1113+
def from_dict(
1114+
cls, target_dict: Dict[str, Any], targetname: Optional[str] = None
1115+
) -> "TargetFile":
11111116
"""Creates TargetFile object from its dict representation."""
11121117
length = target_dict.pop("length")
11131118
hashes = target_dict.pop("hashes")
11141119

11151120
# All fields left in the target_dict are unrecognized.
1116-
return cls(length, hashes, target_dict)
1121+
return cls(length, hashes, targetname, target_dict)
11171122

11181123
def to_dict(self) -> Dict[str, Any]:
11191124
"""Returns the JSON-serializable dictionary representation of self."""

tuf/ngclient/updater.py

+19-19
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,14 @@
6161
import fnmatch
6262
import logging
6363
import os
64-
from typing import Any, Dict, List, Optional
64+
from typing import Dict, List, Optional
6565
from urllib import parse
6666

6767
from securesystemslib import hash as sslib_hash
6868
from securesystemslib import util as sslib_util
6969

7070
from tuf import exceptions
71+
from tuf.api.metadata import TargetFile
7172
from tuf.ngclient._internal import requests_fetcher, trusted_metadata_set
7273
from tuf.ngclient.config import UpdaterConfig
7374
from tuf.ngclient.fetcher import FetcherInterface
@@ -143,8 +144,8 @@ def refresh(self) -> None:
143144
self._load_snapshot()
144145
self._load_targets("targets", "root")
145146

146-
def get_one_valid_targetinfo(self, target_path: str) -> Dict:
147-
"""Returns target information for 'target_path'.
147+
def get_one_valid_targetinfo(self, target_path: str) -> TargetFile:
148+
"""Returns TargetFile instance with information for 'target_path'.
148149
149150
The return value can be used as an argument to
150151
:func:`download_target()` and :func:`updated_targets()`.
@@ -174,8 +175,8 @@ def get_one_valid_targetinfo(self, target_path: str) -> Dict:
174175

175176
@staticmethod
176177
def updated_targets(
177-
targets: List[Dict[str, Any]], destination_directory: str
178-
) -> List[Dict[str, Any]]:
178+
targets: List[TargetFile], destination_directory: str
179+
) -> List[TargetFile]:
179180
"""Checks whether local cached target files are up to date
180181
181182
After retrieving the target information for the targets that should be
@@ -198,17 +199,16 @@ def updated_targets(
198199
# against each hash listed for its fileinfo. Note: join() discards
199200
# 'destination_directory' if 'filepath' contains a leading path
200201
# separator (i.e., is treated as an absolute path).
201-
filepath = target["filepath"]
202-
target_fileinfo: "TargetFile" = target["fileinfo"]
203-
204-
target_filepath = os.path.join(destination_directory, filepath)
202+
target_filepath = os.path.join(
203+
destination_directory, target.targetname
204+
)
205205

206206
if target_filepath in updated_targetpaths:
207207
continue
208208

209209
try:
210210
with open(target_filepath, "rb") as target_file:
211-
target_fileinfo.verify_length_and_hashes(target_file)
211+
target.verify_length_and_hashes(target_file)
212212
# If the file does not exist locally or length and hashes
213213
# do not match, append to updated targets.
214214
except (OSError, exceptions.LengthOrHashMismatchError):
@@ -219,15 +219,15 @@ def updated_targets(
219219

220220
def download_target(
221221
self,
222-
targetinfo: Dict,
222+
targetinfo: TargetFile,
223223
destination_directory: str,
224224
target_base_url: Optional[str] = None,
225225
):
226226
"""Downloads the target file specified by 'targetinfo'.
227227
228228
Args:
229-
targetinfo: data received from get_one_valid_targetinfo() or
230-
updated_targets().
229+
targetinfo: TargetFile instance received from
230+
get_one_valid_targetinfo() or updated_targets().
231231
destination_directory: existing local directory to download into.
232232
Note that new directories may be created inside
233233
destination_directory as required.
@@ -248,15 +248,14 @@ def download_target(
248248
else:
249249
target_base_url = _ensure_trailing_slash(target_base_url)
250250

251-
target_filepath = targetinfo["filepath"]
252-
target_fileinfo: "TargetFile" = targetinfo["fileinfo"]
251+
target_filepath = targetinfo.targetname
253252
full_url = parse.urljoin(target_base_url, target_filepath)
254253

255254
with self._fetcher.download_file(
256-
full_url, target_fileinfo.length
255+
full_url, targetinfo.length
257256
) as target_file:
258257
try:
259-
target_fileinfo.verify_length_and_hashes(target_file)
258+
targetinfo.verify_length_and_hashes(target_file)
260259
except exceptions.LengthOrHashMismatchError as e:
261260
raise exceptions.RepositoryError(
262261
f"{target_filepath} length or hashes do not match"
@@ -368,7 +367,7 @@ def _load_targets(self, role: str, parent_role: str) -> None:
368367
self._trusted_set.update_delegated_targets(data, role, parent_role)
369368
self._persist_metadata(role, data)
370369

371-
def _preorder_depth_first_walk(self, target_filepath) -> Dict:
370+
def _preorder_depth_first_walk(self, target_filepath) -> TargetFile:
372371
"""
373372
Interrogates the tree of target delegations in order of appearance
374373
(which implicitly order trustworthiness), and returns the matching
@@ -460,7 +459,8 @@ def _preorder_depth_first_walk(self, target_filepath) -> Dict:
460459
self.config.max_delegations,
461460
)
462461

463-
return {"filepath": target_filepath, "fileinfo": target}
462+
target.targetname = target_filepath
463+
return target
464464

465465

466466
def _visit_child_role(child_role: Dict, target_filepath: str) -> str:

0 commit comments

Comments
 (0)