2929import tempfile
3030
3131from securesystemslib .formats import encode_canonical
32- from securesystemslib .util import load_json_file , persist_temp_file
32+ from securesystemslib .util import (
33+ load_json_file ,
34+ load_json_string ,
35+ persist_temp_file
36+ )
3337from securesystemslib .storage import StorageBackendInterface
3438from securesystemslib .keys import create_signature , verify_signature
3539from tuf .repository_lib import (
@@ -69,12 +73,34 @@ class Metadata():
6973 ]
7074
7175 """
72- def __init__ (
73- self , signed : 'Signed' = None , signatures : list = None ) -> None :
74- # TODO: How much init magic do we want?
76+ def __init__ (self , signed : 'Signed' , signatures : list ) -> None :
7577 self .signed = signed
7678 self .signatures = signatures
7779
80+ @classmethod
81+ def from_dict (cls , metadata : JsonDict ) -> 'Metadata' :
82+ _type = metadata ['signed' ]['_type' ]
83+
84+ if _type == 'targets' :
85+ signed_cls = Targets
86+ elif _type == 'snapshot' :
87+ signed_cls = Snapshot
88+ elif _type == 'timestamp' :
89+ signed_cls = Timestamp
90+ elif _type == 'root' :
91+ # TODO: implement Root class
92+ raise NotImplementedError ('Root not yet implemented' )
93+ else :
94+ raise ValueError (f'unrecognized metadata type "{ _type } "' )
95+
96+ return Metadata (
97+ signed = signed_cls .from_dict (metadata ['signed' ]),
98+ signatures = metadata ['signatures' ])
99+
100+ def from_json (metadata_json : str ) -> 'Metadata' :
101+ """Deserialize a json string and pass to from_dict(). """
102+ return Metadata .from_dict (load_json_string (metadata_json ))
103+
78104
79105 def to_json (self , compact : bool = False ) -> None :
80106 """Returns the optionally compacted JSON representation of self. """
@@ -174,27 +200,7 @@ def from_json_file(
174200 A TUF Metadata object.
175201
176202 """
177- signable = load_json_file (filename , storage_backend )
178-
179- # TODO: Should we use constants?
180- # And/or maybe a dispatch table? (<-- maybe too much magic)
181- _type = signable ['signed' ]['_type' ]
182-
183- if _type == 'targets' :
184- inner_cls = Targets
185- elif _type == 'snapshot' :
186- inner_cls = Snapshot
187- elif _type == 'timestamp' :
188- inner_cls = Timestamp
189- elif _type == 'root' :
190- # TODO: implement Root class
191- raise NotImplementedError ('Root not yet implemented' )
192- else :
193- raise ValueError (f'unrecognized metadata type "{ _type } "' )
194-
195- return Metadata (
196- signed = inner_cls (** signable ['signed' ]),
197- signatures = signable ['signatures' ])
203+ return Metadata .from_dict (load_json_file (filename , storage_backend ))
198204
199205 def to_json_file (
200206 self , filename : str , compact : bool = False ,
@@ -237,41 +243,42 @@ class Signed:
237243 # we keep it to match spec terminology (I often refer to this as "payload",
238244 # or "inner metadata")
239245
240- # TODO: Re-think default values. It might be better to pass some things
241- # as args and not es kwargs. Then we'd need to pop those from
242- # signable["signed"] in read_from_json and pass them explicitly, which
243- # some say is better than implicit. :)
244246 def __init__ (
245- self , _type : str = None , version : int = 0 ,
246- spec_version : str = None , expires : datetime = None
247- ) -> None :
248- # TODO: How much init magic do we want?
247+ self , _type : str , version : int , spec_version : str ,
248+ expires : datetime ) -> None :
249249
250250 self ._type = _type
251+ self .version = version
251252 self .spec_version = spec_version
253+ self .expires = expires
252254
253- # We always intend times to be UTC
254- # NOTE: we could do this with datetime.fromisoformat() but that is not
255- # available in Python 2.7's datetime
256- # NOTE: Store as datetime object for convenient handling, use 'expires'
257- # property to get the TUF metadata format representation
258- self .__expiration = iso8601 .parse_date (expires ).replace (tzinfo = None )
259-
255+ # TODO: Should we separate data validation from constructor?
260256 if version < 0 :
261257 raise ValueError (f'version must be < 0, got { version } ' )
258+
262259 self .version = version
263260
264- @property
265- def signed_bytes (self ) -> bytes :
266- return encode_canonical (self .as_dict ()).encode ('UTF-8' )
261+ @classmethod
262+ def from_dict (cls , signed_dict ) -> 'Signed' :
263+ # NOTE: Convert 'expires' TUF metadata string representation
264+ # to datetime object, as the constructor expects it. See 'to_dict' for
265+ # the inverse operation.
266+ signed_dict ['expires' ] = iso8601 .parse_date (
267+ signed_dict ['expires' ]).replace (tzinfo = None )
268+
269+ # NOTE: Any additional conversion of dict metadata should happen here
270+ # or in the corresponding derived class of 'Signed'. E.g. if the
271+ # delegations field
272+
273+ return cls (** signed_dict )
267274
268275 @property
269- def expires (self ) -> str :
270- return self .__expiration . isoformat () + 'Z'
276+ def signed_bytes (self ) -> bytes :
277+ return encode_canonical ( self .to_dict ()). encode ( 'UTF-8' )
271278
272279 def bump_expiration (self , delta : timedelta = timedelta (days = 1 )) -> None :
273280 """Increments the expires attribute by the passed timedelta. """
274- self .__expiration = self . __expiration + delta
281+ self .expires += delta
275282
276283 def bump_version (self ) -> None :
277284 """Increments the metadata version number by 1."""
@@ -283,7 +290,7 @@ def to_dict(self) -> JsonDict:
283290 '_type' : self ._type ,
284291 'version' : self .version ,
285292 'spec_version' : self .spec_version ,
286- 'expires' : self .expires
293+ 'expires' : self .expires . isoformat () + 'Z'
287294 }
288295
289296class Timestamp (Signed ):
@@ -305,12 +312,17 @@ class Timestamp(Signed):
305312 }
306313
307314 """
308- def __init__ (self , meta : JsonDict = None , ** kwargs ) -> None :
309- super ().__init__ (** kwargs )
310- # TODO: How much init magic do we want?
311- # TODO: Is there merit in creating classes for dict fields?
315+ def __init__ (
316+ self , _type : str , version : int , spec_version : str ,
317+ expires : datetime , meta : JsonDict ) -> None :
318+ super ().__init__ (_type , version , spec_version , expires )
319+ # TODO: Add class for meta
312320 self .meta = meta
313321
322+ @classmethod
323+ def from_dict (cls , timestamp_dict ):
324+ return super ().from_dict (timestamp_dict )
325+
314326 def to_dict (self ) -> JsonDict :
315327 """Returns the JSON-serializable dictionary representation of self. """
316328 json_dict = super ().to_dict ()
@@ -354,12 +366,17 @@ class Snapshot(Signed):
354366 }
355367
356368 """
357- def __init__ (self , meta : JsonDict = None , ** kwargs ) -> None :
358- # TODO: How much init magic do we want?
359- # TODO: Is there merit in creating classes for dict fields?
360- super ().__init__ (** kwargs )
369+ def __init__ (
370+ self , _type : str , version : int , spec_version : str ,
371+ expires : datetime , meta : JsonDict ) -> None :
372+ super ().__init__ (_type , version , spec_version , expires )
373+ # TODO: Add class for meta
361374 self .meta = meta
362375
376+ @classmethod
377+ def from_dict (cls , snapshot_dict ):
378+ return super ().from_dict (snapshot_dict )
379+
363380 def to_dict (self ) -> JsonDict :
364381 """Returns the JSON-serializable dictionary representation of self. """
365382 json_dict = super ().to_dict ()
@@ -437,14 +454,18 @@ class Targets(Signed):
437454
438455 """
439456 def __init__ (
440- self , targets : JsonDict = None , delegations : JsonDict = None ,
441- ** kwargs ) -> None :
442- # TODO: How much init magic do we want?
443- # TODO: Is there merit in creating classes for dict fields?
444- super (). __init__ ( ** kwargs )
457+ self , _type : str , version : int , spec_version : str ,
458+ expires : datetime , targets : JsonDict , delegations : JsonDict
459+ ) -> None :
460+ super (). __init__ ( _type , version , spec_version , expires )
461+ # TODO: Add class for meta
445462 self .targets = targets
446463 self .delegations = delegations
447464
465+ @classmethod
466+ def from_dict (cls , targets_dict ):
467+ return super ().from_dict (targets_dict )
468+
448469 def to_dict (self ) -> JsonDict :
449470 """Returns the JSON-serializable dictionary representation of self. """
450471 json_dict = super ().to_dict ()
0 commit comments