mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-27 00:19:36 -05:00
a104936bc6
* [wip] validation framework Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * [wip] wip Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * working for shallow structs!!! Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix needle pointer deref Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add comment Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix nested structs Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix nested struct pointers Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add tests Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix slices / arrays Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix struct parsing Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * extend tests Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * expose API Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * extend in-package documentation Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * linter fixes Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * fix naming Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * add missing license headers Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com> * align with review Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> --------- Signed-off-by: Moritz Sanft <58110325+msanft@users.noreply.github.com> Co-authored-by: Daniel Weiße <66256922+daniel-weisse@users.noreply.github.com>
270 lines
7.6 KiB
Go
270 lines
7.6 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
package validation
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
// Error is returned when a document is not valid.
|
|
type Error struct {
|
|
Path string
|
|
Err error
|
|
}
|
|
|
|
/*
|
|
newError creates a new validation Error.
|
|
|
|
To find the path to the exported field that failed validation, it traverses "doc"
|
|
recursively until it finds a field in "doc" that matches the reference to "field".
|
|
*/
|
|
func newError(doc, field referenceableValue, errMsg error) *Error {
|
|
// traverse the top level struct (i.e. the "haystack") until addr (i.e. the "needle") is found
|
|
path, err := traverse(doc, field, newPathBuilder(doc._type.Name()))
|
|
if err != nil {
|
|
return &Error{
|
|
Path: "unknown",
|
|
Err: fmt.Errorf("cannot find path to field: %w. original error: %w", err, errMsg),
|
|
}
|
|
}
|
|
|
|
return &Error{
|
|
Path: path,
|
|
Err: errMsg,
|
|
}
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e *Error) Error() string {
|
|
return fmt.Sprintf("validating %s: %s", e.Path, e.Err)
|
|
}
|
|
|
|
// Unwrap implements the error interface.
|
|
func (e *Error) Unwrap() error {
|
|
return e.Err
|
|
}
|
|
|
|
/*
|
|
traverse "haystack" recursively until it finds a field that matches
|
|
the reference saved in "needle", while building a pseudo-JSONPath to the field.
|
|
|
|
If it traverses a level down, it appends the name of the struct tag
|
|
or another entity like array index or map field to path.
|
|
|
|
When a field matches the reference to the given field, it returns the
|
|
path to the field.
|
|
*/
|
|
func traverse(haystack referenceableValue, needle referenceableValue, path pathBuilder) (string, error) {
|
|
// recursion anchor: doc is the field we are looking for.
|
|
// Join the path and return.
|
|
if foundNeedle(haystack, needle) {
|
|
return path.string(), nil
|
|
}
|
|
|
|
kind := haystack._type.Kind()
|
|
switch kind {
|
|
case reflect.Struct:
|
|
// Traverse all visible struct fields.
|
|
for _, field := range reflect.VisibleFields(haystack._type) {
|
|
// skip unexported fields
|
|
if !field.IsExported() {
|
|
continue
|
|
}
|
|
|
|
fieldVal := recPointerDeref(haystack.value.FieldByName(field.Name))
|
|
if isNilPtrOrInvalid(fieldVal) {
|
|
continue
|
|
}
|
|
|
|
fieldAddr := haystack.addr + field.Offset
|
|
newHaystack := referenceableValue{
|
|
value: fieldVal,
|
|
addr: fieldVal.UnsafeAddr(),
|
|
_type: fieldVal.Type(),
|
|
}
|
|
if canTraverse(fieldVal) {
|
|
// When a field is not the needle and cannot be traversed further,
|
|
// a errCannotTraverse is returned. Therefore, we only want to handle
|
|
// the case where the field is the needle.
|
|
if path, err := traverse(newHaystack, needle, path.appendStructField(field)); err == nil {
|
|
return path, nil
|
|
}
|
|
}
|
|
if foundNeedle(referenceableValue{addr: fieldAddr, _type: field.Type}, needle) {
|
|
return path.appendStructField(field).string(), nil
|
|
}
|
|
}
|
|
case reflect.Slice, reflect.Array:
|
|
// Traverse slice / Array elements
|
|
for i := 0; i < haystack.value.Len(); i++ {
|
|
// see struct case
|
|
itemVal := recPointerDeref(haystack.value.Index(i))
|
|
if isNilPtrOrInvalid(itemVal) {
|
|
continue
|
|
}
|
|
newHaystack := referenceableValue{
|
|
value: itemVal,
|
|
addr: itemVal.UnsafeAddr(),
|
|
_type: itemVal.Type(),
|
|
}
|
|
if canTraverse(itemVal) {
|
|
if path, err := traverse(newHaystack, needle, path.appendArrayIndex(i)); err == nil {
|
|
return path, nil
|
|
}
|
|
}
|
|
if foundNeedle(newHaystack, needle) {
|
|
return path.appendArrayIndex(i).string(), nil
|
|
}
|
|
}
|
|
case reflect.Map:
|
|
// Traverse map elements
|
|
iter := haystack.value.MapRange()
|
|
for iter.Next() {
|
|
// see struct case
|
|
mapKey := iter.Key().String()
|
|
mapVal := recPointerDeref(iter.Value())
|
|
if isNilPtrOrInvalid(mapVal) {
|
|
continue
|
|
}
|
|
if canTraverse(mapVal) {
|
|
newHaystack := referenceableValue{
|
|
value: mapVal,
|
|
addr: mapVal.UnsafeAddr(),
|
|
_type: mapVal.Type(),
|
|
mapKey: mapKey,
|
|
}
|
|
if path, err := traverse(newHaystack, needle, path.appendMapKey(mapKey)); err == nil {
|
|
return path, nil
|
|
}
|
|
}
|
|
// check if reference to map is the needle and the map key matches
|
|
if foundNeedle(referenceableValue{addr: haystack.addr, _type: haystack._type, mapKey: mapKey}, needle) {
|
|
return path.appendMapKey(mapKey).string(), nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// Primitive type, but not the value we are looking for.
|
|
return "", errCannotTraverse
|
|
}
|
|
|
|
// referenceableValue is a type that can be passed as any (thus being copied) without losing the reference to the actual value.
|
|
type referenceableValue struct {
|
|
value reflect.Value
|
|
_type reflect.Type
|
|
mapKey string // special case for map values, which are not addressable
|
|
addr uintptr
|
|
}
|
|
|
|
// errCannotTraverse is returned when a field cannot be traversed further.
|
|
var errCannotTraverse = errors.New("cannot traverse anymore")
|
|
|
|
// recPointerDeref recursively dereferences pointers and unpacks interfaces until a non-pointer value is found.
|
|
func recPointerDeref(val reflect.Value) reflect.Value {
|
|
switch val.Kind() {
|
|
case reflect.Ptr, reflect.UnsafePointer, reflect.Interface:
|
|
return recPointerDeref(val.Elem())
|
|
}
|
|
return val
|
|
}
|
|
|
|
// pointerDeref dereferences pointers and unpacks interfaces.
|
|
// If the value is not a pointer, it is returned unchanged.
|
|
func pointerDeref(val reflect.Value) reflect.Value {
|
|
switch val.Kind() {
|
|
case reflect.Ptr, reflect.UnsafePointer, reflect.Interface:
|
|
return val.Elem()
|
|
}
|
|
return val
|
|
}
|
|
|
|
/*
|
|
canTraverse whether a value can be further traversed.
|
|
|
|
For pointer types, false is returned.
|
|
*/
|
|
func canTraverse(v reflect.Value) bool {
|
|
switch v.Kind() {
|
|
case reflect.Struct, reflect.Slice, reflect.Array, reflect.Map:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isNilPtrOrInvalid returns true if a value is a nil pointer or if the value is of an invalid kind.
|
|
func isNilPtrOrInvalid(v reflect.Value) bool {
|
|
switch v.Kind() {
|
|
case reflect.Ptr, reflect.UnsafePointer, reflect.Interface, reflect.Slice, reflect.Map:
|
|
return v.IsNil()
|
|
case reflect.Invalid:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/*
|
|
foundNeedle returns whether the given value is the needle.
|
|
|
|
It does so by comparing the address and type of the value to the address and type of the needle.
|
|
The comparison of types is necessary because the first value of a struct has the same address as the struct itself.
|
|
*/
|
|
func foundNeedle(haystack, needle referenceableValue) bool {
|
|
return haystack.addr == needle.addr &&
|
|
haystack._type == needle._type &&
|
|
haystack.mapKey == needle.mapKey
|
|
}
|
|
|
|
// pathBuilder is a helper to build a field path.
|
|
type pathBuilder struct {
|
|
buf []string // slice can be copied by value when its non-zero, contrary to a strings.Builder
|
|
}
|
|
|
|
// newPathBuilder creates a new pathBuilder from the identifier of a top level document.
|
|
func newPathBuilder(topLevelDoc string) pathBuilder {
|
|
return pathBuilder{
|
|
buf: []string{topLevelDoc},
|
|
}
|
|
}
|
|
|
|
// appendStructField appends the JSON / YAML struct tag of a field to the path.
|
|
// If no struct tag is present, the field name is used.
|
|
func (p pathBuilder) appendStructField(field reflect.StructField) pathBuilder {
|
|
switch {
|
|
case field.Tag.Get("json") != "":
|
|
p.buf = append(p.buf, fmt.Sprintf(".%s", field.Tag.Get("json")))
|
|
case field.Tag.Get("yaml") != "":
|
|
p.buf = append(p.buf, fmt.Sprintf(".%s", field.Tag.Get("yaml")))
|
|
default:
|
|
p.buf = append(p.buf, fmt.Sprintf(".%s", field.Name))
|
|
}
|
|
return p
|
|
}
|
|
|
|
// appendArrayIndex appends the index of an array to the path.
|
|
func (p pathBuilder) appendArrayIndex(i int) pathBuilder {
|
|
p.buf = append(p.buf, fmt.Sprintf("[%d]", i))
|
|
return p
|
|
}
|
|
|
|
// appendMapKey appends the key of a map to the path.
|
|
func (p pathBuilder) appendMapKey(k string) pathBuilder {
|
|
p.buf = append(p.buf, fmt.Sprintf("[\"%s\"]", k))
|
|
return p
|
|
}
|
|
|
|
// string returns the path.
|
|
func (p pathBuilder) string() string {
|
|
// Remove struct tag prefix
|
|
return strings.TrimPrefix(
|
|
strings.Join(p.buf, ""),
|
|
".",
|
|
)
|
|
}
|