1616
1717"""
1818import abc
19+ import io
1920import tempfile
2021from datetime import datetime , timedelta
21- from typing import Any , ClassVar , Dict , List , Mapping , Optional , Tuple , Type
22+ from typing import (
23+ Any ,
24+ BinaryIO ,
25+ ClassVar ,
26+ Dict ,
27+ List ,
28+ Mapping ,
29+ Optional ,
30+ Tuple ,
31+ Type ,
32+ Union ,
33+ )
2234
35+ from securesystemslib import hash as sslib_hash
2336from securesystemslib import keys as sslib_keys
2437from securesystemslib .signer import Signature , Signer
2538from securesystemslib .storage import FilesystemBackend , StorageBackendInterface
@@ -622,7 +635,53 @@ def remove_key(self, role: str, keyid: str) -> None:
622635 del self .keys [keyid ]
623636
624637
625- class MetaFile :
638+ class BaseFile :
639+ """A base class of MetaFile and TargetFile.
640+
641+ Encapsulates common static methods for length and hash verification.
642+ """
643+
644+ @staticmethod
645+ def _verify_hashes (
646+ data : Union [bytes , BinaryIO ], expected_hashes : Dict [str , str ]
647+ ) -> None :
648+ """Verifies that the hash of 'data' matches 'expected_hashes'"""
649+ is_bytes = isinstance (data , bytes )
650+ for algo , exp_hash in expected_hashes .items ():
651+ if is_bytes :
652+ digest_object = sslib_hash .digest (algo )
653+ digest_object .update (data )
654+ else :
655+ # if data is not bytes, assume it is a file object
656+ digest_object = sslib_hash .digest_fileobject (data , algo )
657+
658+ observed_hash = digest_object .hexdigest ()
659+ if observed_hash != exp_hash :
660+ raise exceptions .LengthOrHashMismatchError (
661+ f"Observed hash { observed_hash } does not match"
662+ f"expected hash { exp_hash } "
663+ )
664+
665+ @staticmethod
666+ def _verify_length (
667+ data : Union [bytes , BinaryIO ], expected_length : int
668+ ) -> None :
669+ """Verifies that the length of 'data' matches 'expected_length'"""
670+ if isinstance (data , bytes ):
671+ observed_length = len (data )
672+ else :
673+ # if data is not bytes, assume it is a file object
674+ data .seek (0 , io .SEEK_END )
675+ observed_length = data .tell ()
676+
677+ if observed_length != expected_length :
678+ raise exceptions .LengthOrHashMismatchError (
679+ f"Observed length { observed_length } does not match"
680+ f"expected length { expected_length } "
681+ )
682+
683+
684+ class MetaFile (BaseFile ):
626685 """A container with information about a particular metadata file.
627686
628687 Attributes:
@@ -678,6 +737,22 @@ def to_dict(self) -> Dict[str, Any]:
678737
679738 return res_dict
680739
740+ def verify_length_and_hashes (self , data : Union [bytes , BinaryIO ]):
741+ """Verifies that the length and hashes of "data" match expected
742+ values.
743+ Args:
744+ data: File object or its content in bytes.
745+ Raises:
746+ LengthOrHashMismatchError: Calculated length or hashes do not
747+ match expected values.
748+ """
749+ if self .length is not None :
750+ self ._verify_length (data , self .length )
751+
752+ # Skip the check in case of an empty dictionary too
753+ if self .hashes :
754+ self ._verify_hashes (data , self .hashes )
755+
681756
682757class Timestamp (Signed ):
683758 """A container for the signed part of timestamp metadata.
@@ -905,7 +980,7 @@ def to_dict(self) -> Dict[str, Any]:
905980 }
906981
907982
908- class TargetFile :
983+ class TargetFile ( BaseFile ) :
909984 """A container with information about a particular target file.
910985
911986 Attributes:
@@ -923,12 +998,6 @@ class TargetFile:
923998
924999 """
9251000
926- @property
927- def custom (self ):
928- if self .unrecognized_fields is None :
929- return None
930- return self .unrecognized_fields .get ("custom" , None )
931-
9321001 def __init__ (
9331002 self ,
9341003 length : int ,
@@ -939,6 +1008,12 @@ def __init__(
9391008 self .hashes = hashes
9401009 self .unrecognized_fields = unrecognized_fields or {}
9411010
1011+ @property
1012+ def custom (self ):
1013+ if self .unrecognized_fields is None :
1014+ return None
1015+ return self .unrecognized_fields .get ("custom" , None )
1016+
9421017 @classmethod
9431018 def from_dict (cls , target_dict : Dict [str , Any ]) -> "TargetFile" :
9441019 """Creates TargetFile object from its dict representation."""
@@ -955,6 +1030,18 @@ def to_dict(self) -> Dict[str, Any]:
9551030 ** self .unrecognized_fields ,
9561031 }
9571032
1033+ def verify_length_and_hashes (self , data : Union [bytes , BinaryIO ]):
1034+ """Verifies that the length and hashes of "data" match expected
1035+ values.
1036+ Args:
1037+ data: File object or its content in bytes.
1038+ Raises:
1039+ LengthOrHashMismatchError: Calculated length or hashes do not
1040+ match expected values.
1041+ """
1042+ self ._verify_length (data , self .length )
1043+ self ._verify_hashes (data , self .hashes )
1044+
9581045
9591046class Targets (Signed ):
9601047 """A container for the signed part of targets metadata.
0 commit comments