diff --git a/tests/test_api.py b/tests/test_api.py index 4d8ef1f9..76b8ba45 100755 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -47,6 +47,7 @@ import_ed25519_privatekey_from_file ) +from securesystemslib import hash as sslib_hash from securesystemslib.signer import ( SSlibSigner, Signature @@ -621,7 +622,49 @@ def test_length_and_hash_validation(self): file1_targetfile.length = expected_length file1_targetfile.hashes = {'sha256': 'incorrecthash'} self.assertRaises(exceptions.LengthOrHashMismatchError, - file1_targetfile.verify_length_and_hashes, file1) + file1_targetfile.verify_length_and_hashes, file1) + + def test_targetfile_from_file(self): + # Test with an existing file and valid hash algorithm + file_path = os.path.join(self.repo_dir, 'targets', 'file1.txt') + targetfile_from_file = TargetFile.from_file( + file_path, file_path, ['sha256'] + ) + + with open(file_path, "rb") as file: + targetfile_from_file.verify_length_and_hashes(file) + + # Test with a non-existing file + file_path = os.path.join(self.repo_dir, 'targets', 'file123.txt') + self.assertRaises( + FileNotFoundError, + TargetFile.from_file, + file_path, + file_path, + [sslib_hash.DEFAULT_HASH_ALGORITHM] + ) + + # Test with an unsupported algorithm + file_path = os.path.join(self.repo_dir, 'targets', 'file1.txt') + self.assertRaises( + exceptions.UnsupportedAlgorithmError, + TargetFile.from_file, + file_path, + file_path, + ['123'] + ) + + def test_targetfile_from_data(self): + data = b"Inline test content" + target_file_path = os.path.join(self.repo_dir, 'targets', 'file1.txt') + + # Test with a valid hash algorithm + targetfile_from_data = TargetFile.from_data(target_file_path, data, ['sha256']) + targetfile_from_data.verify_length_and_hashes(data) + + # Test with no algorithms specified + targetfile_from_data = TargetFile.from_data(target_file_path, data) + targetfile_from_data.verify_length_and_hashes(data) def test_is_delegated_role(self): # test path matches diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index fb4a1daf..cca82471 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -1219,6 +1219,78 @@ def to_dict(self) -> Dict[str, Any]: **self.unrecognized_fields, } + @classmethod + def from_file( + cls, + target_file_path: str, + local_path: str, + hash_algorithms: Optional[List[str]] = None, + ) -> "TargetFile": + """Creates TargetFile object from a file. + Arguments: + target_file_path: The TargetFile path. + local_path: The local path to the file to create TargetFile from. + hash_algorithms: An optional list of hash algorithms to create + the hashes with. If not specified the securesystemslib default + hash algorithm is used. + Raises: + FileNotFoundError: The file doesn't exist. + UnsupportedAlgorithmError: The hash algorithms list + contains an unsupported algorithm. + """ + with open(local_path, "rb") as file: + return cls.from_data(target_file_path, file, hash_algorithms) + + @classmethod + def from_data( + cls, + target_file_path: str, + data: Union[bytes, IO[bytes]], + hash_algorithms: Optional[List[str]] = None, + ) -> "TargetFile": + """Creates TargetFile object from bytes. + Arguments: + target_file_path: The TargetFile path. + data: The data to create TargetFile from. + hash_algorithms: An optional list of hash algorithms to create + the hashes with. If not specified the securesystemslib default + hash algorithm is used. + Raises: + UnsupportedAlgorithmError: The hash algorithms list + contains an unsupported algorithm. + """ + if isinstance(data, bytes): + length = len(data) + else: + data.seek(0, io.SEEK_END) + length = data.tell() + + hashes = {} + + if hash_algorithms is None: + hash_algorithms = [sslib_hash.DEFAULT_HASH_ALGORITHM] + + for algorithm in hash_algorithms: + try: + if isinstance(data, bytes): + digest_object = sslib_hash.digest(algorithm) + digest_object.update(data) + else: + digest_object = sslib_hash.digest_fileobject( + data, algorithm + ) + except ( + sslib_exceptions.UnsupportedAlgorithmError, + sslib_exceptions.FormatError, + ) as e: + raise exceptions.UnsupportedAlgorithmError( + f"Unsupported algorithm '{algorithm}'" + ) from e + + hashes[algorithm] = digest_object.hexdigest() + + return cls(length, hashes, target_file_path) + def verify_length_and_hashes(self, data: Union[bytes, IO[bytes]]) -> None: """Verifies that length and hashes of "data" match expected values.