Validate canonical comparison manifests
This commit is contained in:
@@ -239,6 +239,9 @@ func readBundleFile(directory, name string) ([]byte, error) {
|
||||
}
|
||||
|
||||
func decodeManifest(data []byte) (Manifest, error) {
|
||||
if err := validateManifestJSON(data); err != nil {
|
||||
return Manifest{}, unrecognizedBundleError("decode manifest", err)
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
var manifest Manifest
|
||||
@@ -255,6 +258,162 @@ func decodeManifest(data []byte) (Manifest, error) {
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
var manifestFields = map[string]jsonValueValidator{
|
||||
"schemaVersion": nil,
|
||||
"comparisonId": nil,
|
||||
"startedAt": nil,
|
||||
"finishedAt": nil,
|
||||
"reportId": nil,
|
||||
"validPeriod": validateValidPeriodJSON,
|
||||
"timezone": nil,
|
||||
"promptId": nil,
|
||||
"promptVersion": nil,
|
||||
"promptHash": nil,
|
||||
"dataPackage": validateDataPackageJSON,
|
||||
"total": nil,
|
||||
"succeeded": nil,
|
||||
"failed": nil,
|
||||
"results": validateResultsJSON,
|
||||
}
|
||||
|
||||
var validPeriodFields = map[string]jsonValueValidator{
|
||||
"start": nil,
|
||||
"end": nil,
|
||||
}
|
||||
|
||||
var dataPackageFields = map[string]jsonValueValidator{
|
||||
"path": nil,
|
||||
"sha256": nil,
|
||||
}
|
||||
|
||||
var resultFields = map[string]jsonValueValidator{
|
||||
"position": nil,
|
||||
"profileId": nil,
|
||||
"backendId": nil,
|
||||
"modelName": nil,
|
||||
"status": nil,
|
||||
"validationStatus": nil,
|
||||
"reportPath": nil,
|
||||
"error": validateSafeErrorJSON,
|
||||
}
|
||||
|
||||
var safeErrorFields = map[string]jsonValueValidator{
|
||||
"category": nil,
|
||||
"message": nil,
|
||||
}
|
||||
|
||||
type jsonValueValidator func(*json.Decoder) error
|
||||
|
||||
func validateManifestJSON(data []byte) error {
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
if err := validateJSONObject(decoder, manifestFields); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := decoder.Token(); err != io.EOF {
|
||||
if err == nil {
|
||||
return fmt.Errorf("manifest has multiple JSON values")
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateValidPeriodJSON(decoder *json.Decoder) error {
|
||||
return validateJSONObject(decoder, validPeriodFields)
|
||||
}
|
||||
|
||||
func validateDataPackageJSON(decoder *json.Decoder) error {
|
||||
return validateJSONObject(decoder, dataPackageFields)
|
||||
}
|
||||
|
||||
func validateResultsJSON(decoder *json.Decoder) error {
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if delimiter, ok := token.(json.Delim); !ok || delimiter != '[' {
|
||||
return fmt.Errorf("results must be an array")
|
||||
}
|
||||
for decoder.More() {
|
||||
if err := validateJSONObject(decoder, resultFields); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
token, err = decoder.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if delimiter, ok := token.(json.Delim); !ok || delimiter != ']' {
|
||||
return fmt.Errorf("results has an invalid array terminator")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSafeErrorJSON(decoder *json.Decoder) error {
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if token == nil {
|
||||
return nil
|
||||
}
|
||||
if delimiter, ok := token.(json.Delim); !ok || delimiter != '{' {
|
||||
return fmt.Errorf("error must be an object")
|
||||
}
|
||||
return validateJSONObjectBody(decoder, safeErrorFields)
|
||||
}
|
||||
|
||||
func validateJSONObject(decoder *json.Decoder, fields map[string]jsonValueValidator) error {
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if delimiter, ok := token.(json.Delim); !ok || delimiter != '{' {
|
||||
return fmt.Errorf("manifest value must be an object")
|
||||
}
|
||||
return validateJSONObjectBody(decoder, fields)
|
||||
}
|
||||
|
||||
func validateJSONObjectBody(decoder *json.Decoder, fields map[string]jsonValueValidator) error {
|
||||
seen := make(map[string]struct{}, len(fields))
|
||||
for decoder.More() {
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, ok := token.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("manifest field name is invalid")
|
||||
}
|
||||
validator, known := fields[name]
|
||||
if !known {
|
||||
return fmt.Errorf("manifest field %q is not canonical", name)
|
||||
}
|
||||
if _, duplicate := seen[name]; duplicate {
|
||||
return fmt.Errorf("manifest field %q is duplicated", name)
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
if validator == nil {
|
||||
var value json.RawMessage
|
||||
if err := decoder.Decode(&value); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := validator(decoder); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
token, err := decoder.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if delimiter, ok := token.(json.Delim); !ok || delimiter != '}' {
|
||||
return fmt.Errorf("manifest object has an invalid terminator")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unrecognizedBundleError(action string, err error) error {
|
||||
if err == nil {
|
||||
return fmt.Errorf("%w: %s", ErrUnrecognizedBundle, action)
|
||||
|
||||
Reference in New Issue
Block a user