From 24b1db678121eb68dc0ec194980c599567821fd4 Mon Sep 17 00:00:00 2001 From: Kamui Date: Wed, 16 Nov 2022 18:22:21 -0600 Subject: [PATCH 1/4] feat: generate hash-prefixed path names for target Signed-off-by: Kamui --- tests/test_api.py | 6 ++++++ tuf/api/metadata.py | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index fde1617f5a..d88f215db1 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -725,6 +725,12 @@ def test_targetfile_from_data(self) -> None: targetfile_from_data = TargetFile.from_data(target_file_path, data) targetfile_from_data.verify_length_and_hashes(data) + def test_targetfile_hash_prefix_paths(self) -> None: + target = TargetFile( + 100, {"sha256": "abc", "md5": "def"}, "public/path/file.ext" + ) + self.assertEqual(sorted(target.get_prefixed_paths()), ["public/path/abc.file.ext", "public/path/def.file.ext"]) + def test_is_delegated_role(self) -> None: # test path matches # see more extensive tests in test_is_target_in_pathpattern() diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index c21fc3eb31..25380f7dd8 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -33,6 +33,7 @@ import io import logging import tempfile +import pathlib from datetime import datetime from typing import ( IO, @@ -1737,6 +1738,14 @@ def verify_length_and_hashes(self, data: Union[bytes, IO[bytes]]) -> None: self._verify_length(data, self.length) self._verify_hashes(data, self.hashes) + def get_prefixed_paths(self) -> List[str]: + paths = [] + path = pathlib.Path(self.path) + name, parent = path.name, path.parent + for hash in self.hashes.values(): + paths.append(str(parent.joinpath(f"{hash}.{name}"))) + return paths + class Targets(Signed): """A container for the signed part of targets metadata. From 1e47e390fbf89a261594046d302d3e5e4b352106 Mon Sep 17 00:00:00 2001 From: Kamui Date: Wed, 16 Nov 2022 18:28:46 -0600 Subject: [PATCH 2/4] docs: add docstring for method Signed-off-by: Kamui --- tuf/api/metadata.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 25380f7dd8..eefd5f0436 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -1739,6 +1739,9 @@ def verify_length_and_hashes(self, data: Union[bytes, IO[bytes]]) -> None: self._verify_hashes(data, self.hashes) def get_prefixed_paths(self) -> List[str]: + """ + Returns hash-prefixed paths for the given target file path. + """ paths = [] path = pathlib.Path(self.path) name, parent = path.name, path.parent From 0eef15ad2828f558f19973071b8ea6e1a58d1870 Mon Sep 17 00:00:00 2001 From: Kamui Date: Fri, 18 Nov 2022 16:36:08 -0600 Subject: [PATCH 3/4] fix: parse manually and handle url edge cases Signed-off-by: Kamui --- tests/test_api.py | 30 ++++++++++++++++++++++++++++++ tuf/api/metadata.py | 23 +++++++++++++++++++---- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index d88f215db1..1bcf422a36 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -731,6 +731,36 @@ def test_targetfile_hash_prefix_paths(self) -> None: ) self.assertEqual(sorted(target.get_prefixed_paths()), ["public/path/abc.file.ext", "public/path/def.file.ext"]) + target = TargetFile( + 100, {"sha256": "abc", "md5": "def"}, "" + ) + self.assertEqual(sorted(target.get_prefixed_paths()), []) + + target = TargetFile( + 100, {"sha256": "abc", "md5": "def"}, "public/path/" + ) + self.assertEqual(sorted(target.get_prefixed_paths()), []) + + target = TargetFile( + 100, {"sha256": "abc", "md5": "def"}, "file.ext" + ) + self.assertEqual(sorted(target.get_prefixed_paths()), ["abc.file.ext", "def.file.ext"]) + + target = TargetFile( + 100, {"sha256": "abc", "md5": "def"}, "public/path/.ext" + ) + self.assertEqual(sorted(target.get_prefixed_paths()), ["public/path/abc..ext", "public/path/def..ext"]) + + target = TargetFile( + 100, {"sha256": "abc"}, "/root/file.ext" + ) + self.assertEqual(sorted(target.get_prefixed_paths()), ["/root/abc.file.ext"]) + + target = TargetFile( + 100, {"sha256": "abc"}, "/" + ) + self.assertEqual(sorted(target.get_prefixed_paths()), []) + def test_is_delegated_role(self) -> None: # test path matches # see more extensive tests in test_is_target_in_pathpattern() diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index eefd5f0436..1dff113350 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -1740,13 +1740,28 @@ def verify_length_and_hashes(self, data: Union[bytes, IO[bytes]]) -> None: def get_prefixed_paths(self) -> List[str]: """ - Returns hash-prefixed paths for the given target file path. + Returns hash-prefixed paths for the given target file path. Empty result in the case of an invalid path. + + Expects self.path to be a URL path fragment, not a filesystem path. """ paths = [] - path = pathlib.Path(self.path) - name, parent = path.name, path.parent + try: + if not self.path: + raise ValueError + elif "/" not in self.path: + parent, name = None, self.path + else: + parent, name = self.path.rsplit("/", 1) + if name == "": + raise ValueError + except ValueError: + return paths + for hash in self.hashes.values(): - paths.append(str(parent.joinpath(f"{hash}.{name}"))) + path = f"{hash}.{name}" + if parent is not None: + path = f"{parent}/{path}" + paths.append(path) return paths From cddae3b892e331b6faed6ece57f72ac0690006a0 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen Date: Mon, 20 Mar 2023 16:12:00 +0200 Subject: [PATCH 4/4] Updates to TargetFile.get_prefixed_paths() * Use the same solution for producing the paths as we already do in ngclient * Fix linting issues * Modify the test results according to new code (I believe these are correct, although some cases are so edge cases that disagreement may exist. Most importantly I think the method should always return as many paths as there are hashes listed Signed-off-by: Jussi Kukkonen --- tests/test_api.py | 44 ++++++++++++++++++-------------------------- tuf/api/metadata.py | 24 ++++-------------------- 2 files changed, 22 insertions(+), 46 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 1bcf422a36..14ae12c973 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -725,41 +725,33 @@ def test_targetfile_from_data(self) -> None: targetfile_from_data = TargetFile.from_data(target_file_path, data) targetfile_from_data.verify_length_and_hashes(data) - def test_targetfile_hash_prefix_paths(self) -> None: - target = TargetFile( - 100, {"sha256": "abc", "md5": "def"}, "public/path/file.ext" + def test_targetfile_get_prefixed_paths(self) -> None: + target = TargetFile(100, {"sha256": "abc", "md5": "def"}, "a/b/f.ext") + self.assertEqual( + target.get_prefixed_paths(), ["a/b/abc.f.ext", "a/b/def.f.ext"] ) - self.assertEqual(sorted(target.get_prefixed_paths()), ["public/path/abc.file.ext", "public/path/def.file.ext"]) - target = TargetFile( - 100, {"sha256": "abc", "md5": "def"}, "" - ) - self.assertEqual(sorted(target.get_prefixed_paths()), []) + target = TargetFile(100, {"sha256": "abc", "md5": "def"}, "") + self.assertEqual(target.get_prefixed_paths(), ["abc.", "def."]) - target = TargetFile( - 100, {"sha256": "abc", "md5": "def"}, "public/path/" - ) - self.assertEqual(sorted(target.get_prefixed_paths()), []) + target = TargetFile(100, {"sha256": "abc", "md5": "def"}, "a/b/") + self.assertEqual(target.get_prefixed_paths(), ["a/b/abc.", "a/b/def."]) - target = TargetFile( - 100, {"sha256": "abc", "md5": "def"}, "file.ext" + target = TargetFile(100, {"sha256": "abc", "md5": "def"}, "f.ext") + self.assertEqual( + target.get_prefixed_paths(), ["abc.f.ext", "def.f.ext"] ) - self.assertEqual(sorted(target.get_prefixed_paths()), ["abc.file.ext", "def.file.ext"]) - target = TargetFile( - 100, {"sha256": "abc", "md5": "def"}, "public/path/.ext" + target = TargetFile(100, {"sha256": "abc", "md5": "def"}, "a/b/.ext") + self.assertEqual( + target.get_prefixed_paths(), ["a/b/abc..ext", "a/b/def..ext"] ) - self.assertEqual(sorted(target.get_prefixed_paths()), ["public/path/abc..ext", "public/path/def..ext"]) - target = TargetFile( - 100, {"sha256": "abc"}, "/root/file.ext" - ) - self.assertEqual(sorted(target.get_prefixed_paths()), ["/root/abc.file.ext"]) + target = TargetFile(100, {"sha256": "abc"}, "/root/file.ext") + self.assertEqual(target.get_prefixed_paths(), ["/root/abc.file.ext"]) - target = TargetFile( - 100, {"sha256": "abc"}, "/" - ) - self.assertEqual(sorted(target.get_prefixed_paths()), []) + target = TargetFile(100, {"sha256": "abc"}, "/") + self.assertEqual(target.get_prefixed_paths(), ["/abc."]) def test_is_delegated_role(self) -> None: # test path matches diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index 1dff113350..cf739892eb 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -33,7 +33,6 @@ import io import logging import tempfile -import pathlib from datetime import datetime from typing import ( IO, @@ -1740,28 +1739,13 @@ def verify_length_and_hashes(self, data: Union[bytes, IO[bytes]]) -> None: def get_prefixed_paths(self) -> List[str]: """ - Returns hash-prefixed paths for the given target file path. Empty result in the case of an invalid path. - - Expects self.path to be a URL path fragment, not a filesystem path. + Return hash-prefixed URL path fragments for the target file path. """ paths = [] - try: - if not self.path: - raise ValueError - elif "/" not in self.path: - parent, name = None, self.path - else: - parent, name = self.path.rsplit("/", 1) - if name == "": - raise ValueError - except ValueError: - return paths + parent, sep, name = self.path.rpartition("/") + for hash_value in self.hashes.values(): + paths.append(f"{parent}{sep}{hash_value}.{name}") - for hash in self.hashes.values(): - path = f"{hash}.{name}" - if parent is not None: - path = f"{parent}/{path}" - paths.append(path) return paths