Skip to content

Commit f6bec3b

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 25993d6 commit f6bec3b

File tree

4 files changed

+29
-27
lines changed

4 files changed

+29
-27
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

tuf/api/metadata.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -1117,13 +1117,15 @@ class TargetFile(BaseFile):
11171117
Attributes:
11181118
length: An integer indicating the length of the target file.
11191119
hashes: A dictionary of hash algorithm names to hash values.
1120+
path: A string denoting the target file path.
11201121
unrecognized_fields: Dictionary of all unrecognized fields.
11211122
"""
11221123

11231124
def __init__(
11241125
self,
11251126
length: int,
11261127
hashes: Dict[str, str],
1128+
path: str,
11271129
unrecognized_fields: Optional[Mapping[str, Any]] = None,
11281130
) -> None:
11291131

@@ -1132,20 +1134,21 @@ def __init__(
11321134

11331135
self.length = length
11341136
self.hashes = hashes
1137+
self.path = path
11351138
self.unrecognized_fields = unrecognized_fields or {}
11361139

11371140
@property
11381141
def custom(self) -> Any:
11391142
return self.unrecognized_fields.get("custom", None)
11401143

11411144
@classmethod
1142-
def from_dict(cls, target_dict: Dict[str, Any]) -> "TargetFile":
1145+
def from_dict(cls, target_dict: Dict[str, Any], path: str) -> "TargetFile":
11431146
"""Creates TargetFile object from its dict representation."""
11441147
length = target_dict.pop("length")
11451148
hashes = target_dict.pop("hashes")
11461149

11471150
# All fields left in the target_dict are unrecognized.
1148-
return cls(length, hashes, target_dict)
1151+
return cls(length, hashes, path, target_dict)
11491152

11501153
def to_dict(self) -> Dict[str, Any]:
11511154
"""Returns the JSON-serializable dictionary representation of self."""
@@ -1211,7 +1214,9 @@ def from_dict(cls, signed_dict: Dict[str, Any]) -> "Targets":
12111214
delegations = Delegations.from_dict(delegations_dict)
12121215
res_targets = {}
12131216
for target_path, target_info in targets.items():
1214-
res_targets[target_path] = TargetFile.from_dict(target_info)
1217+
res_targets[target_path] = TargetFile.from_dict(
1218+
target_info, target_path
1219+
)
12151220
# All fields left in the targets_dict are unrecognized.
12161221
return cls(*common_args, res_targets, delegations, signed_dict)
12171222

tuf/ngclient/updater.py

+18-21
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,15 +249,14 @@ def download_target(
252249
else:
253250
target_base_url = _ensure_trailing_slash(target_base_url)
254251

255-
target_filepath = targetinfo["filepath"]
256-
target_fileinfo: "TargetFile" = targetinfo["fileinfo"]
252+
target_filepath = targetinfo.path
257253
full_url = parse.urljoin(target_base_url, target_filepath)
258254

259255
with self._fetcher.download_file(
260-
full_url, target_fileinfo.length
256+
full_url, targetinfo.length
261257
) as target_file:
262258
try:
263-
target_fileinfo.verify_length_and_hashes(target_file)
259+
targetinfo.verify_length_and_hashes(target_file)
264260
except exceptions.LengthOrHashMismatchError as e:
265261
raise exceptions.RepositoryError(
266262
f"{target_filepath} length or hashes do not match"
@@ -374,7 +370,7 @@ def _load_targets(self, role: str, parent_role: str) -> None:
374370

375371
def _preorder_depth_first_walk(
376372
self, target_filepath: str
377-
) -> Optional[Dict[str, Any]]:
373+
) -> Optional[TargetFile]:
378374
"""
379375
Interrogates the tree of target delegations in order of appearance
380376
(which implicitly order trustworthiness), and returns the matching
@@ -407,7 +403,8 @@ def _preorder_depth_first_walk(
407403

408404
if target is not None:
409405
logger.debug("Found target in current role %s", role_name)
410-
return {"filepath": target_filepath, "fileinfo": target}
406+
target.targetname = target_filepath
407+
return target
411408

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

0 commit comments

Comments
 (0)