diff --git a/tests/test_api.py b/tests/test_api.py index f4a596eaee..164d8b30cc 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2,7 +2,7 @@ # Copyright 2020, New York University and the TUF contributors # SPDX-License-Identifier: MIT OR Apache-2.0 -""" Unit tests for api/metdata.py +""" Unit tests for api/metadata.py Skipped on Python < 3.6. diff --git a/tuf/api/metadata.py b/tuf/api/metadata.py index dfa2ef5f10..f0c452ae26 100644 --- a/tuf/api/metadata.py +++ b/tuf/api/metadata.py @@ -77,14 +77,14 @@ def __init__( self.signatures = signatures def as_dict(self) -> JsonDict: - """Returns the JSON-compatible dictionary representation. """ + """Returns the JSON-serializable dictionary representation of self. """ return { 'signatures': self.signatures, 'signed': self.signed.as_dict() } def as_json(self, compact: bool = False) -> None: - """Returns the optionally compacted JSON representation. """ + """Returns the optionally compacted JSON representation of self. """ return json.dumps( self.as_dict(), indent=(None if compact else 1), @@ -141,7 +141,7 @@ def read_from_json( cls, filename: str, storage_backend: Optional[StorageBackendInterface] = None ) -> 'Metadata': - """Loads JSON-formatted TUF metadata from a file storage. + """Loads JSON-formatted TUF metadata from file storage. Arguments: filename: The path to read the file from. @@ -183,7 +183,7 @@ def read_from_json( def write_to_json( self, filename: str, compact: bool = False, storage_backend: StorageBackendInterface = None) -> None: - """Writes the JSON representation of the instance to file storage. + """Writes the JSON representation of self to file storage. Arguments: filename: The path to write the file to. @@ -203,6 +203,21 @@ def write_to_json( class Signed: + """A base class for the signed part of TUF metadata. + + Objects with base class Signed are usually included in a Metablock object + on the signed attribute. This class provides attributes and methods that + are common for all TUF metadata types (roles). + + Attributes: + _type: The metadata type string. + version: The metadata version number. + spec_version: The TUF specification version number (semver) the + metadata format adheres to. + expires: The metadata expiration date in 'YYYY-MM-DDTHH:MM:SSZ' format. + signed_bytes: The UTF-8 encoded canonical JSON representation of self. + + """ # NOTE: Signed is a stupid name, because this might not be signed yet, but # we keep it to match spec terminology (I often refer to this as "payload", # or "inner metadata") @@ -237,19 +252,18 @@ def signed_bytes(self) -> bytes: @property def expires(self) -> str: - """The expiration property in TUF metadata format.""" return self.__expiration.isoformat() + 'Z' def bump_expiration(self, delta: timedelta = timedelta(days=1)) -> None: + """Increments the expires attribute by the passed timedelta. """ self.__expiration = self.__expiration + delta def bump_version(self) -> None: + """Increments the metadata version number by 1.""" self.version += 1 def as_dict(self) -> JsonDict: - # NOTE: The classes should be the single source of truth about metadata - # let's define the dict representation here and not in some dubious - # build_dict_conforming_to_schema + """Returns the JSON-serializable dictionary representation of self. """ return { '_type': self._type, 'version': self.version, @@ -263,7 +277,24 @@ def read_from_json( storage_backend: Optional[StorageBackendInterface] = None ) -> Metadata: signable = load_json_file(filename, storage_backend) + """Loads corresponding JSON-formatted metadata from file storage. + + Arguments: + filename: The path to read the file from. + storage_backend: An object that implements + securesystemslib.storage.StorageBackendInterface. Per default + a (local) FilesystemBackend is used. + + Raises: + securesystemslib.exceptions.StorageError: The file cannot be read. + securesystemslib.exceptions.Error, ValueError: The metadata cannot + be parsed. + + Returns: + A TUF Metadata object whose signed attribute contains an object + of this class. + """ # FIXME: It feels dirty to access signable["signed"]["version"] here in # order to do this check, and also a bit random (there are likely other # things to check), but later we don't have the filename anymore. If we @@ -281,6 +312,20 @@ def read_from_json( class Timestamp(Signed): + """A container for the signed part of timestamp metadata. + + Attributes: + meta: A dictionary that contains information about snapshot metadata:: + + { + "snapshot.json": { + "version" : , + "length" : // optional + "hashes" : // optional + } + } + + """ def __init__(self, meta: JsonDict = None, **kwargs) -> None: super().__init__(**kwargs) # TODO: How much init magic do we want? @@ -288,14 +333,16 @@ def __init__(self, meta: JsonDict = None, **kwargs) -> None: self.meta = meta def as_dict(self) -> JsonDict: + """Returns the JSON-serializable dictionary representation of self. """ json_dict = super().as_dict() json_dict.update({ 'meta': self.meta }) return json_dict - # Update metadata about the snapshot metadata. def update(self, version: int, length: int, hashes: JsonDict) -> None: + """Assigns passed info about snapshot metadata to meta dictionary. """ + # TODO: Should we assign it fileinfo = self.meta.get('snapshot.json', {}) fileinfo['version'] = version fileinfo['length'] = length @@ -304,6 +351,27 @@ def update(self, version: int, length: int, hashes: JsonDict) -> None: class Snapshot(Signed): + """A container for the signed part of snapshot metadata. + + Attributes: + meta: A dictionary that contains information about targets metadata:: + + { + "targets.json": { + "version" : , + "length" : // optional + "hashes" : // optional + }, + ".json>: { + ... + }, + ".json>: { + ... + }, + ... + } + + """ def __init__(self, meta: JsonDict = None, **kwargs) -> None: # TODO: How much init magic do we want? # TODO: Is there merit in creating classes for dict fields? @@ -311,6 +379,7 @@ def __init__(self, meta: JsonDict = None, **kwargs) -> None: self.meta = meta def as_dict(self) -> JsonDict: + """Returns the JSON-serializable dictionary representation of self. """ json_dict = super().as_dict() json_dict.update({ 'meta': self.meta @@ -321,6 +390,7 @@ def as_dict(self) -> JsonDict: def update( self, rolename: str, version: int, length: Optional[int] = None, hashes: Optional[JsonDict] = None) -> None: + """Add or update the meta dictionary for a given targets role. """ metadata_fn = f'{rolename}.json' self.meta[metadata_fn] = {'version': version}