diff --git a/data.proto b/data.proto index 650f01c..737eb3d 100644 --- a/data.proto +++ b/data.proto @@ -20,7 +20,7 @@ import "schema.proto"; package data; -option go_package = "github.com/sdcio/sdc-protos/sdcpb;schema_server"; +option go_package = "github.com/sdcio/sdc-protos/sdcpb;sdcpb"; service DataServer { // datastore diff --git a/go.mod b/go.mod index 2877ebe..444e258 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,16 @@ module github.com/sdcio/sdc-protos -go 1.23.9 +go 1.24.0 require ( + github.com/sdcio/logger v0.0.3 github.com/google/go-cmp v0.7.0 google.golang.org/grpc v1.75.1 google.golang.org/protobuf v1.36.9 ) require ( + github.com/go-logr/logr v1.4.3 // indirect golang.org/x/net v0.41.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.26.0 // indirect diff --git a/go.sum b/go.sum index 5a88dfa..0378756 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/sdcio/logger v0.0.3 h1:IFUbObObGry+S8lHGwOQKKRxJSuOphgRU/hxVhOdMOM= +github.com/sdcio/logger v0.0.3/go.mod h1:yWaOxK/G6vszjg8tKZiMqiEjlZouHsjFME4zSk+SAEA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= diff --git a/schema.proto b/schema.proto index 18bfc82..bce18aa 100644 --- a/schema.proto +++ b/schema.proto @@ -16,7 +16,7 @@ syntax = "proto3"; package schema; -option go_package = "github.com/sdcio/sdc-protos/sdcpb;schema_server"; +option go_package = "github.com/sdcio/sdc-protos/sdcpb;sdcpb"; service SchemaServer { // returns schema name, vendor, version, and files path(s) diff --git a/sdcpb/blame_tree_element_additions.go b/sdcpb/blame_tree_element_additions.go index ac4ba1d..4f456d9 100644 --- a/sdcpb/blame_tree_element_additions.go +++ b/sdcpb/blame_tree_element_additions.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb import ( "fmt" diff --git a/sdcpb/choice_info_additions.go b/sdcpb/choice_info_additions.go index 8f328fb..0bccf7b 100644 --- a/sdcpb/choice_info_additions.go +++ b/sdcpb/choice_info_additions.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb func (c *ChoiceInfo) GetChoiceByName(choiceName string) *ChoiceInfoChoice { return c.Choice[choiceName] diff --git a/sdcpb/choice_info_choice_additions.go b/sdcpb/choice_info_choice_additions.go index 9ec629d..a6a288e 100644 --- a/sdcpb/choice_info_choice_additions.go +++ b/sdcpb/choice_info_choice_additions.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb func (c *ChoiceInfoChoice) GetAllAttributes() []string { result := make([]string, 0, len(c.Case)) diff --git a/sdcpb/container_schema_additions.go b/sdcpb/container_schema_additions.go index 3b6cb68..67cf7af 100644 --- a/sdcpb/container_schema_additions.go +++ b/sdcpb/container_schema_additions.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb func (x *ContainerSchema) GetMandatoryChildrenConfig() []*MandatoryChild { var result []*MandatoryChild diff --git a/sdcpb/data.pb.go b/sdcpb/data.pb.go index 82d63f1..a5c06d7 100644 --- a/sdcpb/data.pb.go +++ b/sdcpb/data.pb.go @@ -18,15 +18,16 @@ // protoc v3.21.12 // source: data.proto -package schema_server +package sdcpb import ( + reflect "reflect" + sync "sync" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" anypb "google.golang.org/protobuf/types/known/anypb" emptypb "google.golang.org/protobuf/types/known/emptypb" - reflect "reflect" - sync "sync" ) const ( diff --git a/sdcpb/data_grpc.pb.go b/sdcpb/data_grpc.pb.go index 0b2a0a6..f5f1a37 100644 --- a/sdcpb/data_grpc.pb.go +++ b/sdcpb/data_grpc.pb.go @@ -18,10 +18,11 @@ // - protoc v3.21.12 // source: data.proto -package schema_server +package sdcpb import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/sdcpb/identityref_additions.go b/sdcpb/identityref_additions.go index 8ec95aa..ba5f784 100644 --- a/sdcpb/identityref_additions.go +++ b/sdcpb/identityref_additions.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb import "fmt" diff --git a/sdcpb/path_additions.go b/sdcpb/path_additions.go index 456fdbe..3751bb9 100644 --- a/sdcpb/path_additions.go +++ b/sdcpb/path_additions.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb import ( "errors" diff --git a/sdcpb/path_additions_test.go b/sdcpb/path_additions_test.go index c21e885..8267ebd 100644 --- a/sdcpb/path_additions_test.go +++ b/sdcpb/path_additions_test.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb import ( "testing" diff --git a/sdcpb/path_elem.go b/sdcpb/path_elem.go index 7ed7982..3885d4e 100644 --- a/sdcpb/path_elem.go +++ b/sdcpb/path_elem.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb import ( "iter" diff --git a/sdcpb/path_set.go b/sdcpb/path_set.go index e399d9d..67be91c 100644 --- a/sdcpb/path_set.go +++ b/sdcpb/path_set.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb import "iter" diff --git a/sdcpb/path_test.go b/sdcpb/path_test.go index 9a7e3d9..75a09fe 100644 --- a/sdcpb/path_test.go +++ b/sdcpb/path_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package schema_server +package sdcpb import ( "testing" diff --git a/sdcpb/paths.go b/sdcpb/paths.go index 23e3066..6fb1622 100644 --- a/sdcpb/paths.go +++ b/sdcpb/paths.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb type Paths []*Path diff --git a/sdcpb/schema.pb.go b/sdcpb/schema.pb.go index 99da6e5..b65d631 100644 --- a/sdcpb/schema.pb.go +++ b/sdcpb/schema.pb.go @@ -18,13 +18,14 @@ // protoc v3.21.12 // source: schema.proto -package schema_server +package sdcpb import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/sdcpb/schema_elem_additions.go b/sdcpb/schema_elem_additions.go index 70ab73e..b47299b 100644 --- a/sdcpb/schema_elem_additions.go +++ b/sdcpb/schema_elem_additions.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb func (s *SchemaElem) IsState() bool { switch x := s.Schema.(type) { diff --git a/sdcpb/schema_grpc.pb.go b/sdcpb/schema_grpc.pb.go index e51420e..4d881b9 100644 --- a/sdcpb/schema_grpc.pb.go +++ b/sdcpb/schema_grpc.pb.go @@ -18,10 +18,11 @@ // - protoc v3.21.12 // source: schema.proto -package schema_server +package sdcpb import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/sdcpb/transaction_set_response_intent_additions.go b/sdcpb/transaction_set_response_intent_additions.go index c93fd5c..9ca7e3c 100644 --- a/sdcpb/transaction_set_response_intent_additions.go +++ b/sdcpb/transaction_set_response_intent_additions.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb func (t *TransactionSetResponseIntent) Failed() bool { return len(t.Errors) > 0 diff --git a/sdcpb/transactionsetresponse_additions.go b/sdcpb/transactionsetresponse_additions.go index 3f169db..9691f31 100644 --- a/sdcpb/transactionsetresponse_additions.go +++ b/sdcpb/transactionsetresponse_additions.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb import "fmt" diff --git a/sdcpb/typed_value_additions.go b/sdcpb/typed_value_additions.go index 8a2f024..9e47c82 100644 --- a/sdcpb/typed_value_additions.go +++ b/sdcpb/typed_value_additions.go @@ -1,4 +1,4 @@ -package schema_server +package sdcpb import ( "bytes" @@ -117,9 +117,6 @@ func toStringSorted(tvs []*TypedValue) []string { // ToString converts the TypedValue to the real, non proto string func (tv *TypedValue) ToString() string { - if tv == nil { - return "" - } switch tv.Value.(type) { case *TypedValue_AnyVal: return string(tv.GetAnyVal().GetValue()) // questionable... diff --git a/sdcpb/typed_value_conversions.go b/sdcpb/typed_value_conversions.go new file mode 100644 index 0000000..0a0e508 --- /dev/null +++ b/sdcpb/typed_value_conversions.go @@ -0,0 +1,511 @@ +package sdcpb + +import ( + "fmt" + "math" + "regexp" + "strconv" + "strings" + + logf "github.com/sdcio/logger" + "github.com/sdcio/sdc-protos/utils" +) + +func SchemaElemToTV(schemaObject *SchemaElem, v string, ts uint64) (*TypedValue, error) { + var schemaType *SchemaLeafType + switch { + case schemaObject.GetField() != nil: + schemaType = schemaObject.GetField().GetType() + case schemaObject.GetLeaflist() != nil: + schemaType = schemaObject.GetLeaflist().GetType() + case schemaObject.GetContainer() != nil: + if !schemaObject.GetContainer().IsPresence { + return nil, fmt.Errorf("non presence container update") + } + return nil, nil + } + return TVFromString(schemaType, v, ts) +} + +func TVFromString(schemaType *SchemaLeafType, v string, ts uint64) (*TypedValue, error) { + if schemaType == nil { + return nil, fmt.Errorf("schemaType cannot be nil") + } + + var tv *TypedValue + var err error + switch schemaType.Type { + case "string": + tv, err = ConvertString(v, schemaType) + case "union": + tv, err = ConvertUnion(v, schemaType.UnionTypes) + case "boolean": + tv, err = ConvertBoolean(v, schemaType) + case "int8": + // TODO: HEX and OCTAL pre-processing for all INT types + // https://www.rfc-editor.org/rfc/rfc6020.html#page-112 + tv, err = ConvertInt8(v, schemaType) + case "int16": + tv, err = ConvertInt16(v, schemaType) + case "int32": + tv, err = ConvertInt32(v, schemaType) + case "int64": + tv, err = ConvertInt64(v, schemaType) + case "uint8": + tv, err = ConvertUint8(v, schemaType) + case "uint16": + tv, err = ConvertUint16(v, schemaType) + case "uint32": + tv, err = ConvertUint32(v, schemaType) + case "uint64": + tv, err = ConvertUint64(v, schemaType) + case "enumeration": + tv, err = ConvertEnumeration(v, schemaType) + case "empty": + tv, err = &TypedValue{Value: &TypedValue_EmptyVal{}}, nil + case "bits": + tv, err = ConvertBits(v, schemaType) + case "binary": // https://www.rfc-editor.org/rfc/rfc6020.html#section-9.8 + tv, err = ConvertBinary(v, schemaType) + case "leafref": // https://www.rfc-editor.org/rfc/rfc6020.html#section-9.9 + tv, err = ConvertLeafRef(v, schemaType) + case "identityref": //TODO: https://www.rfc-editor.org/rfc/rfc6020.html#section-9.10 + tv, err = ConvertIdentityRef(v, schemaType) + case "instance-identifier": //TODO: https://www.rfc-editor.org/rfc/rfc6020.html#section-9.13 + tv, err = ConvertInstanceIdentifier(v, schemaType) + case "decimal64": + // TODO: is the following TODO still valid? I think not + // TODO: fraction-digits (https://www.rfc-editor.org/rfc/rfc6020.html#section-9.3.4) + tv, err = ConvertDecimal64(v, schemaType) + default: + tv, err = nil, fmt.Errorf("FromString conversion not implemented for type '%s'", schemaType.Type) + } + + if err != nil { + return nil, err + } + // Set timestamp + tv.Timestamp = ts + return tv, nil +} + +func ConvertInstanceIdentifier(value string, slt *SchemaLeafType) (*TypedValue, error) { + // delegate to string, validation is left for a different party at a later stage in processing + return ConvertString(value, slt) +} + +func ConvertIdentityRef(value string, schemaType *SchemaLeafType) (*TypedValue, error) { + before, name, found := strings.Cut(value, ":") + if !found { + name = before + } + prefix, ok := schemaType.IdentityPrefixesMap[name] + if !ok { + identities := make([]string, 0, len(schemaType.IdentityPrefixesMap)) + for k := range schemaType.IdentityPrefixesMap { + identities = append(identities, k) + } + return nil, fmt.Errorf("identity %s not found, possible values are %s", value, strings.Join(identities, ", ")) + } + module, ok := schemaType.ModulePrefixMap[name] + if !ok { + identities := make([]string, 0, len(schemaType.IdentityPrefixesMap)) + for k := range schemaType.IdentityPrefixesMap { + identities = append(identities, k) + } + return nil, fmt.Errorf("identity %s not found, possible values are %s", value, strings.Join(identities, ", ")) + } + return &TypedValue{ + Value: &TypedValue_IdentityrefVal{IdentityrefVal: &IdentityRef{Value: name, Prefix: prefix, Module: module}}, + }, nil +} + +func ConvertBinary(value string, slt *SchemaLeafType) (*TypedValue, error) { + // Binary is basically a base64 encoded string that might carry a length restriction + // so we should be fine with delegating to string + return ConvertString(value, slt) +} + +func ConvertLeafRef(value string, slt *SchemaLeafType) (*TypedValue, error) { + // Try to convert based on the target type info + return TVFromString(slt.LeafrefTargetType, value, 0) +} + +func ConvertEnumeration(value string, slt *SchemaLeafType) (*TypedValue, error) { + // iterate the valid values as per schema + for _, item := range slt.EnumNames { + // if value is found, return a StringVal + if value == item { + return &TypedValue{ + Value: &TypedValue_StringVal{ + StringVal: value, + }, + }, nil + } + } + // If value is not found return an error + return nil, fmt.Errorf("value %q does not match any valid enum values [%s]", value, strings.Join(slt.EnumNames, ", ")) +} + +func ConvertBoolean(value string, _ *SchemaLeafType) (*TypedValue, error) { + bval, err := strconv.ParseBool(value) + if err != nil { + // if it is any other value, return error + return nil, err + } + // otherwise return the BoolVal TypedValue + return &TypedValue{ + Value: &TypedValue_BoolVal{ + BoolVal: bval, + }, + }, nil +} + +func ConvertSdcpbNumberToUint64(mm *Number) (uint64, error) { + if mm.Negative { + return 0, fmt.Errorf("negative number to uint conversion") + } + return mm.Value, nil +} + +func intAbs(x int64) uint64 { + ui := uint64(x) + if x < 0 { + return ^(ui) + 1 + } + return ui +} + +func ConvertSdcpbNumberToInt64(mm *Number) (int64, error) { + if mm.Negative { + if mm.Value > intAbs(math.MinInt64) { + return 0, fmt.Errorf("error converting -%d to int64: overflow", mm.Value) + } + return -int64(mm.Value), nil + } + + if mm.Value > math.MaxInt64 { + return 0, fmt.Errorf("error converting %d to int64 overflow", mm.Value) + } + return int64(mm.Value), nil +} + +func convertUint(value string, minMaxs []*SchemaMinMaxType, ranges *utils.Rnges[uint64]) (*TypedValue, error) { + if ranges == nil { + ranges = utils.NewRnges[uint64]() + } + for _, x := range minMaxs { + min, err := ConvertSdcpbNumberToUint64(x.Min) + if err != nil { + return nil, err + } + max, err := ConvertSdcpbNumberToUint64(x.Max) + if err != nil { + return nil, err + } + ranges.AddRange(min, max) + } + + uValue, err := strconv.ParseUint(value, 10, 64) + if err != nil { + return nil, err + } + // validate the value against the ranges + valid := ranges.IsWithinAnyRange(uValue) + if !valid { + return nil, fmt.Errorf("%q not within ranges: %s", value, ranges.String()) + } + // return the TypedValue + return &TypedValue{Value: &TypedValue_UintVal{UintVal: uValue}}, nil +} + +func ConvertUint8(value string, lst *SchemaLeafType) (*TypedValue, error) { + // create the ranges + ranges := utils.NewRnges[uint64]() + ranges.AddRange(0, math.MaxUint8) + + return convertUint(value, lst.Range, ranges) +} + +func ConvertUint16(value string, lst *SchemaLeafType) (*TypedValue, error) { + // create the ranges + ranges := utils.NewRnges[uint64]() + ranges.AddRange(0, math.MaxUint16) + + return convertUint(value, lst.Range, ranges) +} + +func ConvertUint32(value string, lst *SchemaLeafType) (*TypedValue, error) { + // create the ranges + ranges := utils.NewRnges[uint64]() + ranges.AddRange(0, math.MaxUint32) + + return convertUint(value, lst.Range, ranges) +} + +func ConvertUint64(value string, lst *SchemaLeafType) (*TypedValue, error) { + // create the ranges + ranges := utils.NewRnges[uint64]() + + return convertUint(value, lst.Range, ranges) +} + +func convertInt(value string, minMaxs []*SchemaMinMaxType, ranges *utils.Rnges[int64]) (*TypedValue, error) { + for _, x := range minMaxs { + min, err := ConvertSdcpbNumberToInt64(x.Min) + if err != nil { + return nil, err + } + max, err := ConvertSdcpbNumberToInt64(x.Max) + if err != nil { + return nil, err + } + ranges.AddRange(min, max) + } + + // validate the value against the ranges + iValue, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil, err + } + // validate the value against the ranges + valid := ranges.IsWithinAnyRange(iValue) + if !valid { + return nil, fmt.Errorf("%q not within ranges: %s", value, ranges.String()) + } + // return the TypedValue + return &TypedValue{Value: &TypedValue_IntVal{IntVal: iValue}}, nil +} + +func ConvertInt8(value string, lst *SchemaLeafType) (*TypedValue, error) { + // create the ranges + ranges := utils.NewRnges[int64]() + ranges.AddRange(math.MinInt8, math.MaxInt8) + + return convertInt(value, lst.Range, ranges) +} + +func ConvertInt16(value string, lst *SchemaLeafType) (*TypedValue, error) { + // create the ranges + ranges := utils.NewRnges[int64]() + ranges.AddRange(math.MinInt16, math.MaxInt16) + + return convertInt(value, lst.Range, ranges) +} + +func ConvertInt32(value string, lst *SchemaLeafType) (*TypedValue, error) { + // create the ranges + ranges := utils.NewRnges[int64]() + ranges.AddRange(math.MinInt32, math.MaxInt32) + + return convertInt(value, lst.Range, ranges) +} +func ConvertInt64(value string, lst *SchemaLeafType) (*TypedValue, error) { + // create the ranges + ranges := utils.NewRnges[int64]() + + return convertInt(value, lst.Range, ranges) +} + +func xmlRegexConvert(s string) string { + + cTest := func(r rune, prev rune) bool { + // if ^ is not following a [ or if $ we want to return true + return (r == '^' && prev != '[') || r == '$' + } + + b := strings.Builder{} + b.Grow(len(s) + len(s)/4) + slashes := 0 + prevR := rune(0) + + for _, r := range s { + if r == '\\' { + slashes++ + prevR = r + b.WriteRune(r) + continue + } + + if cTest(r, prevR) && slashes%2 == 0 { + b.WriteRune('\\') + } + + slashes = 0 + prevR = r + b.WriteRune(r) + } + return b.String() +} + +func ConvertString(value string, lst *SchemaLeafType) (*TypedValue, error) { + // check length of the string if the length property is set + // length will contain a range like string definition "5..60" or "7..10|40..45" + if len(lst.Length) != 0 { + _, err := convertUint(strconv.Itoa(len(value)), lst.Length, nil) + + if err != nil { + return nil, err + } + + } + + overallMatch := true + // If the type has multiple "pattern" statements, the expressions are + // ANDed together, i.e., all such expressions have to match. + for _, sp := range lst.Patterns { + // The set of metacharacters is not the same between XML schema and perl/python/go REs + // the set of metacharacters for XML is: .\?*+{}()[] (https://www.w3.org/TR/xmlschema-2/#dt-metac) + // the set of metacharacters defined in go is: \.+*?()|[]{}^$ (go/libexec/src/regexp/regexp.go:714) + // we need therefore to escape some values + // TODO check about '^' + + escaped := xmlRegexConvert(sp.Pattern) + re, err := regexp.Compile(escaped) + if err != nil { + //TODO: Do we want to stop here? + logf.DefaultLogger.Error(err, "unable to compile regex", "pattern", sp.Pattern) + return nil, fmt.Errorf("unable to compile regex: %w", err) + } + match := re.MatchString(value) + // if it is a match and not inverted + // or it is not a match but inverted + // then this is valid + if (match && !sp.Inverted) || (!match && sp.Inverted) { + continue + } else { + overallMatch = false + break + } + } + if overallMatch { + return &TypedValue{ + Value: &TypedValue_StringVal{ + StringVal: value, + }, + }, nil + } + return nil, fmt.Errorf("%q does not match patterns", value) + +} + +func ConvertDecimal64(value string, lst *SchemaLeafType) (*TypedValue, error) { + v := strings.TrimSpace(value) + if v == "" { + return nil, fmt.Errorf("empty decimal64 string") + } + + neg := false + if strings.HasPrefix(v, "-") { + neg = true + v = v[1:] + } else if strings.HasPrefix(v, "+") { + v = v[1:] + } + + if v == "" { + return nil, fmt.Errorf("no digits after sign") + } + + parts := strings.SplitN(v, ".", 2) + intPart := parts[0] + fracPart := "" + if len(parts) == 2 { + fracPart = parts[1] + } + + // Require at least one digit total (either int or frac) + if intPart == "" && fracPart == "" { + return nil, fmt.Errorf("no digits in decimal64 value") + } + + if intPart == "" { + intPart = "0" + } + + combined := intPart + fracPart + if combined == "" { + return nil, fmt.Errorf("no digits to parse") + } + + digits, err := strconv.ParseInt(combined, 10, 64) + if err != nil { + return nil, err + } + if neg { + digits = -digits + } + + precision := uint32(len(fracPart)) + + d64 := &Decimal64{ + Digits: digits, + Precision: precision, + } + + return &TypedValue{ + Value: &TypedValue_DecimalVal{ + DecimalVal: d64, + }, + }, nil +} + +func ConvertUnion(value string, slts []*SchemaLeafType) (*TypedValue, error) { + // iterate over the union types try to convert without error + for _, slt := range slts { + tv, err := TVFromString(slt, value, 0) + // if no error type conversion was fine + if err != nil { + continue + } + // return the TypedValue + return tv, nil + } + return nil, fmt.Errorf("no union type fit the provided value %q", value) +} + +func validateBitString(value string, allowed []*Bit) bool { + //split string to individual bits + bits := strings.Fields(value) + // empty string is fine + if len(bits) == 0 { + return true + } + // track pos inside allowed slice + pos := 0 + for _, b := range bits { + // increase pos until we get to an allowed bit or we reach the end of the slice + for pos < len(allowed) && allowed[pos].GetName() != b { + pos++ + } + // if we are at the end of the array, we did not validate + if pos == len(allowed) { + return false + } + //move past found element + pos++ + } + return true +} + +func ConvertBits(value string, slt *SchemaLeafType) (*TypedValue, error) { + if slt == nil { + return nil, fmt.Errorf("type information is nil") + } + if len(slt.Bits) == 0 { + return nil, fmt.Errorf("type information is missing bits information") + } + if validateBitString(value, slt.Bits) { + return &TypedValue{ + Value: &TypedValue_StringVal{ + StringVal: value, + }, + }, nil + } + // If value is not valid return an error + validBits := make([]string, 0, len(slt.Bits)) + for _, b := range slt.Bits { + validBits = append(validBits, b.GetName()) + } + return nil, fmt.Errorf("value %q does not follow required bit ordering [%s]", value, strings.Join(validBits, " ")) +} diff --git a/sdcpb/typed_value_conversions_test.go b/sdcpb/typed_value_conversions_test.go new file mode 100644 index 0000000..ca9406e --- /dev/null +++ b/sdcpb/typed_value_conversions_test.go @@ -0,0 +1,167 @@ +package sdcpb + +import ( + "reflect" + "testing" + + "google.golang.org/protobuf/proto" +) + +func TestXMLRegexConvert(t *testing.T) { + + tests := []struct { + name string + in string + want string + }{ + { + name: "anchors become literals", + in: `^\d+$`, + want: `\^\d+\$`, + }, + { + name: "already-escaped anchors stay escaped", + in: `foo\$bar`, + want: `foo\$bar`, + }, + { + name: "caret in char class is left alone, dollar is escaped", + in: `[^\w]+$`, + want: `[^\w]+\$`, + }, + { + name: "caret later inside char class is escaped", + in: `[a^b]`, + want: `[a\^b]`, + }, + { + name: "caret escaped inside char class is escaped", + in: `[\^]`, + want: `[\^]`, + }, + { + name: "caret in char class multiple times, dollar is escaped", + in: `[^a^b]`, + want: `[^a\^b]`, + }, + { + name: "anchors preceded by a single back-slash stay escaped", + in: `\^test\$`, + want: `\^test\$`, + }, + { + name: "empty string", + in: ``, + want: ``, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := xmlRegexConvert(tt.in); !reflect.DeepEqual(got, tt.want) { + t.Errorf("XMLRegexConvert() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestValidateBitString(t *testing.T) { + ref := []*Bit{ + {Name: "a", Position: 0}, + {Name: "b", Position: 0}, + {Name: "c", Position: 0}, + } + + tests := []struct { + name string + input string + want bool + }{ + {"empty string", "", true}, + {"exact match", "a b c", true}, + {"skipped middle", "a c", true}, + {"single first element", "a", true}, + {"single last element", "c", true}, + {"out of order", "a c b", false}, + {"wrong order overall", "b c a", false}, + {"unknown single element", "d", false}, + {"unknown element in valid", "a c d", false}, + {"duplicate token", "a a", false}, + {"leading / trailing spaces", " a b ", true}, + {"input longer than schema", "a b c d", false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := validateBitString(tc.input, ref) + if got != tc.want { + t.Fatalf("validateBitString(%q) = %v, want %v", tc.input, got, tc.want) + } + }) + } +} + +func TestConvertBits(t *testing.T) { + slt := &SchemaLeafType{ + Bits: []*Bit{{Name: "a", Position: 0}, {Name: "b", Position: 1}, {Name: "c", Position: 2}}, + } + sltEmpty := &SchemaLeafType{} + + sTv := func(s string) *TypedValue { + return &TypedValue{Value: &TypedValue_StringVal{StringVal: s}} + } + + type inStruct struct { + value string + slt *SchemaLeafType + } + + tests := []struct { + name string + input inStruct + want *TypedValue + wantErr bool + }{ + { + "valid value", + inStruct{"a b c", slt}, + sTv("a b c"), + false, + }, + { + "invalid value", + inStruct{"c b a", slt}, + nil, + true, + }, + {"empty schema, empty input", + inStruct{"", sltEmpty}, + nil, + true, + }, + {"empty schema, non-empty input", + inStruct{"a", sltEmpty}, + nil, + true, + }, + {"nil schema pointer", + inStruct{"a", nil}, + nil, + true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := ConvertBits(tc.input.value, tc.input.slt) + if tc.wantErr && err == nil { + t.Fatalf("wanted error, got nil") + } + if !tc.wantErr && err != nil { + t.Fatalf("wanted no error, got %v", err) + } + if !proto.Equal(got, tc.want) { + t.Fatalf("ConvertBits(%q, %q) = %v, want %v", tc.input.value, tc.input.slt, got, tc.want) + } + }) + } +} diff --git a/utils/rnges.go b/utils/rnges.go new file mode 100644 index 0000000..2d1e67d --- /dev/null +++ b/utils/rnges.go @@ -0,0 +1,86 @@ +// Copyright 2024 Nokia +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "fmt" + "strings" +) + +type Number interface { + int64 | uint64 +} + +// Rnges represents a collection of rng (range) +type Rnges[T Number] struct { + rnges []Rng[T] +} + +// URng represents a single unsigned range +type Rng[T Number] struct { + min T + max T +} + +func NewRnges[T Number]() *Rnges[T] { + r := &Rnges[T]{rnges: make([]Rng[T], 0)} + return r +} + +func (r *Rng[T]) IsInRange(value T) bool { + // return the result + return r.min <= value && value <= r.max +} + +func (r *Rng[T]) String() string { + // return the result + return fmt.Sprintf("%d..%d", r.min, r.max) +} + +func (r *Rnges[T]) IsWithinAnyRange(val T) bool { + // if no ranges defined, return the tv + if len(r.rnges) == 0 { + return true + } + // check the ranges + for _, rng := range r.rnges { + if rng.IsInRange(val) { + return true + } + } + return false +} + +func (r *Rnges[T]) AddRange(min, max T) { + // to make sure the value is in the general limits of the datatype uint8|16|32|64 + // we add the min max as a seperate additional range + r.rnges = append(r.rnges, Rng[T]{ + min: min, + max: max, + }) +} + +func (r *Rnges[T]) String() string { + sb := &strings.Builder{} + sep := "" + sb.WriteString("[ ") + for _, ur := range r.rnges { + sb.WriteString(sep) + sb.WriteString(ur.String()) + sep = ", " + } + sb.WriteString(" ]") + return sb.String() +}