@@ -69,9 +69,7 @@ class Metadata():
69
69
]
70
70
71
71
"""
72
- def __init__ (
73
- self , signed : 'Signed' = None , signatures : list = None ) -> None :
74
- # TODO: How much init magic do we want?
72
+ def __init__ (self , signed : 'Signed' , signatures : list ) -> None :
75
73
self .signed = signed
76
74
self .signatures = signatures
77
75
@@ -167,18 +165,37 @@ def from_json_file(
167
165
168
166
Raises:
169
167
securesystemslib.exceptions.StorageError: The file cannot be read.
170
- securesystemslib.exceptions.Error, ValueError: The metadata cannot
171
- be parsed.
168
+ securesystemslib.exceptions.Error, ValueError, KeyError : The
169
+ metadata cannot be parsed.
172
170
173
171
Returns:
174
172
A TUF Metadata object.
175
173
176
174
"""
177
- signable = load_json_file (filename , storage_backend )
175
+ return cls .from_dict (load_json_file (filename , storage_backend ))
176
+
177
+ @classmethod
178
+ def from_dict (cls , metadata : JsonDict ) -> 'Metadata' :
179
+ """Creates Metadata object from its JSON/dict representation.
180
+
181
+ Calls 'from_dict' for any complex metadata attribute represented by a
182
+ class also that has a 'from_dict' factory method. (Currently this is
183
+ only the signed attribute.)
184
+
185
+ Arguments:
186
+ metadata: TUF metadata in JSON/dict representation, as e.g.
187
+ returned by 'json.loads'.
188
+
189
+ Raises:
190
+ KeyError: The metadata dict format is invalid.
191
+ ValueError: The metadata has an unrecognized signed._type field.
178
192
179
- # TODO: Should we use constants?
180
- # And/or maybe a dispatch table? (<-- maybe too much magic)
181
- _type = signable ['signed' ]['_type' ]
193
+ Returns:
194
+ A TUF Metadata object.
195
+
196
+ """
197
+ # Dispatch to contained metadata class on metadata _type field.
198
+ _type = metadata ['signed' ]['_type' ]
182
199
183
200
if _type == 'targets' :
184
201
inner_cls = Targets
@@ -192,9 +209,13 @@ def from_json_file(
192
209
else :
193
210
raise ValueError (f'unrecognized metadata type "{ _type } "' )
194
211
195
- return Metadata (
196
- signed = inner_cls (** signable ['signed' ]),
197
- signatures = signable ['signatures' ])
212
+ # NOTE: If Signature becomes a class, we should iterate over
213
+ # metadata['signatures'], call Signature.from_dict for each item, and
214
+ # pass a list of Signature objects to the Metadata constructor intead.
215
+ return cls (
216
+ signed = inner_cls .from_dict (metadata ['signed' ]),
217
+ signatures = metadata ['signatures' ])
218
+
198
219
199
220
def to_json_file (
200
221
self , filename : str , compact : bool = False ,
@@ -236,41 +257,48 @@ class Signed:
236
257
# we keep it to match spec terminology (I often refer to this as "payload",
237
258
# or "inner metadata")
238
259
239
- # TODO: Re-think default values. It might be better to pass some things
240
- # as args and not es kwargs. Then we'd need to pop those from
241
- # signable["signed"] in read_from_json and pass them explicitly, which
242
- # some say is better than implicit. :)
243
260
def __init__ (
244
- self , _type : str = None , version : int = 0 ,
245
- spec_version : str = None , expires : datetime = None
246
- ) -> None :
247
- # TODO: How much init magic do we want?
261
+ self , _type : str , version : int , spec_version : str ,
262
+ expires : datetime ) -> None :
248
263
249
264
self ._type = _type
265
+ self .version = version
250
266
self .spec_version = spec_version
267
+ self .expires = expires
251
268
252
- # We always intend times to be UTC
253
- # NOTE: we could do this with datetime.fromisoformat() but that is not
254
- # available in Python 2.7's datetime
255
- # NOTE: Store as datetime object for convenient handling, use 'expires'
256
- # property to get the TUF metadata format representation
257
- self .__expiration = iso8601 .parse_date (expires ).replace (tzinfo = None )
258
-
269
+ # TODO: Should we separate data validation from constructor?
259
270
if version < 0 :
260
271
raise ValueError (f'version must be < 0, got { version } ' )
261
272
self .version = version
262
273
274
+ @classmethod
275
+ def from_dict (cls , signed_dict ) -> 'Signed' :
276
+ """Creates Signed object from its JSON/dict representation. """
277
+
278
+ # Convert 'expires' TUF metadata string to a datetime object, which is
279
+ # what the constructor expects and what we store. The inverse operation
280
+ # is implemented in 'to_dict'.
281
+ signed_dict ['expires' ] = iso8601 .parse_date (
282
+ signed_dict ['expires' ]).replace (tzinfo = None )
283
+ # NOTE: We write the converted 'expires' back into 'signed_dict' above
284
+ # so that we can pass it to the constructor as '**signed_dict' below,
285
+ # along with other fields that belong to Signed subclasses.
286
+ # Any 'from_dict'(-like) conversions of fields that correspond to a
287
+ # subclass should be performed in the 'from_dict' method of that
288
+ # subclass and also be written back into 'signed_dict' before calling
289
+ # super().from_dict.
290
+
291
+ # NOTE: cls might be a subclass of Signed, if 'from_dict' was called on
292
+ # that subclass (see e.g. Metadata.from_dict).
293
+ return cls (** signed_dict )
263
294
264
- @property
265
- def expires (self ) -> str :
266
- return self .__expiration .isoformat () + 'Z'
267
295
def to_canonical_bytes (self ) -> bytes :
268
296
"""Returns the UTF-8 encoded canonical JSON representation of self. """
269
297
return encode_canonical (self .to_dict ()).encode ('UTF-8' )
270
298
271
299
def bump_expiration (self , delta : timedelta = timedelta (days = 1 )) -> None :
272
300
"""Increments the expires attribute by the passed timedelta. """
273
- self .__expiration = self . __expiration + delta
301
+ self .expires += delta
274
302
275
303
def bump_version (self ) -> None :
276
304
"""Increments the metadata version number by 1."""
@@ -282,7 +310,7 @@ def to_dict(self) -> JsonDict:
282
310
'_type' : self ._type ,
283
311
'version' : self .version ,
284
312
'spec_version' : self .spec_version ,
285
- 'expires' : self .expires
313
+ 'expires' : self .expires . isoformat () + 'Z'
286
314
}
287
315
288
316
class Timestamp (Signed ):
@@ -304,10 +332,11 @@ class Timestamp(Signed):
304
332
}
305
333
306
334
"""
307
- def __init__ (self , meta : JsonDict = None , ** kwargs ) -> None :
308
- super ().__init__ (** kwargs )
309
- # TODO: How much init magic do we want?
310
- # TODO: Is there merit in creating classes for dict fields?
335
+ def __init__ (
336
+ self , _type : str , version : int , spec_version : str ,
337
+ expires : datetime , meta : JsonDict ) -> None :
338
+ super ().__init__ (_type , version , spec_version , expires )
339
+ # TODO: Add class for meta
311
340
self .meta = meta
312
341
313
342
def to_dict (self ) -> JsonDict :
@@ -353,10 +382,11 @@ class Snapshot(Signed):
353
382
}
354
383
355
384
"""
356
- def __init__ (self , meta : JsonDict = None , ** kwargs ) -> None :
357
- # TODO: How much init magic do we want?
358
- # TODO: Is there merit in creating classes for dict fields?
359
- super ().__init__ (** kwargs )
385
+ def __init__ (
386
+ self , _type : str , version : int , spec_version : str ,
387
+ expires : datetime , meta : JsonDict ) -> None :
388
+ super ().__init__ (_type , version , spec_version , expires )
389
+ # TODO: Add class for meta
360
390
self .meta = meta
361
391
362
392
def to_dict (self ) -> JsonDict :
@@ -436,14 +466,15 @@ class Targets(Signed):
436
466
437
467
"""
438
468
def __init__ (
439
- self , targets : JsonDict = None , delegations : JsonDict = None ,
440
- ** kwargs ) -> None :
441
- # TODO: How much init magic do we want?
442
- # TODO: Is there merit in creating classes for dict fields?
443
- super (). __init__ ( ** kwargs )
469
+ self , _type : str , version : int , spec_version : str ,
470
+ expires : datetime , targets : JsonDict , delegations : JsonDict
471
+ ) -> None :
472
+ super (). __init__ ( _type , version , spec_version , expires )
473
+ # TODO: Add class for meta
444
474
self .targets = targets
445
475
self .delegations = delegations
446
476
477
+
447
478
def to_dict (self ) -> JsonDict :
448
479
"""Returns the JSON-serializable dictionary representation of self. """
449
480
json_dict = super ().to_dict ()
0 commit comments