From 9fad5f7fafd86ef8eeacd8857d51e125de9c4c29 Mon Sep 17 00:00:00 2001 From: steiler Date: Mon, 4 May 2026 12:23:03 +0200 Subject: [PATCH 1/4] Support Validation of union types This PR fixes validation gaps for YANG leaves defined as union types. Today, union branch selection happens at import time (XML/JSON/proto): the server tries branches in order and stores the first successfully parsed TypedValue. Later, validation runs against the outer union schema type instead of the specific matched branch type. Because the outer union type does not carry branch-specific constraints, several validators can no-op unexpectedly. Problem For union-typed leaves, the current validation path can miss real violations: Pattern and length checks read constraints from the outer union type, which has none. Range checks read constraints from the outer union type, which has none. Leafref checks look for a leafref path on the outer union type, which is empty. This means values that actually matched a leafref branch can bypass live-tree reference validation, allowing dangling references to pass undetected. --- go.mod | 2 +- go.sum | 4 +- pkg/tree/importer/import_config_adapter.go | 5 +- pkg/tree/importer/json/json_tree_importer.go | 4 +- .../importer/json/json_tree_importer_test.go | 45 ++++-- .../importer/proto/proto_tree_importer.go | 10 +- .../proto/proto_tree_importer_test.go | 35 +++++ pkg/tree/importer/xml/xml_tree_importer.go | 4 +- .../importer/xml/xml_tree_importer_test.go | 64 ++++++++ .../validation/validation_entry_leafref.go | 29 +++- .../ops/validation/validation_entry_length.go | 15 +- .../validation_entry_length_test.go | 79 ++++++++++ .../validation/validation_entry_pattern.go | 13 +- .../validation_entry_pattern_test.go | 79 ++++++++++ .../ops/validation/validation_entry_range.go | 14 +- .../validation_test_helpers_test.go | 54 +++++++ .../validation_union_leafref_test.go | 75 +++++++++ .../validation_union_proto_fallback_test.go | 140 +++++++++++++++++ .../validation/validation_union_range_test.go | 64 ++++++++ pkg/tree/processors/processor_importer.go | 6 +- pkg/tree/types/update.go | 39 +++-- pkg/tree/types/update_test.go | 68 ++++++++ tests/schema/sdcio_model.yang | 32 ++++ tests/sdcioygot/sdcio_schema.go | 145 +++++++++++++++++- 24 files changed, 963 insertions(+), 62 deletions(-) create mode 100644 pkg/tree/ops/validation/validation_entry_length_test.go create mode 100644 pkg/tree/ops/validation/validation_entry_pattern_test.go create mode 100644 pkg/tree/ops/validation/validation_test_helpers_test.go create mode 100644 pkg/tree/ops/validation/validation_union_leafref_test.go create mode 100644 pkg/tree/ops/validation/validation_union_proto_fallback_test.go create mode 100644 pkg/tree/ops/validation/validation_union_range_test.go create mode 100644 pkg/tree/types/update_test.go diff --git a/go.mod b/go.mod index 9691778c..a7adf6a7 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/sdcio/cache v0.0.38 github.com/sdcio/logger v0.0.3 github.com/sdcio/schema-server v0.0.34 - github.com/sdcio/sdc-protos v0.0.51 + github.com/sdcio/sdc-protos v0.0.52-0.20260504101706-192d1e8e12bf github.com/sdcio/yang-parser v0.0.12 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 diff --git a/go.sum b/go.sum index ab43f6d6..b7cb0c07 100644 --- a/go.sum +++ b/go.sum @@ -193,8 +193,8 @@ github.com/sdcio/logger v0.0.3 h1:IFUbObObGry+S8lHGwOQKKRxJSuOphgRU/hxVhOdMOM= github.com/sdcio/logger v0.0.3/go.mod h1:yWaOxK/G6vszjg8tKZiMqiEjlZouHsjFME4zSk+SAEA= github.com/sdcio/schema-server v0.0.34 h1:NNDOkvtUMONtBA7cVvN96F+FWGD/Do6HNqfchy9B8eI= github.com/sdcio/schema-server v0.0.34/go.mod h1:6t8HLXpqUqEJmE5yNZh29u/KZw0jlOICdNWns7zE4GE= -github.com/sdcio/sdc-protos v0.0.51 h1:sFc2ct8v4D7rBgFdg/fmXgJffcFhupfx4QJKsSAoolA= -github.com/sdcio/sdc-protos v0.0.51/go.mod h1:FkJMZWtp7Rcc/EedbX2mt1tET/j8KdavNl2BsHf03+o= +github.com/sdcio/sdc-protos v0.0.52-0.20260504101706-192d1e8e12bf h1:O+6B6u6J9ul6uGdG5vyeK5B2eu9ujgubVCtERl0lG8o= +github.com/sdcio/sdc-protos v0.0.52-0.20260504101706-192d1e8e12bf/go.mod h1:FkJMZWtp7Rcc/EedbX2mt1tET/j8KdavNl2BsHf03+o= github.com/sdcio/yang-parser v0.0.12 h1:RSSeqfAOIsJx5Lno5u4/ezyOmQYUduQ22rBfU/mtpJ4= github.com/sdcio/yang-parser v0.0.12/go.mod h1:CBqn3Miq85qmFVGHxHXHLluXkaIOsTzV06IM4DW6+D4= github.com/sirikothe/gotextfsm v1.0.1-0.20200816110946-6aa2cfd355e4 h1:FHUL2HofYJuslFOQdy/JjjP36zxqIpd/dcoiwLMIs7k= diff --git a/pkg/tree/importer/import_config_adapter.go b/pkg/tree/importer/import_config_adapter.go index 6096178f..e4960f7b 100644 --- a/pkg/tree/importer/import_config_adapter.go +++ b/pkg/tree/importer/import_config_adapter.go @@ -30,8 +30,9 @@ type ImportConfigAdapterElement interface { // When and were to expect a Leafs or LeafList is defined by the yang schema. // The String value is typically used for the keys. GetKeyValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (string, error) - // GetTVValue returns the TypedValue based value defined via the SchemaLeafType. Can also only be called on Leafs or LeafLists - GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, error) + // GetTVValue returns the TypedValue based value defined via the SchemaLeafType. Can also only be called on Leafs or LeafLists. + // For union-typed leaves the second return value is the matched branch SchemaLeafType; for non-union or proto-loaded values it is nil. + GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, *sdcpb.SchemaLeafType, error) // returns the name of the actual Level. GetName() string } diff --git a/pkg/tree/importer/json/json_tree_importer.go b/pkg/tree/importer/json/json_tree_importer.go index 0c50a82e..5d01578f 100644 --- a/pkg/tree/importer/json/json_tree_importer.go +++ b/pkg/tree/importer/json/json_tree_importer.go @@ -100,8 +100,8 @@ func (j *JsonTreeImporterElement) GetKeyValue(ctx context.Context, slt *sdcpb.Sc return fmt.Sprintf("%v", j.data), nil } -func (j *JsonTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, error) { - return sdcpb.ConvertJsonValueToTv(j.data, slt) +func (j *JsonTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, *sdcpb.SchemaLeafType, error) { + return sdcpb.ConvertJsonValueToTvWithType(j.data, slt) } func (j *JsonTreeImporterElement) GetName() string { diff --git a/pkg/tree/importer/json/json_tree_importer_test.go b/pkg/tree/importer/json/json_tree_importer_test.go index 732f6db3..421949c3 100644 --- a/pkg/tree/importer/json/json_tree_importer_test.go +++ b/pkg/tree/importer/json/json_tree_importer_test.go @@ -127,11 +127,12 @@ func TestJsonTreeImporter_GetTVValue(t *testing.T) { slt *sdcpb.SchemaLeafType } tests := []struct { - name string - fields fields - args args - want *sdcpb.TypedValue - wantErr bool + name string + fields fields + args args + want *sdcpb.TypedValue + wantMatchedType string // expected matched type name ("" means same as input type) + wantErr bool }{ { name: "string", @@ -144,8 +145,9 @@ func TestJsonTreeImporter_GetTVValue(t *testing.T) { Type: "string", }, }, - wantErr: false, - want: &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foobar"}}, + wantErr: false, + wantMatchedType: "string", + want: &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foobar"}}, }, { name: "int", @@ -158,8 +160,28 @@ func TestJsonTreeImporter_GetTVValue(t *testing.T) { Type: "int32", }, }, - wantErr: false, - want: &sdcpb.TypedValue{Value: &sdcpb.TypedValue_IntVal{IntVal: 5}}, + wantErr: false, + wantMatchedType: "int32", + want: &sdcpb.TypedValue{Value: &sdcpb.TypedValue_IntVal{IntVal: 5}}, + }, + { + name: "union matched string branch", + fields: fields{ + name: "foo", + data: "hello", + }, + args: args{ + &sdcpb.SchemaLeafType{ + Type: "union", + UnionTypes: []*sdcpb.SchemaLeafType{ + {Type: "uint32"}, + {Type: "string"}, + }, + }, + }, + wantErr: false, + wantMatchedType: "string", + want: &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "hello"}}, }, } for _, tt := range tests { @@ -168,7 +190,7 @@ func TestJsonTreeImporter_GetTVValue(t *testing.T) { data: tt.fields.data, name: tt.fields.name, } - got, err := j.GetTVValue(ctx, tt.args.slt) + got, matchedType, err := j.GetTVValue(ctx, tt.args.slt) if (err != nil) != tt.wantErr { t.Errorf("JsonTreeImporter.GetTVValue() error = %v, wantErr %v", err, tt.wantErr) return @@ -176,6 +198,9 @@ func TestJsonTreeImporter_GetTVValue(t *testing.T) { if !reflect.DeepEqual(got, tt.want) { t.Errorf("JsonTreeImporter.GetTVValue() = %v, want %v", got, tt.want) } + if tt.wantMatchedType != "" && (matchedType == nil || matchedType.Type != tt.wantMatchedType) { + t.Errorf("JsonTreeImporter.GetTVValue() matchedType = %v, want type %q", matchedType, tt.wantMatchedType) + } }) } } diff --git a/pkg/tree/importer/proto/proto_tree_importer.go b/pkg/tree/importer/proto/proto_tree_importer.go index 5021a74d..3ae5c901 100644 --- a/pkg/tree/importer/proto/proto_tree_importer.go +++ b/pkg/tree/importer/proto/proto_tree_importer.go @@ -80,20 +80,22 @@ func (p *ProtoTreeImporterElement) GetElement(key string) importer.ImportConfigA } func (p *ProtoTreeImporterElement) GetKeyValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (string, error) { - tv, err := p.GetTVValue(ctx, slt) + tv, _, err := p.GetTVValue(ctx, slt) if err != nil { return "", fmt.Errorf("failed GetTVValue for %s", p.data.Name) } return tv.ToString(), nil } -func (p *ProtoTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, error) { +// GetTVValue unmarshals the proto-serialized TypedValue. No original lexical context is available, +// so the matched union branch type is always nil (graceful fallback to outer schema type in validators). +func (p *ProtoTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, *sdcpb.SchemaLeafType, error) { result := &sdcpb.TypedValue{} err := proto.Unmarshal(p.data.LeafVariant, result) if err != nil { - return nil, err + return nil, nil, err } - return result, nil + return result, nil, nil } func (p *ProtoTreeImporterElement) GetName() string { return p.data.Name diff --git a/pkg/tree/importer/proto/proto_tree_importer_test.go b/pkg/tree/importer/proto/proto_tree_importer_test.go index 652fca88..ff6ca3de 100644 --- a/pkg/tree/importer/proto/proto_tree_importer_test.go +++ b/pkg/tree/importer/proto/proto_tree_importer_test.go @@ -15,7 +15,10 @@ import ( "github.com/sdcio/data-server/pkg/tree/ops" "github.com/sdcio/data-server/pkg/tree/types" "github.com/sdcio/data-server/pkg/utils/testhelper" + sdcpb "github.com/sdcio/sdc-protos/sdcpb" + "github.com/sdcio/sdc-protos/tree_persist" "go.uber.org/mock/gomock" + "google.golang.org/protobuf/proto" ) func TestProtoTreeImporter(t *testing.T) { @@ -175,3 +178,35 @@ func TestProtoTreeImporter(t *testing.T) { }) } } + +func TestProtoTreeImporterElement_GetTVValue_NilMatchedType(t *testing.T) { + ctx := context.Background() + + // Proto importer has no original lexical context, so matched type must always be nil. + tv := &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "hello"}} + raw, err := proto.Marshal(tv) + if err != nil { + t.Fatal(err) + } + + elem := NewProtoTreeImporterElement(&tree_persist.TreeElement{ + Name: "leaf", + LeafVariant: raw, + }) + + unionSlt := &sdcpb.SchemaLeafType{ + Type: "union", + UnionTypes: []*sdcpb.SchemaLeafType{ + {Type: "uint32"}, + {Type: "string"}, + }, + } + + _, matchedType, err := elem.GetTVValue(ctx, unionSlt) + if err != nil { + t.Fatalf("GetTVValue() unexpected error: %v", err) + } + if matchedType != nil { + t.Errorf("proto importer should return nil matchedType, got %v", matchedType) + } +} diff --git a/pkg/tree/importer/xml/xml_tree_importer.go b/pkg/tree/importer/xml/xml_tree_importer.go index e8a8031b..52e791e8 100644 --- a/pkg/tree/importer/xml/xml_tree_importer.go +++ b/pkg/tree/importer/xml/xml_tree_importer.go @@ -81,8 +81,8 @@ func (x *XmlTreeImporterElement) GetKeyValue(ctx context.Context, slt *sdcpb.Sch return tv.ToString(), nil } -func (x *XmlTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, error) { - return sdcpb.TVFromString(slt, x.elem.Text(), 0) +func (x *XmlTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, *sdcpb.SchemaLeafType, error) { + return sdcpb.TVFromStringWithType(slt, x.elem.Text(), 0) } func (x *XmlTreeImporterElement) GetName() string { diff --git a/pkg/tree/importer/xml/xml_tree_importer_test.go b/pkg/tree/importer/xml/xml_tree_importer_test.go index 5b396913..0d4e81f2 100644 --- a/pkg/tree/importer/xml/xml_tree_importer_test.go +++ b/pkg/tree/importer/xml/xml_tree_importer_test.go @@ -237,3 +237,67 @@ func TestXmlTreeImporterElement_IdentityRef(t *testing.T) { t.Fatalf("Integrating xml failed. mismatch (-want +got).\nDiff:\n%s", diff) } } + +func TestXmlTreeImporterElement_GetTVValue_MatchedType(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + xmlText string + slt *sdcpb.SchemaLeafType + wantMatchedType string // expected matched branch TypeName ("" = nil expected) + wantErr bool + }{ + { + name: "non-union returns input type", + xmlText: "hello", + slt: &sdcpb.SchemaLeafType{Type: "string"}, + wantMatchedType: "string", + }, + { + name: "union matched string branch", + xmlText: "hello", + slt: &sdcpb.SchemaLeafType{ + Type: "union", + UnionTypes: []*sdcpb.SchemaLeafType{ + {Type: "uint32"}, + {Type: "string"}, + }, + }, + wantMatchedType: "string", + }, + { + name: "union matched uint32 branch", + xmlText: "42", + slt: &sdcpb.SchemaLeafType{ + Type: "union", + UnionTypes: []*sdcpb.SchemaLeafType{ + {Type: "uint32"}, + {Type: "string"}, + }, + }, + wantMatchedType: "uint32", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doc := etree.NewDocument() + doc.ReadFromString("" + tt.xmlText + "") + elem := NewXmlTreeImporterElement(doc.Root()) + + _, matchedType, err := elem.GetTVValue(ctx, tt.slt) + if (err != nil) != tt.wantErr { + t.Fatalf("GetTVValue() error = %v, wantErr %v", err, tt.wantErr) + } + if tt.wantMatchedType == "" { + if matchedType != nil { + t.Errorf("expected nil matchedType, got %v", matchedType) + } + } else { + if matchedType == nil || matchedType.Type != tt.wantMatchedType { + t.Errorf("matchedType = %v, want type %q", matchedType, tt.wantMatchedType) + } + } + }) + } +} diff --git a/pkg/tree/ops/validation/validation_entry_leafref.go b/pkg/tree/ops/validation/validation_entry_leafref.go index fa37b0ca..47aa2179 100644 --- a/pkg/tree/ops/validation/validation_entry_leafref.go +++ b/pkg/tree/ops/validation/validation_entry_leafref.go @@ -118,6 +118,13 @@ func navigateLeafRef(ctx context.Context, e api.Entry) ([]api.Entry, error) { return nil, fmt.Errorf("error not a leafref %s", e.SdcpbPath()) } + return navigateLeafRefByPath(ctx, e, lref) +} + +// navigateLeafRefByPath navigates the tree to find entries matching the given leafref path and +// the current entry's value. It is the implementation for both direct leafref leaves and union +// leaves whose matched branch is a leafref. +func navigateLeafRefByPath(ctx context.Context, e api.Entry, lref string) ([]api.Entry, error) { lv := e.GetLeafVariants().GetHighestPrecedence(false, true, false) if lv == nil { return nil, fmt.Errorf("no leafvariant found") @@ -206,15 +213,27 @@ func validateLeafRefs(ctx context.Context, e api.Entry, resultChan chan<- *types return } - lref := e.GetSchema().GetField().GetType().GetLeafref() - if e.GetSchema() == nil || lref == "" { + if e.GetSchema() == nil || e.GetSchema().GetField() == nil { + return + } + + // Resolve the effective leaf type — for union leaves, this returns the matched branch; + // for direct leafref leaves it returns the outer schema type unchanged. + lv := e.GetLeafVariants().GetHighestPrecedence(false, true, false) + effectiveType := e.GetSchema().GetField().GetType() + if lv != nil { + effectiveType = lv.Update.EffectiveLeafType(e.GetSchema().GetField().GetType()) + } + + lref := effectiveType.GetLeafref() + if lref == "" { return } - entry, err := navigateLeafRef(ctx, e) + entry, err := navigateLeafRefByPath(ctx, e, lref) if err != nil || len(entry) == 0 { // check if the OptionalInstance (!require-instances [https://datatracker.ietf.org/doc/html/rfc7950#section-9.9.3]) - if e.GetSchema().GetField().GetType().GetOptionalInstance() { + if effectiveType.GetOptionalInstance() { generateOptionalWarning(e, lref, resultChan) return } @@ -239,7 +258,7 @@ func validateLeafRefs(ctx context.Context, e api.Entry, resultChan chan<- *types } // check if the OptionalInstance (!require-instances [https://datatracker.ietf.org/doc/html/rfc7950#section-9.9.3]) - if e.GetSchema().GetField().GetType().GetOptionalInstance() { + if effectiveType.GetOptionalInstance() { generateOptionalWarning(e, lref, resultChan) return } diff --git a/pkg/tree/ops/validation/validation_entry_length.go b/pkg/tree/ops/validation/validation_entry_length.go index fee95d1d..6f20938f 100644 --- a/pkg/tree/ops/validation/validation_entry_length.go +++ b/pkg/tree/ops/validation/validation_entry_length.go @@ -13,18 +13,21 @@ import ( func validateLength(_ context.Context, e api.Entry, resultChan chan<- *types.ValidationResultEntry, stats *types.ValidationStats) { if schema := e.GetSchema().GetField(); schema != nil { - if len(schema.GetType().GetLength()) == 0 { + lv := e.GetLeafVariants().GetHighestPrecedence(false, true, false) + if lv == nil { return } - lv := e.GetLeafVariants().GetHighestPrecedence(false, true, false) - if lv == nil { + effectiveType := lv.Update.EffectiveLeafType(schema.GetType()) + + if len(effectiveType.GetLength()) == 0 { return } + value := lv.Value().GetStringVal() actualLength := utf8.RuneCountInString(value) - for _, lengthDef := range schema.GetType().GetLength() { + for _, lengthDef := range effectiveType.GetLength() { if lengthDef.Min.Value <= uint64(actualLength) && uint64(actualLength) <= lengthDef.Max.Value { // continue if the length is within the range @@ -32,11 +35,11 @@ func validateLength(_ context.Context, e api.Entry, resultChan chan<- *types.Val } // this is already the failure case lenghts := []string{} - for _, lengthDef := range schema.GetType().GetLength() { + for _, lengthDef := range effectiveType.GetLength() { lenghts = append(lenghts, fmt.Sprintf("%d..%d", lengthDef.Min.Value, lengthDef.Max.Value)) } resultChan <- types.NewValidationResultEntry(lv.Owner(), fmt.Errorf("error length of Path: %s, Value: %s not within allowed length %s", e.SdcpbPath().ToXPath(false), value, strings.Join(lenghts, ", ")), types.ValidationResultEntryTypeError) } - stats.Add(types.StatTypeLength, uint32(len(schema.GetType().GetLength()))) + stats.Add(types.StatTypeLength, uint32(len(effectiveType.GetLength()))) } } diff --git a/pkg/tree/ops/validation/validation_entry_length_test.go b/pkg/tree/ops/validation/validation_entry_length_test.go new file mode 100644 index 00000000..3fdcc5d4 --- /dev/null +++ b/pkg/tree/ops/validation/validation_entry_length_test.go @@ -0,0 +1,79 @@ +package validation_test + +import ( + "context" + "strings" + "testing" + + "github.com/openconfig/ygot/ygot" + "github.com/sdcio/data-server/pkg/tree/ops/validation" + "github.com/sdcio/data-server/pkg/utils/testhelper" + sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" + "go.uber.org/mock/gomock" +) + +func TestValidate_Length(t *testing.T) { + tests := []struct { + name string + req *sdcio_schema.Device + wantErrors int + }{ + { + name: "non-union leaf length within range", + req: &sdcio_schema.Device{ + Patterntest: ygot.String("hallo AB"), + }, + wantErrors: 0, + }, + { + name: "non-union leaf length too short", + req: &sdcio_schema.Device{ + Patterntest: ygot.String("hallo A"), + }, + wantErrors: 0, + }, + { + name: "union leaf - string branch length within range", + req: &sdcio_schema.Device{ + Unionlengthtest: &sdcio_schema.SdcioModel_Unionlengthtest_Union_String{String: "12345678"}, + }, + wantErrors: 0, + }, + { + name: "union leaf - string branch length too short", + req: &sdcio_schema.Device{ + Unionlengthtest: &sdcio_schema.SdcioModel_Unionlengthtest_Union_String{String: "12345"}, + }, + wantErrors: 1, + }, + { + name: "union leaf - uint32 branch has no length constraint", + req: &sdcio_schema.Device{ + Unionlengthtest: &sdcio_schema.SdcioModel_Unionlengthtest_Union_Uint32{Uint32: 42}, + }, + wantErrors: 0, + }, + } + + ctx := context.TODO() + mockCtrl := gomock.NewController(t) + + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root, sharedPool := importDeviceJSON(t, ctx, scb, tt.req) + + validationResult, _ := validation.Validate(ctx, root.Entry, validationConfig, sharedPool) + + t.Logf("Validation Errors:\n%s", strings.Join(validationResult.ErrorsStr(), "\n")) + + if len(validationResult.ErrorsStr()) != tt.wantErrors { + t.Errorf("expected %d error(s), got %d: %v", tt.wantErrors, len(validationResult.ErrorsStr()), validationResult.ErrorsStr()) + } + }) + } +} diff --git a/pkg/tree/ops/validation/validation_entry_pattern.go b/pkg/tree/ops/validation/validation_entry_pattern.go index edcfa236..e90f3804 100644 --- a/pkg/tree/ops/validation/validation_entry_pattern.go +++ b/pkg/tree/ops/validation/validation_entry_pattern.go @@ -37,16 +37,19 @@ func yangPatternToGo(p string) string { func validatePattern(_ context.Context, e api.Entry, resultChan chan<- *types.ValidationResultEntry, stats *types.ValidationStats) { if schema := e.GetSchema().GetField(); schema != nil { - if len(schema.GetType().GetPatterns()) == 0 { + lv := e.GetLeafVariants().GetHighestPrecedence(false, true, false) + if lv == nil { return } - lv := e.GetLeafVariants().GetHighestPrecedence(false, true, false) - if lv == nil { + effectiveType := lv.Update.EffectiveLeafType(schema.GetType()) + + if len(effectiveType.GetPatterns()) == 0 { return } + value := lv.Value().GetStringVal() - for _, pattern := range schema.GetType().GetPatterns() { + for _, pattern := range effectiveType.GetPatterns() { if p := pattern.GetPattern(); p != "" { goPattern := yangPatternToGo(p) matched, err := regexp.MatchString(goPattern, value) @@ -60,6 +63,6 @@ func validatePattern(_ context.Context, e api.Entry, resultChan chan<- *types.Va } } - stats.Add(types.StatTypePattern, uint32(len(schema.GetType().GetPatterns()))) + stats.Add(types.StatTypePattern, uint32(len(effectiveType.GetPatterns()))) } } diff --git a/pkg/tree/ops/validation/validation_entry_pattern_test.go b/pkg/tree/ops/validation/validation_entry_pattern_test.go new file mode 100644 index 00000000..9009b21b --- /dev/null +++ b/pkg/tree/ops/validation/validation_entry_pattern_test.go @@ -0,0 +1,79 @@ +package validation_test + +import ( + "context" + "strings" + "testing" + + "github.com/openconfig/ygot/ygot" + "github.com/sdcio/data-server/pkg/tree/ops/validation" + "github.com/sdcio/data-server/pkg/utils/testhelper" + sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" + "go.uber.org/mock/gomock" +) + +func TestValidate_Pattern(t *testing.T) { + tests := []struct { + name string + req *sdcio_schema.Device + wantErrors int + }{ + { + name: "non-union leaf pattern matches", + req: &sdcio_schema.Device{ + Patterntest: ygot.String("hallo AB"), + }, + wantErrors: 0, + }, + { + name: "non-union leaf pattern violation", + req: &sdcio_schema.Device{ + Patterntest: ygot.String("hello AB"), + }, + wantErrors: 1, + }, + { + name: "union leaf - string branch pattern matches", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hallo AB"}, + }, + wantErrors: 0, + }, + { + name: "union leaf - string branch pattern violation", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hello AB"}, + }, + wantErrors: 1, + }, + { + name: "union leaf - uint32 branch has no pattern constraint", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_Uint32{Uint32: 42}, + }, + wantErrors: 0, + }, + } + + ctx := context.TODO() + mockCtrl := gomock.NewController(t) + + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root, sharedPool := importDeviceJSON(t, ctx, scb, tt.req) + + validationResult, _ := validation.Validate(ctx, root.Entry, validationConfig, sharedPool) + + t.Logf("Validation Errors:\n%s", strings.Join(validationResult.ErrorsStr(), "\n")) + + if len(validationResult.ErrorsStr()) != tt.wantErrors { + t.Errorf("expected %d error(s), got %d: %v", tt.wantErrors, len(validationResult.ErrorsStr()), validationResult.ErrorsStr()) + } + }) + } +} diff --git a/pkg/tree/ops/validation/validation_entry_range.go b/pkg/tree/ops/validation/validation_entry_range.go index 2b7c7ba2..6fc0ccda 100644 --- a/pkg/tree/ops/validation/validation_entry_range.go +++ b/pkg/tree/ops/validation/validation_entry_range.go @@ -14,8 +14,7 @@ import ( // validateRange does check this condition. func validateRange(_ context.Context, e api.Entry, resultChan chan<- *types.ValidationResultEntry, stats *types.ValidationStats) { - // if no schema present or Field and LeafList Types do not contain any ranges, return there is nothing to check - if e.GetSchema() == nil || (len(e.GetSchema().GetField().GetType().GetRange()) == 0 && len(e.GetSchema().GetLeaflist().GetType().GetRange()) == 0) { + if e.GetSchema() == nil { return } @@ -30,11 +29,14 @@ func validateRange(_ context.Context, e api.Entry, resultChan chan<- *types.Vali var typeSchema *sdcpb.SchemaLeafType // ranges are defined on Leafs or LeafLists. switch { - case len(e.GetSchema().GetField().GetType().GetRange()) != 0: - // if it is a leaf, extract the value add it as a single value to the tvs slice and check it further down + case e.GetSchema().GetField() != nil: + // For leaf nodes, use the effective type (matched union branch if any, else outer type). + effectiveType := lv.Update.EffectiveLeafType(e.GetSchema().GetField().GetType()) + if len(effectiveType.GetRange()) == 0 { + return + } tvs = []*sdcpb.TypedValue{tv} - // we also need the Field/Leaf Type schema - typeSchema = e.GetSchema().GetField().GetType() + typeSchema = effectiveType case len(e.GetSchema().GetLeaflist().GetType().GetRange()) != 0: // if it is a leaflist, extract the values them to the tvs slice and check them further down tvs = tv.GetLeaflistVal().GetElement() diff --git a/pkg/tree/ops/validation/validation_test_helpers_test.go b/pkg/tree/ops/validation/validation_test_helpers_test.go new file mode 100644 index 00000000..d76743a1 --- /dev/null +++ b/pkg/tree/ops/validation/validation_test_helpers_test.go @@ -0,0 +1,54 @@ +package validation +package validation_test + +import ( + "context" + "encoding/json" + "runtime" + "testing" + + "github.com/openconfig/ygot/ygot" + schemaClient "github.com/sdcio/data-server/pkg/datastore/clients/schema" + "github.com/sdcio/data-server/pkg/pool" + "github.com/sdcio/data-server/pkg/tree" + json_importer "github.com/sdcio/data-server/pkg/tree/importer/json" + "github.com/sdcio/data-server/pkg/tree/types" + sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" + sdcpb "github.com/sdcio/sdc-protos/sdcpb" +) + +// importDeviceJSON creates a fresh tree, imports req via JSON (owner "owner1", priority 5), +// finishes the insertion phase, and returns the root entry and a shared pool ready for validation. +func importDeviceJSON(t *testing.T, ctx context.Context, scb schemaClient.SchemaClientBound, req *sdcio_schema.Device) (*tree.RootEntry, *pool.SharedTaskPool) { + t.Helper() + + tc := tree.NewTreeContext(scb, pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0))) + + root, err := tree.NewTreeRoot(ctx, tc) + if err != nil { + t.Fatal(err) + } + + configJSON, err := ygot.EmitJSON(req, &ygot.EmitJSONConfig{Format: ygot.RFC7951, SkipValidation: true}) + if err != nil { + t.Fatal(err) + } + + var jsonConfig any + if err = json.Unmarshal([]byte(configJSON), &jsonConfig); err != nil { + t.Fatal(err) + } + + jimporter := json_importer.NewJsonTreeImporter(jsonConfig, "owner1", 5, false) + sharedPool := pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0)) + + if _, err = root.ImportConfig(ctx, &sdcpb.Path{}, jimporter, types.NewUpdateInsertFlags(), sharedPool); err != nil { + t.Fatal(err) + } + + if err = root.FinishInsertionPhase(ctx); err != nil { + t.Fatal(err) + } + + return root, sharedPool +} diff --git a/pkg/tree/ops/validation/validation_union_leafref_test.go b/pkg/tree/ops/validation/validation_union_leafref_test.go new file mode 100644 index 00000000..281c2e37 --- /dev/null +++ b/pkg/tree/ops/validation/validation_union_leafref_test.go @@ -0,0 +1,75 @@ +package validation_test + +import ( + "context" + "strings" + "testing" + + "github.com/openconfig/ygot/ygot" + "github.com/sdcio/data-server/pkg/config" + "github.com/sdcio/data-server/pkg/tree/ops/validation" + "github.com/sdcio/data-server/pkg/utils/testhelper" + sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" + "go.uber.org/mock/gomock" +) + +func TestValidate_LeafRef_Union(t *testing.T) { + tests := []struct { + name string + req *sdcio_schema.Device + wantErrors int + }{ + { + name: "union leaf - leafref branch resolves to existing interface", + req: &sdcio_schema.Device{ + Interface: map[string]*sdcio_schema.SdcioModel_Interface{ + "ethernet-1/1": { + Name: ygot.String("ethernet-1/1"), + }, + }, + Unionleafreftest: ygot.String("ethernet-1/1"), + }, + wantErrors: 0, + }, + { + name: "union leaf - leafref branch points to nonexistent interface", + req: &sdcio_schema.Device{ + Interface: map[string]*sdcio_schema.SdcioModel_Interface{ + "ethernet-1/2": { + Name: ygot.String("ethernet-1/2"), + }, + }, + Unionleafreftest: ygot.String("ethernet-1/1"), + }, + wantErrors: 1, + }, + } + + ctx := context.TODO() + mockCtrl := gomock.NewController(t) + + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + // validation config that only runs leafref validation + valConf := config.NewValidationConfig() + valConf.DisabledValidators.DisableAll() + valConf.DisabledValidators.Leafref = false + valConf.SetDisableConcurrency(true) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root, sharedPool := importDeviceJSON(t, ctx, scb, tt.req) + + validationResult, _ := validation.Validate(ctx, root.Entry, valConf, sharedPool) + + t.Logf("Validation Errors:\n%s", strings.Join(validationResult.ErrorsStr(), "\n")) + + if len(validationResult.ErrorsStr()) != tt.wantErrors { + t.Errorf("expected %d error(s), got %d: %v", tt.wantErrors, len(validationResult.ErrorsStr()), validationResult.ErrorsStr()) + } + }) + } +} diff --git a/pkg/tree/ops/validation/validation_union_proto_fallback_test.go b/pkg/tree/ops/validation/validation_union_proto_fallback_test.go new file mode 100644 index 00000000..0238169a --- /dev/null +++ b/pkg/tree/ops/validation/validation_union_proto_fallback_test.go @@ -0,0 +1,140 @@ +package validation_test + +import ( + "context" + "runtime" + "strings" + "testing" + + "github.com/sdcio/data-server/pkg/pool" + "github.com/sdcio/data-server/pkg/tree" + proto_importer "github.com/sdcio/data-server/pkg/tree/importer/proto" + "github.com/sdcio/data-server/pkg/tree/ops" + "github.com/sdcio/data-server/pkg/tree/ops/validation" + "github.com/sdcio/data-server/pkg/tree/types" + "github.com/sdcio/data-server/pkg/utils/testhelper" + sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" + sdcpb "github.com/sdcio/sdc-protos/sdcpb" + "go.uber.org/mock/gomock" +) + +// TestValidate_Union_ProtoFallback verifies that proto-imported union leaves — +// where matchedUnionType is nil because no original lexical context is available — +// fall back gracefully to the outer schema type in all validators. +// Expected behaviour: +// - No panics in any validator. +// - Pattern/length/range/leafref validators silently skip because the outer +// union type carries none of those constraints. +// - This matches pre-change proto import behaviour (regression guard). +func TestValidate_Union_ProtoFallback(t *testing.T) { + tests := []struct { + name string + req *sdcio_schema.Device + wantAfterJSON int + wantAfterProto int + }{ + { + // Valid value: matched string branch pattern ("hallo.*") satisfied. + // After proto round-trip the outer union type has no pattern constraint, + // so the result is still 0 errors — consistent behaviour. + name: "valid union pattern value round-trips without error", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hallo AB"}, + }, + wantAfterJSON: 0, + wantAfterProto: 0, + }, + { + // Pattern-violating value: matched branch has pattern "hallo.*" which + // "hello AB" does not satisfy → 1 error after JSON import. + // After proto round-trip matchedType is nil → fallback to outer union + // type (no pattern) → 0 errors. Validators must not panic. + name: "pattern-violating union value has no error after proto round-trip (fallback)", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hello AB"}, + }, + wantAfterJSON: 1, + wantAfterProto: 0, + }, + { + // Length-violating value: matched string branch has length constraint + // min 8 chars; "123" is too short → 1 error after JSON import. + // After proto round-trip matchedType is nil → fallback → 0 errors. + name: "length-violating union value has no error after proto round-trip (fallback)", + req: &sdcio_schema.Device{ + Unionlengthtest: &sdcio_schema.SdcioModel_Unionlengthtest_Union_String{String: "123"}, + }, + wantAfterJSON: 1, + wantAfterProto: 0, + }, + { + // Range-violating value: matched uint32 branch has range 10..300; + // 500 is out of range → 1 error after JSON import. + // After proto round-trip matchedType is nil → fallback → 0 errors. + name: "range-violating union value has no error after proto round-trip (fallback)", + req: &sdcio_schema.Device{ + Unionrangetest: &sdcio_schema.SdcioModel_Unionrangetest_Union_Uint32{Uint32: 500}, + }, + wantAfterJSON: 1, + wantAfterProto: 0, + }, + } + + const ( + owner = "owner1" + priority = int32(5) + ) + + ctx := context.TODO() + mockCtrl := gomock.NewController(t) + + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // ── Step 1: JSON import ────────────────────────────────────────── + root, sharedPool := importDeviceJSON(t, ctx, scb, tt.req) + + // ── Step 2: validate after JSON import ────────────────────────── + resultJSON, _ := validation.Validate(ctx, root.Entry, validationConfig, sharedPool) + t.Logf("Validation errors after JSON import:\n%s", strings.Join(resultJSON.ErrorsStr(), "\n")) + if got := len(resultJSON.ErrorsStr()); got != tt.wantAfterJSON { + t.Errorf("after JSON import: expected %d error(s), got %d: %v", + tt.wantAfterJSON, got, resultJSON.ErrorsStr()) + } + + // ── Step 3: export to proto, reload ──────────────────────────── + persisted, err := ops.TreeExport(root.Entry, owner, priority, false) + if err != nil { + t.Fatal(err) + } + + tc2 := tree.NewTreeContext(scb, pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0))) + root2, err := tree.NewTreeRoot(ctx, tc2) + if err != nil { + t.Fatal(err) + } + + sharedPool2 := pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0)) + if _, err = root2.ImportConfig(ctx, &sdcpb.Path{}, + proto_importer.NewProtoTreeImporter(persisted), + types.NewUpdateInsertFlags(), sharedPool2); err != nil { + t.Fatal(err) + } + if err = root2.FinishInsertionPhase(ctx); err != nil { + t.Fatal(err) + } + + // ── Step 4: validate after proto round-trip ───────────────────── + resultProto, _ := validation.Validate(ctx, root2.Entry, validationConfig, sharedPool2) + t.Logf("Validation errors after proto round-trip:\n%s", strings.Join(resultProto.ErrorsStr(), "\n")) + if got := len(resultProto.ErrorsStr()); got != tt.wantAfterProto { + t.Errorf("after proto round-trip: expected %d error(s), got %d: %v", + tt.wantAfterProto, got, resultProto.ErrorsStr()) + } + }) + } +} diff --git a/pkg/tree/ops/validation/validation_union_range_test.go b/pkg/tree/ops/validation/validation_union_range_test.go new file mode 100644 index 00000000..959127fc --- /dev/null +++ b/pkg/tree/ops/validation/validation_union_range_test.go @@ -0,0 +1,64 @@ +package validation_test + +import ( + "context" + "strings" + "testing" + + "github.com/sdcio/data-server/pkg/tree/ops/validation" + "github.com/sdcio/data-server/pkg/utils/testhelper" + sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" + "go.uber.org/mock/gomock" +) + +func TestValidate_Range_Union(t *testing.T) { + tests := []struct { + name string + req *sdcio_schema.Device + wantErrors int + }{ + { + name: "union leaf - uint32 branch value within range", + req: &sdcio_schema.Device{ + Unionrangetest: &sdcio_schema.SdcioModel_Unionrangetest_Union_Uint32{Uint32: 100}, + }, + wantErrors: 0, + }, + { + name: "union leaf - uint32 branch value out of range", + req: &sdcio_schema.Device{ + Unionrangetest: &sdcio_schema.SdcioModel_Unionrangetest_Union_Uint32{Uint32: 5}, + }, + wantErrors: 1, + }, + { + name: "union leaf - string branch has no range constraint", + req: &sdcio_schema.Device{ + Unionrangetest: &sdcio_schema.SdcioModel_Unionrangetest_Union_String{String: "hello"}, + }, + wantErrors: 0, + }, + } + + ctx := context.TODO() + mockCtrl := gomock.NewController(t) + + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root, sharedPool := importDeviceJSON(t, ctx, scb, tt.req) + + validationResult, _ := validation.Validate(ctx, root.Entry, validationConfig, sharedPool) + + t.Logf("Validation Errors:\n%s", strings.Join(validationResult.ErrorsStr(), "\n")) + + if len(validationResult.ErrorsStr()) != tt.wantErrors { + t.Errorf("expected %d error(s), got %d: %v", tt.wantErrors, len(validationResult.ErrorsStr()), validationResult.ErrorsStr()) + } + }) + } +} diff --git a/pkg/tree/processors/processor_importer.go b/pkg/tree/processors/processor_importer.go index f8c6daf8..ae360437 100644 --- a/pkg/tree/processors/processor_importer.go +++ b/pkg/tree/processors/processor_importer.go @@ -152,11 +152,11 @@ func (task importConfigTask) Run(ctx context.Context, submit func(pool.Task) err return nil case *sdcpb.SchemaElem_Field: - tv, err := task.importerElement.GetTVValue(ctx, x.Field.GetType()) + tv, matchedType, err := task.importerElement.GetTVValue(ctx, x.Field.GetType()) if err != nil { return err } - upd := types.NewUpdate(task.entry, tv, task.context.intentPrio, task.context.intentName, 0) + upd := types.NewUpdate(task.entry, tv, task.context.intentPrio, task.context.intentName, 0).WithMatchedType(matchedType) task.entry.GetLeafVariants().AddWithStats(api.NewLeafEntry(upd, task.context.insertFlags, task.entry), task.context.stats) return nil @@ -189,7 +189,7 @@ func (task importConfigTask) Run(ctx context.Context, submit func(pool.Task) err scalarArr = &sdcpb.ScalarArray{Element: []*sdcpb.TypedValue{}} } - tv, err := task.importerElement.GetTVValue(ctx, x.Leaflist.GetType()) + tv, _, err := task.importerElement.GetTVValue(ctx, x.Leaflist.GetType()) if err != nil { return err } diff --git a/pkg/tree/types/update.go b/pkg/tree/types/update.go index a03245ae..03360398 100644 --- a/pkg/tree/types/update.go +++ b/pkg/tree/types/update.go @@ -11,11 +11,12 @@ import ( ) type Update struct { - value *sdcpb.TypedValue - priority int32 - intentName string - timestamp int64 - parent UpdateParent + value *sdcpb.TypedValue + priority int32 + intentName string + timestamp int64 + parent UpdateParent + matchedUnionType *sdcpb.SchemaLeafType } func NewUpdateFromSdcpbUpdate(parent UpdateParent, u *sdcpb.Update, prio int32, intent string, ts int64) *Update { @@ -44,11 +45,12 @@ func (u *Update) DeepCopy() *Update { clonedVal := proto.Clone(u.Value()).(*sdcpb.TypedValue) return &Update{ - value: clonedVal, - priority: u.Priority(), - intentName: u.intentName, - timestamp: u.timestamp, - parent: u.parent, + value: clonedVal, + priority: u.Priority(), + intentName: u.intentName, + timestamp: u.timestamp, + parent: u.parent, + matchedUnionType: u.matchedUnionType, } } @@ -85,6 +87,23 @@ func (u *Update) Value() *sdcpb.TypedValue { return u.value } +// EffectiveLeafType returns the matched union branch type if set, +// otherwise returns the fallback (the outer schema type). Used by +// all validators to enforce branch-specific constraints. +func (u *Update) EffectiveLeafType(fallback *sdcpb.SchemaLeafType) *sdcpb.SchemaLeafType { + if u.matchedUnionType != nil { + return u.matchedUnionType + } + return fallback +} + +// WithMatchedType stores the matched union branch type on the Update and +// returns the receiver for fluent chaining. +func (u *Update) WithMatchedType(t *sdcpb.SchemaLeafType) *Update { + u.matchedUnionType = t + return u +} + func (u *Update) ValueAsBytes() ([]byte, error) { return proto.Marshal(u.value) } diff --git a/pkg/tree/types/update_test.go b/pkg/tree/types/update_test.go new file mode 100644 index 00000000..aa595eac --- /dev/null +++ b/pkg/tree/types/update_test.go @@ -0,0 +1,68 @@ +package types + +import ( + "testing" + + sdcpb "github.com/sdcio/sdc-protos/sdcpb" +) + +func TestEffectiveLeafType_ReturnsFallback_WhenMatchedTypeNil(t *testing.T) { + u := NewUpdate(nil, &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foo"}}, 0, "owner", 0) + fallback := &sdcpb.SchemaLeafType{TypeName: "string"} + + result := u.EffectiveLeafType(fallback) + + if result != fallback { + t.Errorf("expected fallback SchemaLeafType, got %v", result) + } +} + +func TestEffectiveLeafType_ReturnsMatchedType_WhenSet(t *testing.T) { + u := NewUpdate(nil, &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foo"}}, 0, "owner", 0) + matched := &sdcpb.SchemaLeafType{TypeName: "uint32"} + fallback := &sdcpb.SchemaLeafType{TypeName: "string"} + + u.WithMatchedType(matched) + result := u.EffectiveLeafType(fallback) + + if result != matched { + t.Errorf("expected matched SchemaLeafType, got %v", result) + } +} + +func TestWithMatchedType_IsFluent(t *testing.T) { + u := NewUpdate(nil, &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foo"}}, 0, "owner", 0) + matched := &sdcpb.SchemaLeafType{TypeName: "uint32"} + + returned := u.WithMatchedType(matched) + + if returned != u { + t.Errorf("WithMatchedType should return the same Update for chaining") + } +} + +func TestDeepCopy_PreservesMatchedType(t *testing.T) { + matched := &sdcpb.SchemaLeafType{TypeName: "uint32"} + u := NewUpdate(nil, &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foo"}}, 0, "owner", 0). + WithMatchedType(matched) + + copied := u.DeepCopy() + + if copied.EffectiveLeafType(nil) != matched { + t.Errorf("DeepCopy should preserve the matchedUnionType pointer") + } +} + +func TestEqual_IgnoresMatchedType(t *testing.T) { + tv := &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foo"}} + withMatched := NewUpdate(nil, tv, 0, "owner", 0). + WithMatchedType(&sdcpb.SchemaLeafType{TypeName: "uint32"}) + withoutMatched := NewUpdate(nil, tv, 0, "owner", 0) + + if !withMatched.Equal(withoutMatched) { + t.Errorf("Equal should return true regardless of matchedUnionType when TypedValue bytes match") + } + if !withoutMatched.Equal(withMatched) { + t.Errorf("Equal should be symmetric regardless of matchedUnionType") + } +} diff --git a/tests/schema/sdcio_model.yang b/tests/schema/sdcio_model.yang index f6f03ceb..f038c738 100644 --- a/tests/schema/sdcio_model.yang +++ b/tests/schema/sdcio_model.yang @@ -91,4 +91,36 @@ module sdcio_model { range "10..300 | 5000..5020 | 9999"; } } + leaf unionpatterntest { + type union { + type string { + pattern 'hallo [0-9a-fA-F]*'; + } + type uint32; + } + } + leaf unionlengthtest { + type union { + type string { + length "7..10"; + } + type uint32; + } + } + leaf unionrangetest { + type union { + type uint32 { + range "10..300"; + } + type string; + } + } + leaf unionleafreftest { + type union { + type leafref { + path "/interface/name"; + } + type string; + } + } } diff --git a/tests/sdcioygot/sdcio_schema.go b/tests/sdcioygot/sdcio_schema.go index 0b9184a9..23a159bc 100644 --- a/tests/sdcioygot/sdcio_schema.go +++ b/tests/sdcioygot/sdcio_schema.go @@ -128,6 +128,10 @@ type Device struct { RangetestLeaflist []uint32 `path:"rangetestLeaflist" module:"sdcio_model"` Rangetestsigned *int32 `path:"rangetestsigned" module:"sdcio_model"` Rangetestunsigned *uint32 `path:"rangetestunsigned" module:"sdcio_model"` + Unionleafreftest *string `path:"unionleafreftest" module:"sdcio_model"` + Unionlengthtest SdcioModel_Unionlengthtest_Union `path:"unionlengthtest" module:"sdcio_model"` + Unionpatterntest SdcioModel_Unionpatterntest_Union `path:"unionpatterntest" module:"sdcio_model"` + Unionrangetest SdcioModel_Unionrangetest_Union `path:"unionrangetest" module:"sdcio_model"` } // IsYANGGoStruct ensures that Device implements the yang.GoStruct @@ -316,6 +320,126 @@ func (*Device) ΛBelongingModule() string { return "" } +// SdcioModel_Unionlengthtest_Union is an interface that is implemented by valid types for the union +// for the leaf /sdcio_model/unionlengthtest within the YANG schema. +type SdcioModel_Unionlengthtest_Union interface { + Is_SdcioModel_Unionlengthtest_Union() +} + +// SdcioModel_Unionlengthtest_Union_String is used when /sdcio_model/unionlengthtest +// is to be set to a string value. +type SdcioModel_Unionlengthtest_Union_String struct { + String string +} + +// Is_SdcioModel_Unionlengthtest_Union ensures that SdcioModel_Unionlengthtest_Union_String +// implements the SdcioModel_Unionlengthtest_Union interface. +func (*SdcioModel_Unionlengthtest_Union_String) Is_SdcioModel_Unionlengthtest_Union() {} + +// SdcioModel_Unionlengthtest_Union_Uint32 is used when /sdcio_model/unionlengthtest +// is to be set to a uint32 value. +type SdcioModel_Unionlengthtest_Union_Uint32 struct { + Uint32 uint32 +} + +// Is_SdcioModel_Unionlengthtest_Union ensures that SdcioModel_Unionlengthtest_Union_Uint32 +// implements the SdcioModel_Unionlengthtest_Union interface. +func (*SdcioModel_Unionlengthtest_Union_Uint32) Is_SdcioModel_Unionlengthtest_Union() {} + +// To_SdcioModel_Unionlengthtest_Union takes an input interface{} and attempts to convert it to a struct +// which implements the SdcioModel_Unionlengthtest_Union union. It returns an error if the interface{} supplied +// cannot be converted to a type within the union. +func (t *Device) To_SdcioModel_Unionlengthtest_Union(i interface{}) (SdcioModel_Unionlengthtest_Union, error) { + switch v := i.(type) { + case string: + return &SdcioModel_Unionlengthtest_Union_String{v}, nil + case uint32: + return &SdcioModel_Unionlengthtest_Union_Uint32{v}, nil + default: + return nil, fmt.Errorf("cannot convert %v to SdcioModel_Unionlengthtest_Union, unknown union type, got: %T, want any of [string, uint32]", i, i) + } +} + +// SdcioModel_Unionpatterntest_Union is an interface that is implemented by valid types for the union +// for the leaf /sdcio_model/unionpatterntest within the YANG schema. +type SdcioModel_Unionpatterntest_Union interface { + Is_SdcioModel_Unionpatterntest_Union() +} + +// SdcioModel_Unionpatterntest_Union_String is used when /sdcio_model/unionpatterntest +// is to be set to a string value. +type SdcioModel_Unionpatterntest_Union_String struct { + String string +} + +// Is_SdcioModel_Unionpatterntest_Union ensures that SdcioModel_Unionpatterntest_Union_String +// implements the SdcioModel_Unionpatterntest_Union interface. +func (*SdcioModel_Unionpatterntest_Union_String) Is_SdcioModel_Unionpatterntest_Union() {} + +// SdcioModel_Unionpatterntest_Union_Uint32 is used when /sdcio_model/unionpatterntest +// is to be set to a uint32 value. +type SdcioModel_Unionpatterntest_Union_Uint32 struct { + Uint32 uint32 +} + +// Is_SdcioModel_Unionpatterntest_Union ensures that SdcioModel_Unionpatterntest_Union_Uint32 +// implements the SdcioModel_Unionpatterntest_Union interface. +func (*SdcioModel_Unionpatterntest_Union_Uint32) Is_SdcioModel_Unionpatterntest_Union() {} + +// To_SdcioModel_Unionpatterntest_Union takes an input interface{} and attempts to convert it to a struct +// which implements the SdcioModel_Unionpatterntest_Union union. It returns an error if the interface{} supplied +// cannot be converted to a type within the union. +func (t *Device) To_SdcioModel_Unionpatterntest_Union(i interface{}) (SdcioModel_Unionpatterntest_Union, error) { + switch v := i.(type) { + case string: + return &SdcioModel_Unionpatterntest_Union_String{v}, nil + case uint32: + return &SdcioModel_Unionpatterntest_Union_Uint32{v}, nil + default: + return nil, fmt.Errorf("cannot convert %v to SdcioModel_Unionpatterntest_Union, unknown union type, got: %T, want any of [string, uint32]", i, i) + } +} + +// SdcioModel_Unionrangetest_Union is an interface that is implemented by valid types for the union +// for the leaf /sdcio_model/unionrangetest within the YANG schema. +type SdcioModel_Unionrangetest_Union interface { + Is_SdcioModel_Unionrangetest_Union() +} + +// SdcioModel_Unionrangetest_Union_String is used when /sdcio_model/unionrangetest +// is to be set to a string value. +type SdcioModel_Unionrangetest_Union_String struct { + String string +} + +// Is_SdcioModel_Unionrangetest_Union ensures that SdcioModel_Unionrangetest_Union_String +// implements the SdcioModel_Unionrangetest_Union interface. +func (*SdcioModel_Unionrangetest_Union_String) Is_SdcioModel_Unionrangetest_Union() {} + +// SdcioModel_Unionrangetest_Union_Uint32 is used when /sdcio_model/unionrangetest +// is to be set to a uint32 value. +type SdcioModel_Unionrangetest_Union_Uint32 struct { + Uint32 uint32 +} + +// Is_SdcioModel_Unionrangetest_Union ensures that SdcioModel_Unionrangetest_Union_Uint32 +// implements the SdcioModel_Unionrangetest_Union interface. +func (*SdcioModel_Unionrangetest_Union_Uint32) Is_SdcioModel_Unionrangetest_Union() {} + +// To_SdcioModel_Unionrangetest_Union takes an input interface{} and attempts to convert it to a struct +// which implements the SdcioModel_Unionrangetest_Union union. It returns an error if the interface{} supplied +// cannot be converted to a type within the union. +func (t *Device) To_SdcioModel_Unionrangetest_Union(i interface{}) (SdcioModel_Unionrangetest_Union, error) { + switch v := i.(type) { + case string: + return &SdcioModel_Unionrangetest_Union_String{v}, nil + case uint32: + return &SdcioModel_Unionrangetest_Union_Uint32{v}, nil + default: + return nil, fmt.Errorf("cannot convert %v to SdcioModel_Unionrangetest_Union, unknown union type, got: %T, want any of [string, uint32]", i, i) + } +} + // SdcioModel_Choices represents the /sdcio_model/choices YANG schema element. type SdcioModel_Choices struct { @@ -2599,10 +2723,23 @@ var ( 0x50, 0x6f, 0x7f, 0x43, 0xd8, 0x8f, 0xd1, 0xaf, 0xfd, 0x8a, 0x5d, 0x1e, 0xd7, 0x8a, 0x9d, 0x77, 0x6b, 0xbf, 0x62, 0x9d, 0xa3, 0x5a, 0x30, 0x15, 0x0c, 0xed, 0xc5, 0x39, 0xa3, 0xbb, 0x34, 0x0b, 0x1b, 0xec, 0xd4, 0x44, 0x4d, 0x95, 0x5b, 0xa3, 0xd8, 0x0c, 0x25, 0xc0, 0x8a, 0xcd, 0x50, 0x6c, - 0x86, 0x62, 0x33, 0x0e, 0x90, 0xcd, 0x78, 0x95, 0xfc, 0x69, 0x35, 0x17, 0x79, 0x99, 0xcb, 0x86, - 0xe5, 0xfd, 0x6a, 0x7c, 0xc5, 0x9f, 0x1d, 0x27, 0x6d, 0x41, 0xb6, 0xb3, 0x99, 0x8d, 0xf8, 0x9f, - 0x12, 0x99, 0xca, 0x5f, 0xf0, 0x93, 0xb5, 0xce, 0x4a, 0xbe, 0xbc, 0x7a, 0xf9, 0x3f, 0x00, 0x00, - 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x31, 0x8e, 0x68, 0x02, 0xca, 0xf3, 0x01, 0x00, + 0x86, 0x62, 0x33, 0x0e, 0x90, 0xcd, 0xc8, 0x34, 0x98, 0x0b, 0xdb, 0x72, 0xec, 0x29, 0x36, 0x46, + 0x2e, 0x1e, 0xd1, 0x33, 0x82, 0xa9, 0x96, 0xca, 0x5c, 0x96, 0x69, 0x2e, 0xfd, 0xd9, 0xa6, 0x65, + 0x05, 0x33, 0xe0, 0x47, 0x3f, 0x5e, 0x04, 0xd8, 0xbf, 0xb2, 0x5a, 0x5d, 0xc8, 0xde, 0x15, 0x4a, + 0x66, 0xb0, 0xf1, 0x69, 0x9d, 0xe5, 0xb6, 0x6c, 0x82, 0xdd, 0x91, 0x31, 0xc4, 0xc1, 0xae, 0xfa, + 0x86, 0x24, 0x75, 0xc3, 0xcc, 0x9c, 0xa6, 0x33, 0xa8, 0xf2, 0x05, 0xc6, 0x1e, 0x93, 0x09, 0x48, + 0x5e, 0xa2, 0x86, 0x4a, 0x5c, 0x0e, 0x4d, 0x5c, 0x38, 0x00, 0x48, 0x69, 0xc3, 0x48, 0xe5, 0xb3, + 0xfb, 0xc3, 0x65, 0x87, 0x53, 0x96, 0x0c, 0x78, 0x38, 0x5f, 0xf8, 0xb8, 0x35, 0xff, 0xf1, 0x6a, + 0xc8, 0x71, 0x65, 0x9e, 0x23, 0xe8, 0x60, 0x3f, 0x7f, 0x1f, 0x63, 0x15, 0xdc, 0xc4, 0xdb, 0x97, + 0xac, 0x4b, 0x73, 0xa3, 0x33, 0x40, 0x94, 0x06, 0x8c, 0xd6, 0xca, 0x06, 0xb2, 0x7e, 0x3c, 0x40, + 0xe6, 0xad, 0x9d, 0x50, 0x5f, 0x40, 0x8b, 0x9b, 0x68, 0xd0, 0x2e, 0xb7, 0x54, 0x4b, 0x65, 0xa4, + 0x95, 0x91, 0xce, 0x71, 0x69, 0x41, 0x27, 0x9b, 0x41, 0xdb, 0xb2, 0x94, 0x12, 0x57, 0x4a, 0x5c, + 0x29, 0x71, 0x88, 0x12, 0x8f, 0x38, 0x7a, 0x86, 0x0a, 0xdf, 0xb4, 0x53, 0x0a, 0xfc, 0xd0, 0x14, + 0xf8, 0x01, 0x68, 0xb5, 0xce, 0x11, 0xa9, 0x35, 0x66, 0x22, 0xe2, 0x88, 0xa3, 0xac, 0x1d, 0x32, + 0x56, 0xaf, 0x62, 0x3d, 0xcd, 0x3b, 0x9c, 0xd2, 0xb0, 0xbc, 0x5f, 0x8d, 0xaf, 0xf8, 0xb3, 0xe3, + 0xa4, 0x15, 0xcc, 0xf6, 0x81, 0x95, 0x46, 0xfc, 0x4f, 0x89, 0xc3, 0x28, 0xbf, 0xe0, 0x27, 0x6b, + 0x7d, 0xf0, 0xe4, 0xe5, 0xd5, 0xcb, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0xff, + 0xff, 0x8b, 0x36, 0x32, 0x25, 0xad, 0x09, 0x02, 0x00, } ) From 8178231c8b297b7aebf7999991dfcee50f244597 Mon Sep 17 00:00:00 2001 From: steiler Date: Mon, 4 May 2026 12:45:57 +0200 Subject: [PATCH 2/4] update Co-authored-by: Copilot --- .../validation_test_helpers_test.go | 22 +++---------------- 1 file changed, 3 insertions(+), 19 deletions(-) diff --git a/pkg/tree/ops/validation/validation_test_helpers_test.go b/pkg/tree/ops/validation/validation_test_helpers_test.go index d76743a1..49cca743 100644 --- a/pkg/tree/ops/validation/validation_test_helpers_test.go +++ b/pkg/tree/ops/validation/validation_test_helpers_test.go @@ -1,20 +1,16 @@ -package validation package validation_test import ( "context" - "encoding/json" "runtime" "testing" - "github.com/openconfig/ygot/ygot" schemaClient "github.com/sdcio/data-server/pkg/datastore/clients/schema" "github.com/sdcio/data-server/pkg/pool" "github.com/sdcio/data-server/pkg/tree" - json_importer "github.com/sdcio/data-server/pkg/tree/importer/json" "github.com/sdcio/data-server/pkg/tree/types" + "github.com/sdcio/data-server/pkg/utils/testhelper" sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" - sdcpb "github.com/sdcio/sdc-protos/sdcpb" ) // importDeviceJSON creates a fresh tree, imports req via JSON (owner "owner1", priority 5), @@ -29,20 +25,7 @@ func importDeviceJSON(t *testing.T, ctx context.Context, scb schemaClient.Schema t.Fatal(err) } - configJSON, err := ygot.EmitJSON(req, &ygot.EmitJSONConfig{Format: ygot.RFC7951, SkipValidation: true}) - if err != nil { - t.Fatal(err) - } - - var jsonConfig any - if err = json.Unmarshal([]byte(configJSON), &jsonConfig); err != nil { - t.Fatal(err) - } - - jimporter := json_importer.NewJsonTreeImporter(jsonConfig, "owner1", 5, false) - sharedPool := pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0)) - - if _, err = root.ImportConfig(ctx, &sdcpb.Path{}, jimporter, types.NewUpdateInsertFlags(), sharedPool); err != nil { + if _, err = testhelper.LoadYgotStructIntoTreeRoot(ctx, req, root.Entry, "owner1", 5, false, types.NewUpdateInsertFlags()); err != nil { t.Fatal(err) } @@ -50,5 +33,6 @@ func importDeviceJSON(t *testing.T, ctx context.Context, scb schemaClient.Schema t.Fatal(err) } + sharedPool := pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0)) return root, sharedPool } From e1f9acee19e42d1da473a6c44ecc3693822754cd Mon Sep 17 00:00:00 2001 From: steiler Date: Tue, 12 May 2026 16:53:37 +0200 Subject: [PATCH 3/4] Union validation: XPath leafref, tests, and PR notes Apply EffectiveLeafType in navigateLeafRef so FollowLeafRef matches validateLeafRefs for union+leafref leaves. Document leaf-list union range limitation. Add optional-instance union leafref tests, internal navigateLeafRef/FollowLeafRef test, and Update Equal/String coverage. Regenerate sdcio ygot for unionoptionalleafreftest. Refs: https://github.com/sdcio/data-server/issues/177 Co-authored-by: Cursor --- docs/pr-423-supplement.txt | 14 + .../navigate_leafref_union_internal_test.go | 79 ++ .../validation/validation_entry_leafref.go | 33 +- .../ops/validation/validation_entry_range.go | 2 + .../validation_union_leafref_optional_test.go | 76 ++ pkg/tree/types/update_test.go | 27 + tests/schema/sdcio_model.yang | 9 + tests/sdcioygot/sdcio_schema.go | 843 +++++++++--------- 8 files changed, 656 insertions(+), 427 deletions(-) create mode 100644 docs/pr-423-supplement.txt create mode 100644 pkg/tree/ops/validation/navigate_leafref_union_internal_test.go create mode 100644 pkg/tree/ops/validation/validation_union_leafref_optional_test.go diff --git a/docs/pr-423-supplement.txt b/docs/pr-423-supplement.txt new file mode 100644 index 00000000..e8c73cdd --- /dev/null +++ b/docs/pr-423-supplement.txt @@ -0,0 +1,14 @@ +Paste into GitHub PR #423 description (append or merge with existing text). + +--- + +Closes https://github.com/sdcio/data-server/issues/177 (when merged). + +Follow-ups in this branch: +- navigateLeafRef / FollowLeafRef now use EffectiveLeafType so XPath and must evaluation agree with validateLeafRefs for union leaves whose matched branch is a leafref. +- Leaf-list range validation still uses the outer member type only; per-element union branch metadata is not modeled (see comment in validation_entry_range.go). +- Proto-imported config cannot recover which union branch matched; validators fall back to the outer union type (see validation_union_proto_fallback_test.go). Documented for reviewers. + +Tests added: union optional-instance leafref (warning vs error), navigateLeafRef + FollowLeafRef on union leafref, Update.Equal / String edge cases. + +Note: Regex compile-error path in validatePattern remains difficult to exercise with pyang-valid YANG patterns; consider a targeted refactor if coverage is required. diff --git a/pkg/tree/ops/validation/navigate_leafref_union_internal_test.go b/pkg/tree/ops/validation/navigate_leafref_union_internal_test.go new file mode 100644 index 00000000..623e9a56 --- /dev/null +++ b/pkg/tree/ops/validation/navigate_leafref_union_internal_test.go @@ -0,0 +1,79 @@ +package validation + +import ( + "context" + "runtime" + "testing" + + "github.com/openconfig/ygot/ygot" + "github.com/sdcio/data-server/pkg/pool" + "github.com/sdcio/data-server/pkg/tree" + "github.com/sdcio/data-server/pkg/tree/ops" + "github.com/sdcio/data-server/pkg/tree/types" + "github.com/sdcio/data-server/pkg/utils/testhelper" + sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" + sdcpb "github.com/sdcio/sdc-protos/sdcpb" + "go.uber.org/mock/gomock" +) + +func TestNavigateLeafRef_unionMatchedBranch(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + tc := tree.NewTreeContext(scb, pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0))) + root, err := tree.NewTreeRoot(ctx, tc) + if err != nil { + t.Fatal(err) + } + + req := &sdcio_schema.Device{ + Interface: map[string]*sdcio_schema.SdcioModel_Interface{ + "ethernet-1/1": { + Name: ygot.String("ethernet-1/1"), + }, + }, + Unionleafreftest: ygot.String("ethernet-1/1"), + } + if _, err := testhelper.LoadYgotStructIntoTreeRoot(ctx, req, root.Entry, "owner1", 5, false, types.NewUpdateInsertFlags()); err != nil { + t.Fatal(err) + } + if err := root.FinishInsertionPhase(ctx); err != nil { + t.Fatal(err) + } + + path := &sdcpb.Path{Elem: []*sdcpb.PathElem{sdcpb.NewPathElem("unionleafreftest", nil)}} + entry, err := ops.NavigateSdcpbPath(ctx, root.Entry, path) + if err != nil { + t.Fatal(err) + } + + entries, err := navigateLeafRef(ctx, entry) + if err != nil { + t.Fatalf("navigateLeafRef: %v", err) + } + if len(entries) != 1 { + t.Fatalf("navigateLeafRef: want 1 target, got %d", len(entries)) + } + last := entries[0].SdcpbPath().GetElem() + if len(last) == 0 || last[len(last)-1].GetName() != "name" { + t.Fatalf("navigateLeafRef: want leaf name, last elem = %v", last) + } + + adapter := newYangParserEntryAdapter(ctx, entry) + next, err := adapter.FollowLeafRef() + if err != nil { + t.Fatalf("FollowLeafRef: %v", err) + } + nae, ok := next.(*yangParserEntryAdapter) + if !ok { + t.Fatalf("FollowLeafRef: got %T", next) + } + el := nae.e.SdcpbPath().GetElem() + if len(el) == 0 || el[len(el)-1].GetName() != "name" { + t.Fatalf("FollowLeafRef: want leaf name, last elem = %v", el) + } +} diff --git a/pkg/tree/ops/validation/validation_entry_leafref.go b/pkg/tree/ops/validation/validation_entry_leafref.go index 47aa2179..db2b73fc 100644 --- a/pkg/tree/ops/validation/validation_entry_leafref.go +++ b/pkg/tree/ops/validation/validation_entry_leafref.go @@ -93,7 +93,9 @@ func breadthSearch(ctx context.Context, e api.Entry, sdcpbPath *sdcpb.Path) ([]a return resultEntries, nil } -// navigateLeafRef +// navigateLeafRef resolves a leafref on the current entry, including when the schema type is a +// union whose matched branch (see types.Update.EffectiveLeafType) is the leafref. This keeps +// XPath/must evaluation (FollowLeafRef) aligned with validateLeafRefs. func navigateLeafRef(ctx context.Context, e api.Entry) ([]api.Entry, error) { // leafref path takes as an argument a string that MUST refer to a leaf or leaf-list node. @@ -108,13 +110,30 @@ func navigateLeafRef(ctx context.Context, e api.Entry) ([]api.Entry, error) { // If /bar/baz resolves to a leaf-list, then foo must have a value equal to one of the existing entries in that leaf-list. // Thus, the "pointer" is not to the entire leaf-list node, but to one instance of it, selected by value. + if e.GetSchema() == nil { + return nil, fmt.Errorf("error not a leafref %s", e.SdcpbPath()) + } + + lv := e.GetLeafVariants().GetHighestPrecedence(false, true, false) + var lref string - switch { - case e.GetSchema().GetField().GetType().GetLeafref() != "": - lref = e.GetSchema().GetField().GetType().GetLeafref() - case e.GetSchema().GetLeaflist().GetType().GetLeafref() != "": - lref = e.GetSchema().GetLeaflist().GetType().GetLeafref() - default: + if field := e.GetSchema().GetField(); field != nil { + effectiveType := field.GetType() + if lv != nil { + effectiveType = lv.Update.EffectiveLeafType(field.GetType()) + } + lref = effectiveType.GetLeafref() + } + if lref == "" { + if ll := e.GetSchema().GetLeaflist(); ll != nil { + effectiveType := ll.GetType() + if lv != nil { + effectiveType = lv.Update.EffectiveLeafType(ll.GetType()) + } + lref = effectiveType.GetLeafref() + } + } + if lref == "" { return nil, fmt.Errorf("error not a leafref %s", e.SdcpbPath()) } diff --git a/pkg/tree/ops/validation/validation_entry_range.go b/pkg/tree/ops/validation/validation_entry_range.go index 6fc0ccda..39ff6ced 100644 --- a/pkg/tree/ops/validation/validation_entry_range.go +++ b/pkg/tree/ops/validation/validation_entry_range.go @@ -41,6 +41,8 @@ func validateRange(_ context.Context, e api.Entry, resultChan chan<- *types.Vali // if it is a leaflist, extract the values them to the tvs slice and check them further down tvs = tv.GetLeaflistVal().GetElement() // we also need the Field/Leaf Type schema + // Union member types on leaf-list entries are not modeled per element (single matchedUnionType + // on the enclosing Update); range for union-typed leaf-lists would need richer metadata. typeSchema = e.GetSchema().GetLeaflist().GetType() default: // if no ranges exist return diff --git a/pkg/tree/ops/validation/validation_union_leafref_optional_test.go b/pkg/tree/ops/validation/validation_union_leafref_optional_test.go new file mode 100644 index 00000000..1d1d46b9 --- /dev/null +++ b/pkg/tree/ops/validation/validation_union_leafref_optional_test.go @@ -0,0 +1,76 @@ +package validation_test + +import ( + "context" + "strings" + "testing" + + "github.com/openconfig/ygot/ygot" + "github.com/sdcio/data-server/pkg/config" + "github.com/sdcio/data-server/pkg/tree/ops/validation" + "github.com/sdcio/data-server/pkg/utils/testhelper" + sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" + "go.uber.org/mock/gomock" +) + +func TestValidate_LeafRef_UnionOptionalInstance(t *testing.T) { + tests := []struct { + name string + req *sdcio_schema.Device + wantErrors int + wantWarnings int + }{ + { + name: "optional union leafref - missing target issues warning not error", + req: &sdcio_schema.Device{ + Unionoptionalleafreftest: ygot.String("ethernet-1/1"), + }, + wantErrors: 0, + wantWarnings: 1, + }, + { + name: "optional union leafref - target exists, no warning", + req: &sdcio_schema.Device{ + Interface: map[string]*sdcio_schema.SdcioModel_Interface{ + "ethernet-1/1": { + Name: ygot.String("ethernet-1/1"), + }, + }, + Unionoptionalleafreftest: ygot.String("ethernet-1/1"), + }, + wantErrors: 0, + wantWarnings: 0, + }, + } + + ctx := context.TODO() + mockCtrl := gomock.NewController(t) + + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + valConf := config.NewValidationConfig() + valConf.DisabledValidators.DisableAll() + valConf.DisabledValidators.Leafref = false + valConf.SetDisableConcurrency(true) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + root, sharedPool := importDeviceJSON(t, ctx, scb, tt.req) + + validationResult, _ := validation.Validate(ctx, root.Entry, valConf, sharedPool) + + t.Logf("Errors:\n%s", strings.Join(validationResult.ErrorsStr(), "\n")) + t.Logf("Warnings:\n%s", strings.Join(validationResult.WarningsStr(), "\n")) + + if len(validationResult.ErrorsStr()) != tt.wantErrors { + t.Errorf("errors: want %d, got %d: %v", tt.wantErrors, len(validationResult.ErrorsStr()), validationResult.ErrorsStr()) + } + if len(validationResult.WarningsStr()) != tt.wantWarnings { + t.Errorf("warnings: want %d, got %d: %v", tt.wantWarnings, len(validationResult.WarningsStr()), validationResult.WarningsStr()) + } + }) + } +} diff --git a/pkg/tree/types/update_test.go b/pkg/tree/types/update_test.go index aa595eac..abfe6669 100644 --- a/pkg/tree/types/update_test.go +++ b/pkg/tree/types/update_test.go @@ -1,6 +1,7 @@ package types import ( + "strings" "testing" sdcpb "github.com/sdcio/sdc-protos/sdcpb" @@ -66,3 +67,29 @@ func TestEqual_IgnoresMatchedType(t *testing.T) { t.Errorf("Equal should be symmetric regardless of matchedUnionType") } } + +func TestEqual_falseWhenOwnerDiffers(t *testing.T) { + tv := &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foo"}} + a := NewUpdate(nil, tv, 0, "a", 0) + b := NewUpdate(nil, tv, 0, "b", 0) + if a.Equal(b) { + t.Error("Equal should be false when intentName differs") + } +} + +func TestEqual_falseWhenPriorityDiffers(t *testing.T) { + tv := &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foo"}} + a := NewUpdate(nil, tv, 1, "owner", 0) + b := NewUpdate(nil, tv, 2, "owner", 0) + if a.Equal(b) { + t.Error("Equal should be false when priority differs") + } +} + +func TestString_nilParentPathPlaceholder(t *testing.T) { + u := NewUpdate(nil, &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "x"}}, 0, "o", 0) + s := u.String() + if !strings.Contains(s, "") { + t.Errorf("String should show path when parent nil: %q", s) + } +} diff --git a/tests/schema/sdcio_model.yang b/tests/schema/sdcio_model.yang index f038c738..a6ad047d 100644 --- a/tests/schema/sdcio_model.yang +++ b/tests/schema/sdcio_model.yang @@ -123,4 +123,13 @@ module sdcio_model { type string; } } + leaf unionoptionalleafreftest { + type union { + type leafref { + path "/interface/name"; + require-instance false; + } + type string; + } + } } diff --git a/tests/sdcioygot/sdcio_schema.go b/tests/sdcioygot/sdcio_schema.go index 23a159bc..d55f6ce0 100644 --- a/tests/sdcioygot/sdcio_schema.go +++ b/tests/sdcioygot/sdcio_schema.go @@ -4,7 +4,7 @@ of structs which represent a YANG schema. The generated schema can be compressed by a series of transformations (compression was false in this case). -This package was generated by /home/mava/go/pkg/mod/github.com/openconfig/ygot@v0.34.0/genutil/names.go +This package was generated by /tmp/cursor-sandbox-cache/05aff03e847bb6f36f1d71a0f9d279f8/go-mod/github.com/openconfig/ygot@v0.34.0/genutil/names.go using the following YANG input files: - ./tests/schema/sdcio_model.yang - ./tests/schema/sdcio_model_choice.yang @@ -130,6 +130,7 @@ type Device struct { Rangetestunsigned *uint32 `path:"rangetestunsigned" module:"sdcio_model"` Unionleafreftest *string `path:"unionleafreftest" module:"sdcio_model"` Unionlengthtest SdcioModel_Unionlengthtest_Union `path:"unionlengthtest" module:"sdcio_model"` + Unionoptionalleafreftest *string `path:"unionoptionalleafreftest" module:"sdcio_model"` Unionpatterntest SdcioModel_Unionpatterntest_Union `path:"unionpatterntest" module:"sdcio_model"` Unionrangetest SdcioModel_Unionrangetest_Union `path:"unionrangetest" module:"sdcio_model"` } @@ -2314,432 +2315,434 @@ var ( // fields within the struct. ySchema = []byte{ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xec, 0x7d, 0xed, 0x72, 0xdb, 0x38, - 0x96, 0xf6, 0xff, 0x5c, 0x05, 0x5a, 0xef, 0xbb, 0x65, 0x69, 0xda, 0xb4, 0x28, 0xd9, 0x56, 0xda, - 0xde, 0x4a, 0xf5, 0x3a, 0xe9, 0x74, 0x6f, 0xaa, 0xe3, 0x9e, 0x54, 0x9c, 0x99, 0x1f, 0x63, 0x6b, - 0xbd, 0x94, 0x08, 0x49, 0xac, 0x48, 0xa4, 0x96, 0x84, 0x9c, 0xa8, 0xdb, 0xde, 0x6b, 0xdf, 0x22, + 0x96, 0xf6, 0xff, 0x5c, 0x05, 0x46, 0xef, 0xbb, 0x65, 0x69, 0xda, 0xb4, 0x28, 0xd9, 0x56, 0xda, + 0xde, 0x4a, 0xf5, 0x3a, 0xe9, 0x74, 0x6f, 0xaa, 0xe3, 0xee, 0x54, 0x9c, 0x99, 0x1f, 0x63, 0x6b, + 0xbd, 0x94, 0x04, 0x49, 0xac, 0x50, 0xa4, 0x96, 0x84, 0x9c, 0xa8, 0xdb, 0xde, 0x6b, 0xdf, 0x22, 0x29, 0x51, 0xa4, 0x48, 0x02, 0x07, 0x20, 0x28, 0x91, 0x12, 0xaa, 0xa6, 0x32, 0x6e, 0x1b, 0x24, - 0xf1, 0xf1, 0x9c, 0xaf, 0xe7, 0x00, 0x07, 0x7f, 0xbd, 0x42, 0x08, 0xa1, 0xc6, 0x1f, 0xc6, 0x0c, - 0x37, 0xae, 0x51, 0xc3, 0xc4, 0x4f, 0xd6, 0x10, 0x37, 0x4e, 0xc3, 0xdf, 0xfe, 0x6e, 0xd9, 0x66, - 0xe3, 0x1a, 0x75, 0x56, 0xff, 0xf9, 0xce, 0xb1, 0x47, 0xd6, 0xb8, 0x71, 0x8d, 0xf4, 0xd5, 0x2f, - 0x7e, 0xb1, 0xdc, 0xc6, 0x35, 0x0a, 0x5f, 0x11, 0xfc, 0x62, 0x38, 0x71, 0xac, 0x21, 0xf6, 0x12, - 0xbf, 0x4c, 0xbc, 0x7f, 0xdd, 0xe0, 0x34, 0xf9, 0xe7, 0xe4, 0x87, 0xa2, 0x5f, 0x6f, 0x7f, 0x30, - 0xfa, 0xc3, 0x27, 0x17, 0x8f, 0xac, 0xef, 0xa9, 0xcf, 0x24, 0x3e, 0xe5, 0x99, 0x43, 0xcb, 0x79, - 0x9c, 0x39, 0x26, 0x9e, 0x3e, 0x86, 0x9f, 0xdd, 0xfa, 0x6a, 0xd0, 0xfa, 0xce, 0x59, 0xb8, 0x43, - 0x9c, 0xf9, 0xa6, 0xb0, 0x67, 0x78, 0xf9, 0xcd, 0x71, 0xfd, 0xce, 0x35, 0xe6, 0xe1, 0x47, 0x4f, - 0xb3, 0x1b, 0xfe, 0xa7, 0xe1, 0xdd, 0xb8, 0xe3, 0xc5, 0x0c, 0xdb, 0xa4, 0x71, 0x8d, 0x88, 0xbb, - 0xc0, 0x39, 0x0d, 0x63, 0xad, 0xb2, 0xfa, 0x98, 0x7a, 0xe8, 0x25, 0xf1, 0x9b, 0x97, 0xad, 0x99, - 0xd8, 0x5e, 0x82, 0xad, 0xa5, 0x18, 0x1a, 0x1e, 0x65, 0x70, 0xc9, 0x55, 0x09, 0xda, 0xe6, 0x74, - 0x7a, 0xb5, 0x40, 0x97, 0x39, 0x7f, 0xce, 0x5b, 0x28, 0xc8, 0x82, 0x89, 0x2d, 0x1c, 0x74, 0x01, - 0xb9, 0x17, 0x92, 0x7b, 0x41, 0x85, 0x17, 0x36, 0x7b, 0x81, 0x73, 0x16, 0x9a, 0xb9, 0xe0, 0x9b, - 0x85, 0x37, 0x3c, 0xdc, 0x61, 0xcf, 0x47, 0xb4, 0xf6, 0x41, 0x73, 0xc6, 0xd0, 0x56, 0xcb, 0x7f, - 0xc1, 0x68, 0xc6, 0x82, 0x01, 0x0f, 0x1c, 0x8a, 0xc1, 0x82, 0x17, 0x1e, 0xc2, 0x30, 0x11, 0x86, - 0x4b, 0x61, 0xd8, 0xd0, 0xe1, 0xc3, 0x80, 0x11, 0x18, 0x4e, 0x9c, 0xb0, 0x12, 0x82, 0x17, 0xc3, - 0x0c, 0x14, 0x86, 0x9b, 0x08, 0xec, 0xe4, 0xc0, 0x4f, 0x14, 0x86, 0x85, 0xe1, 0x58, 0x18, 0x96, - 0xd2, 0xe0, 0x09, 0x83, 0x29, 0x10, 0xae, 0xdc, 0xb0, 0x4d, 0xc0, 0x57, 0xc3, 0x53, 0x3c, 0xe3, - 0x5f, 0x83, 0x38, 0x94, 0xc3, 0x57, 0x70, 0x4e, 0x21, 0x1f, 0xac, 0x85, 0xe1, 0x5d, 0x04, 0xe6, - 0x72, 0xe1, 0x5e, 0x14, 0xf6, 0xd2, 0xe0, 0x2f, 0x4d, 0x0c, 0xa4, 0x8b, 0x03, 0x9f, 0x58, 0x70, - 0x8a, 0x87, 0xb0, 0x98, 0x44, 0x0f, 0x0a, 0x49, 0x4a, 0x0a, 0x44, 0x02, 0xc2, 0xb2, 0x2d, 0x34, - 0xba, 0xe0, 0xe3, 0xa2, 0xc2, 0x23, 0x43, 0x88, 0xca, 0x11, 0x26, 0x59, 0x42, 0x25, 0x5d, 0xb8, - 0xa4, 0x0b, 0x59, 0x69, 0xc2, 0x26, 0x26, 0x74, 0x82, 0xc2, 0x17, 0x8d, 0xe2, 0xcb, 0x72, 0x8e, - 0x25, 0xe1, 0x88, 0xb8, 0x96, 0x3d, 0x2e, 0x82, 0x9d, 0xb5, 0x29, 0xfa, 0xe9, 0xd5, 0x6e, 0xe6, - 0xad, 0x5c, 0xf5, 0x76, 0x63, 0xdb, 0x0e, 0x31, 0x88, 0xe5, 0xd8, 0x62, 0x5a, 0xce, 0x1b, 0x4e, - 0xf0, 0xcc, 0x98, 0x1b, 0x64, 0xe2, 0xcf, 0x6e, 0x3b, 0x06, 0xb1, 0xf6, 0x8a, 0xa8, 0x68, 0x6f, - 0x42, 0xe3, 0x76, 0xe0, 0xca, 0xc6, 0xfe, 0xd5, 0x04, 0xd5, 0x9b, 0xbf, 0x8c, 0x8b, 0x21, 0xb1, - 0x57, 0x8b, 0x7a, 0xe7, 0x7f, 0xf5, 0x36, 0xc0, 0xf5, 0xbb, 0xf0, 0xa3, 0x8f, 0xef, 0xfc, 0x6f, - 0x04, 0xff, 0xbe, 0xf7, 0x3f, 0xf1, 0xaa, 0x9c, 0x19, 0xe7, 0x98, 0xed, 0xc6, 0xd4, 0x19, 0x8b, - 0x3b, 0x4e, 0xfe, 0xc3, 0xbc, 0x86, 0x0b, 0x8f, 0x8c, 0xc5, 0xd4, 0x17, 0xfc, 0x7b, 0xfe, 0xf9, - 0x1d, 0x19, 0x53, 0x8f, 0x53, 0x3b, 0xf4, 0xc5, 0x5c, 0x3a, 0x5d, 0xb9, 0x74, 0xca, 0xa5, 0xdb, - 0xad, 0xce, 0x13, 0xb6, 0x26, 0x11, 0x0e, 0x06, 0x8e, 0x33, 0xc5, 0x86, 0x2d, 0xb2, 0xf8, 0x6b, - 0xf3, 0xd1, 0x29, 0x4b, 0x25, 0x49, 0x0d, 0x12, 0xf1, 0x77, 0xe2, 0x1a, 0xda, 0xc2, 0xf6, 0x88, - 0x31, 0x98, 0xf2, 0x4d, 0x9a, 0x8f, 0x4d, 0x0f, 0xdb, 0x01, 0xd0, 0xf9, 0x34, 0x50, 0x81, 0x85, - 0x09, 0x4c, 0x0b, 0xb2, 0x3c, 0x64, 0x0c, 0x89, 0xf5, 0x54, 0x01, 0xe9, 0x0c, 0x67, 0xa0, 0x4a, - 0xf2, 0xb9, 0x3d, 0x45, 0x65, 0x0b, 0x27, 0xb8, 0x75, 0x5f, 0x2a, 0x72, 0x05, 0x1d, 0x1b, 0x71, - 0x87, 0x86, 0x87, 0xcb, 0x82, 0xb9, 0x2f, 0xb0, 0xa5, 0x79, 0x29, 0x99, 0xde, 0xe4, 0x9c, 0x48, - 0x91, 0x09, 0xa4, 0x0f, 0x34, 0xbf, 0xfb, 0x94, 0xae, 0x07, 0x30, 0xef, 0xf2, 0x71, 0xf8, 0x5d, - 0xc5, 0xe1, 0x17, 0x75, 0x31, 0x8e, 0x82, 0xc3, 0xef, 0x8a, 0x71, 0xf8, 0x5d, 0xc5, 0xe1, 0x2b, - 0x0e, 0x7f, 0xcf, 0x1c, 0xbe, 0x0a, 0x42, 0x55, 0x10, 0xaa, 0x82, 0x50, 0x15, 0x84, 0xaa, 0x20, - 0x54, 0x05, 0xa1, 0x2a, 0x08, 0x3d, 0xa0, 0x20, 0xb4, 0xdb, 0xe6, 0x71, 0x31, 0xe1, 0x41, 0x68, - 0xf7, 0x58, 0x82, 0xd0, 0xae, 0x70, 0x10, 0xca, 0xb5, 0xf7, 0x0c, 0x38, 0x10, 0xbe, 0x01, 0x64, - 0x77, 0xfd, 0x85, 0x73, 0x1f, 0x24, 0xa3, 0x6b, 0x80, 0x2e, 0x65, 0xed, 0x0e, 0x65, 0xa1, 0x2c, - 0xd9, 0xf7, 0x4d, 0x0f, 0x63, 0xbd, 0x6b, 0x0c, 0x9d, 0x45, 0xa0, 0x36, 0xf2, 0x76, 0xc4, 0x06, - 0x7f, 0xde, 0xf5, 0x7e, 0xd8, 0x8c, 0x8f, 0xa2, 0x8a, 0x6d, 0x87, 0x0d, 0xba, 0x28, 0x6b, 0x37, - 0xac, 0x87, 0xdd, 0x27, 0xec, 0xb2, 0x77, 0xc2, 0xae, 0xda, 0xd1, 0x77, 0xc1, 0x76, 0x76, 0xbe, - 0x0b, 0x36, 0x67, 0xb9, 0x78, 0x2d, 0xee, 0x3e, 0x37, 0xc1, 0x66, 0x2f, 0xa7, 0x98, 0x1e, 0x62, - 0xee, 0x81, 0x35, 0x08, 0x71, 0xe1, 0xf4, 0x59, 0xd0, 0x1a, 0xc6, 0x9e, 0xe9, 0x95, 0x61, 0xcf, - 0x18, 0x90, 0x38, 0x04, 0xf2, 0x8c, 0x0e, 0x19, 0x39, 0xb6, 0x19, 0x1c, 0xc9, 0xf0, 0x6f, 0xc2, - 0x00, 0x6e, 0xb6, 0x10, 0x63, 0x88, 0xbf, 0xe2, 0xe5, 0x0d, 0x17, 0xc8, 0xd7, 0x0f, 0x28, 0x9c, - 0x2b, 0x9c, 0x57, 0x0d, 0xe7, 0x5c, 0xca, 0xff, 0x77, 0xbc, 0x64, 0xe3, 0xb9, 0xf1, 0xd1, 0xf2, - 0x08, 0x53, 0x40, 0x1a, 0xb7, 0x96, 0xfd, 0x7e, 0x8a, 0xfd, 0x45, 0xf1, 0xe8, 0x58, 0x6e, 0xdc, - 0x1a, 0xdf, 0x63, 0x2d, 0x3b, 0x3f, 0x5d, 0x5c, 0xf4, 0x5e, 0x5f, 0x5c, 0xe8, 0xaf, 0xcf, 0x5f, - 0xeb, 0x57, 0x97, 0x97, 0x9d, 0x5e, 0xe7, 0x92, 0xf2, 0xf0, 0xdf, 0x5d, 0x13, 0xbb, 0xd8, 0x7c, - 0xeb, 0x77, 0xdc, 0x5e, 0x4c, 0xa7, 0x90, 0xa6, 0xff, 0xf0, 0x02, 0x7f, 0x25, 0xa0, 0x11, 0x77, - 0xee, 0xa4, 0xfb, 0x88, 0x6c, 0x53, 0x7d, 0x21, 0x86, 0x7b, 0xec, 0xbf, 0xe0, 0xf1, 0x2e, 0x7c, - 0x81, 0x14, 0xff, 0x1e, 0x40, 0xa3, 0x34, 0x66, 0x0b, 0x2f, 0x9f, 0xac, 0x05, 0xb8, 0x5c, 0xc1, - 0xb0, 0x9b, 0x67, 0xab, 0x81, 0xb7, 0xd0, 0x1b, 0x44, 0x0b, 0x49, 0x1b, 0xef, 0x5d, 0xd7, 0x71, - 0x6f, 0xb1, 0xe7, 0x19, 0x63, 0x0c, 0xd7, 0xc3, 0x9f, 0xf1, 0xd4, 0x20, 0xd6, 0x13, 0x46, 0x9f, - 0x0c, 0x32, 0x41, 0x1a, 0xfa, 0x32, 0xc1, 0x2e, 0x46, 0x7e, 0xd7, 0xd1, 0x00, 0x23, 0xfc, 0xdd, - 0x18, 0x92, 0xe9, 0x12, 0x75, 0x51, 0xd8, 0x09, 0x84, 0x6d, 0xe2, 0x5a, 0xd8, 0x3b, 0x63, 0xe9, - 0x6d, 0x0e, 0xf5, 0x17, 0x57, 0x7d, 0xd8, 0x1f, 0x83, 0x36, 0x5b, 0x0d, 0x02, 0x20, 0xeb, 0x22, - 0xda, 0x2f, 0xa1, 0xf9, 0x44, 0xc7, 0xbf, 0x0b, 0xdd, 0x02, 0xc6, 0x48, 0x42, 0x42, 0x4a, 0x02, - 0xca, 0xcd, 0xc0, 0x73, 0xa6, 0x0b, 0x72, 0xb4, 0x40, 0x11, 0x1d, 0xbf, 0x5c, 0xa0, 0xbc, 0xa2, - 0x73, 0x6c, 0x32, 0x69, 0x88, 0xbc, 0x98, 0x9c, 0xae, 0x65, 0xc1, 0x14, 0x84, 0x66, 0x3a, 0x8b, - 0xc1, 0x14, 0x7f, 0x0d, 0xcc, 0x27, 0x8d, 0x8c, 0x88, 0x35, 0x54, 0xb4, 0x84, 0xa2, 0x25, 0x14, - 0x2d, 0xa1, 0x68, 0x09, 0x15, 0xae, 0xa9, 0x70, 0x8d, 0x87, 0x96, 0xe8, 0x70, 0xf3, 0x12, 0x1d, - 0x85, 0x74, 0x85, 0xf4, 0x1a, 0x22, 0xbd, 0xcb, 0x8d, 0xf4, 0xae, 0x42, 0xba, 0x42, 0xfa, 0x21, - 0x51, 0x70, 0x1d, 0xc4, 0x40, 0xb6, 0x22, 0xe3, 0x0a, 0x93, 0x71, 0x9b, 0x98, 0xac, 0x30, 0x2d, - 0xf7, 0xcb, 0xfa, 0x4d, 0x8a, 0x9f, 0x53, 0xfc, 0xdc, 0x41, 0xf3, 0x73, 0x29, 0xa1, 0x51, 0x4c, - 0x9d, 0x62, 0xea, 0x64, 0x31, 0x75, 0xb9, 0x2c, 0x19, 0x8f, 0x0a, 0x86, 0x90, 0x77, 0x00, 0xda, - 0x6e, 0x6f, 0x84, 0x1d, 0x75, 0x0e, 0xaa, 0x43, 0xda, 0x99, 0xd9, 0xf3, 0x5d, 0x80, 0xb8, 0x1b, - 0x3a, 0x19, 0x7b, 0xba, 0x32, 0x94, 0x51, 0xae, 0xe3, 0xbc, 0x37, 0xd2, 0x8e, 0xb6, 0x64, 0x35, - 0x22, 0xee, 0x28, 0x4b, 0x5a, 0x12, 0x79, 0xf7, 0x64, 0x4c, 0x17, 0x3c, 0x85, 0xf5, 0x56, 0xed, - 0x6b, 0x16, 0xec, 0x41, 0xe0, 0x71, 0x00, 0x01, 0x1f, 0x00, 0x3e, 0xc7, 0x4d, 0x6f, 0x04, 0xe8, - 0xed, 0x72, 0xa2, 0xbd, 0xab, 0xd0, 0xae, 0xd0, 0x7e, 0x18, 0x14, 0x87, 0x84, 0xc0, 0x7d, 0x13, - 0x7d, 0x50, 0xfc, 0x00, 0xba, 0xb7, 0xb8, 0x89, 0xd5, 0xdf, 0x39, 0x79, 0x04, 0x55, 0x46, 0xa4, - 0x9e, 0xe1, 0x8c, 0x7d, 0xc5, 0xcb, 0x0e, 0xdb, 0x61, 0x09, 0x5a, 0xd1, 0x1d, 0x16, 0x5d, 0x39, - 0x2c, 0xd5, 0x70, 0x58, 0x98, 0xb2, 0x06, 0x97, 0xb1, 0x8d, 0x6c, 0x51, 0xda, 0x7c, 0x32, 0x08, - 0xc1, 0xae, 0xcd, 0x3c, 0x8b, 0xd6, 0xf8, 0xda, 0xb9, 0x37, 0xb4, 0x3f, 0x6f, 0xb4, 0x7f, 0xe9, - 0xda, 0xd5, 0x59, 0xff, 0xc7, 0xfc, 0xf1, 0xf6, 0x8b, 0xe1, 0xb9, 0x0b, 0xc2, 0x73, 0x57, 0xe1, - 0x59, 0xe1, 0xb9, 0x18, 0x9e, 0xbb, 0x3b, 0xc0, 0xf3, 0xcc, 0xb0, 0x4d, 0x83, 0x38, 0x6c, 0x48, - 0xaf, 0x1b, 0x2a, 0x54, 0xd7, 0x02, 0xd5, 0xb7, 0xe1, 0x72, 0xb9, 0x4b, 0x4a, 0xac, 0x5f, 0x0a, - 0xf2, 0xa5, 0xb0, 0xfa, 0x9b, 0x44, 0x4f, 0x17, 0x65, 0xf8, 0x06, 0xf4, 0xc4, 0x0e, 0x3b, 0xa1, - 0x23, 0x94, 0xc8, 0x01, 0x24, 0x70, 0x00, 0x89, 0x1b, 0x89, 0x74, 0xa0, 0x18, 0x11, 0xc8, 0xc5, - 0x01, 0xe2, 0xd9, 0x9c, 0x2c, 0x87, 0x8e, 0x3d, 0xca, 0xe7, 0x00, 0x37, 0x4d, 0xb2, 0x39, 0x40, - 0xbd, 0x24, 0x0e, 0xb0, 0xd2, 0xcc, 0x1f, 0x2f, 0xdf, 0x97, 0x2b, 0x89, 0xc9, 0x69, 0xce, 0x1a, - 0xf3, 0x5a, 0xf0, 0xce, 0x01, 0xcb, 0x69, 0x99, 0xd8, 0x26, 0x16, 0x59, 0xba, 0x98, 0xb2, 0xa0, - 0xf1, 0x46, 0x3b, 0xa3, 0x75, 0xd7, 0x1f, 0xad, 0xea, 0xaa, 0x46, 0xfd, 0x93, 0x46, 0xe4, 0xba, - 0xcb, 0x39, 0x71, 0x6e, 0x00, 0x5c, 0xee, 0xaa, 0x61, 0x65, 0xec, 0x2e, 0x65, 0xa9, 0x6a, 0x61, - 0x70, 0xf3, 0x97, 0xb2, 0x6c, 0xff, 0x31, 0x5f, 0xb2, 0x32, 0xa5, 0x8c, 0xb6, 0x99, 0xe0, 0xc3, - 0xea, 0x55, 0x6f, 0x69, 0xf7, 0xec, 0xe4, 0x60, 0x49, 0x33, 0xa6, 0x2c, 0xe2, 0xa3, 0xf1, 0x4f, - 0x63, 0xba, 0x08, 0xee, 0x53, 0x62, 0xd7, 0xff, 0xe0, 0x2c, 0xea, 0x65, 0x6e, 0x9f, 0x5e, 0x17, - 0xe0, 0x7b, 0xc4, 0x3e, 0x7b, 0xbe, 0x8f, 0xef, 0x3a, 0x64, 0x82, 0xdd, 0x9b, 0xe9, 0xd8, 0xd9, - 0xc7, 0xc7, 0x5d, 0xcf, 0x28, 0xca, 0xbc, 0xf5, 0x0b, 0xa7, 0x43, 0xb3, 0xa2, 0x8e, 0x10, 0x88, - 0x6f, 0xa1, 0xda, 0xef, 0xad, 0xd2, 0x7e, 0x4a, 0xfb, 0x29, 0xed, 0xa7, 0xb4, 0x5f, 0x0d, 0xb5, - 0x5f, 0x59, 0xd1, 0x1f, 0x5d, 0x9c, 0xf2, 0xe3, 0xbf, 0x0f, 0xb1, 0xe7, 0x20, 0x21, 0x83, 0x4d, - 0xa2, 0xf6, 0xd4, 0xbd, 0x20, 0xdb, 0x0d, 0x55, 0xe8, 0x50, 0x6e, 0xe8, 0x00, 0xb5, 0x9d, 0xca, - 0x74, 0x2a, 0xd3, 0xa9, 0x4c, 0xa7, 0x32, 0x9d, 0x87, 0x11, 0x38, 0x98, 0xd8, 0x1b, 0xba, 0xd6, - 0x9c, 0x9a, 0x1f, 0x8f, 0x2f, 0x5d, 0xd4, 0x58, 0x69, 0xc1, 0xda, 0x6b, 0xc1, 0x3d, 0x25, 0x21, - 0x32, 0xcd, 0xa8, 0xca, 0x40, 0x04, 0x3e, 0x28, 0xd5, 0xe7, 0x63, 0xf8, 0xa1, 0x5b, 0xcf, 0x02, - 0x7d, 0x51, 0x77, 0x64, 0x64, 0x48, 0x46, 0xc2, 0x0b, 0x0d, 0x9b, 0xec, 0x78, 0x47, 0xb2, 0x35, - 0xaa, 0xf6, 0x56, 0x64, 0x6b, 0x24, 0xcd, 0xff, 0x34, 0xcc, 0x99, 0x65, 0x6b, 0x1e, 0x31, 0x08, - 0xe0, 0x8a, 0xef, 0x78, 0xe3, 0xbc, 0x1d, 0xb0, 0x80, 0x72, 0xe7, 0x0d, 0x6c, 0x07, 0xe7, 0x7b, - 0x32, 0x1b, 0xf4, 0x2b, 0x96, 0x93, 0xb6, 0x46, 0xf5, 0x4e, 0x46, 0x67, 0x61, 0xa5, 0x6c, 0xe5, - 0xce, 0xc6, 0x49, 0x42, 0x8c, 0x2f, 0x68, 0x87, 0x69, 0xec, 0x05, 0xfb, 0x0e, 0xd1, 0xc6, 0x17, - 0xe7, 0x2e, 0xb4, 0x27, 0xa0, 0x6d, 0x83, 0x9d, 0x20, 0x07, 0x17, 0x62, 0x10, 0xb0, 0x67, 0xb0, - 0x1b, 0xb8, 0x1f, 0x96, 0x97, 0x8f, 0x59, 0xa0, 0xef, 0xd6, 0xf8, 0xe2, 0x7c, 0xa0, 0xec, 0xf8, - 0x4f, 0xfa, 0x46, 0xab, 0xef, 0x5d, 0xa3, 0x2e, 0xa0, 0x8b, 0xab, 0xc1, 0x5c, 0xa3, 0xce, 0x4e, - 0xf6, 0x35, 0x72, 0x94, 0x23, 0xa7, 0x9f, 0xd7, 0x83, 0x7b, 0xb4, 0x11, 0xb6, 0x9a, 0xcd, 0x33, - 0xf4, 0x06, 0x9d, 0x84, 0x23, 0x3e, 0x69, 0x21, 0xc3, 0x36, 0x91, 0x47, 0x0c, 0x97, 0x78, 0xda, - 0x37, 0x8b, 0x4c, 0x9a, 0x67, 0x67, 0x6d, 0xdf, 0x40, 0x9d, 0xa2, 0x13, 0x6f, 0xe9, 0x11, 0x3c, - 0xd3, 0x4f, 0x5a, 0x2d, 0xe4, 0xb8, 0xc8, 0x76, 0x48, 0x93, 0xd5, 0x0e, 0x82, 0x06, 0xae, 0xd3, - 0x5d, 0x34, 0xc9, 0xd8, 0x9c, 0x6d, 0x82, 0x22, 0x11, 0x09, 0xd6, 0x45, 0x2f, 0x72, 0xb0, 0x4b, - 0x58, 0x21, 0xe5, 0x2a, 0x27, 0xca, 0x14, 0xec, 0xa6, 0xd0, 0x76, 0xfe, 0xd6, 0x2b, 0x8a, 0x1e, - 0x72, 0xf1, 0x08, 0xbb, 0xa0, 0x32, 0xfa, 0x1c, 0x58, 0xfe, 0xfc, 0xeb, 0x3b, 0xd4, 0xfd, 0xa9, - 0x77, 0x7e, 0x8d, 0xbe, 0x4c, 0x30, 0xfa, 0xb0, 0x76, 0x77, 0x3c, 0xf4, 0x9b, 0xeb, 0x2c, 0xe6, - 0xe8, 0xf6, 0xc3, 0x5b, 0xa4, 0x21, 0x6b, 0x74, 0xe3, 0xcf, 0xd8, 0x1d, 0x31, 0xc8, 0xc2, 0x2b, - 0x79, 0x8b, 0xf9, 0x66, 0x94, 0xbb, 0xdc, 0x65, 0x2e, 0x30, 0x0d, 0xe5, 0x61, 0xe0, 0xa8, 0xe3, - 0x59, 0xe5, 0xf2, 0x88, 0xba, 0x3c, 0xec, 0xe5, 0x44, 0xd0, 0x3d, 0xa5, 0x1f, 0xb1, 0x3d, 0x0e, - 0xe2, 0xb3, 0xc2, 0x7a, 0xe6, 0xd6, 0x82, 0xdf, 0xba, 0x10, 0x92, 0x7d, 0x1c, 0x97, 0x6e, 0xfd, - 0xea, 0x1a, 0x43, 0x7f, 0xb8, 0xbf, 0x58, 0x63, 0x8b, 0x55, 0x6c, 0x21, 0x39, 0x65, 0x78, 0x1c, - 0x1c, 0x0e, 0xa7, 0xd6, 0x42, 0xe0, 0xa4, 0xc4, 0xfc, 0x10, 0x9c, 0x7f, 0xa8, 0xdd, 0xcb, 0xcb, - 0xea, 0x0d, 0x76, 0x0f, 0x8a, 0x2b, 0x0a, 0xb4, 0x35, 0x42, 0x03, 0x7c, 0x3a, 0x30, 0x0f, 0xdb, - 0x2b, 0xf5, 0x55, 0x7b, 0xf5, 0x55, 0x02, 0x1d, 0x97, 0xb5, 0x3f, 0x9d, 0x2c, 0x00, 0x7b, 0xd3, - 0xc9, 0x42, 0x21, 0xaa, 0xfe, 0x88, 0x5a, 0x58, 0x36, 0xe9, 0xf4, 0x00, 0x88, 0xea, 0x51, 0x9a, - 0x7c, 0x36, 0xec, 0x31, 0xde, 0x9b, 0x25, 0xd4, 0x8f, 0xc7, 0x12, 0xf6, 0x2e, 0x2f, 0xcf, 0x95, - 0x2d, 0xf4, 0x3b, 0xb9, 0x62, 0xb7, 0x19, 0x4a, 0x2a, 0x68, 0xa5, 0xb4, 0x54, 0xed, 0xb5, 0xd4, - 0xc6, 0x93, 0xa1, 0xac, 0x68, 0x5d, 0x3c, 0xf7, 0xf3, 0x23, 0xf2, 0xdc, 0xf5, 0xc3, 0x51, 0x56, - 0x32, 0x8e, 0x17, 0x36, 0x67, 0xe3, 0x19, 0xd1, 0x9f, 0x57, 0x54, 0xe6, 0xf3, 0xd4, 0x69, 0xea, - 0xcf, 0x9d, 0x7b, 0x5d, 0xbb, 0xea, 0x07, 0xff, 0x3c, 0x77, 0x9b, 0xf7, 0xba, 0x76, 0xb1, 0xfa, - 0x8f, 0xcb, 0x7b, 0x5d, 0xbb, 0xec, 0xb7, 0x9e, 0xef, 0x3b, 0xd1, 0xdf, 0x83, 0x1f, 0x5b, 0xcf, - 0x98, 0x4c, 0xb0, 0x6b, 0x63, 0xa2, 0x35, 0x83, 0x5f, 0x34, 0x1f, 0x1e, 0xcc, 0xd6, 0x5f, 0xfa, - 0x69, 0xe7, 0xa5, 0xd9, 0xbe, 0x37, 0x06, 0x43, 0xb3, 0xdf, 0xfa, 0xb9, 0xd9, 0xde, 0xfa, 0x53, - 0xeb, 0xe7, 0x76, 0x73, 0xbb, 0x79, 0xeb, 0xb9, 0xe9, 0x7f, 0xbd, 0xd3, 0xf7, 0x7f, 0xf3, 0xdc, - 0xec, 0x74, 0xef, 0x75, 0xed, 0xa7, 0x7e, 0xab, 0xd5, 0x7a, 0xb6, 0xdc, 0x81, 0x58, 0xd7, 0xa6, - 0xc6, 0x78, 0xeb, 0x33, 0xdd, 0xe0, 0x33, 0xba, 0xae, 0xb7, 0x5a, 0xad, 0x72, 0xce, 0x53, 0x7a, - 0x8b, 0x41, 0x7e, 0x9a, 0x32, 0xad, 0x8b, 0xe3, 0xad, 0x2b, 0x56, 0xb0, 0x47, 0xd9, 0x86, 0xf5, - 0x17, 0xd8, 0xf5, 0xb5, 0x01, 0x19, 0x51, 0xc1, 0x8c, 0x17, 0xe2, 0xbd, 0x10, 0xba, 0x01, 0x21, - 0xc6, 0xfb, 0x35, 0xab, 0x97, 0x42, 0xc5, 0xa2, 0x0c, 0x16, 0x7b, 0xcf, 0x85, 0x52, 0x68, 0x58, - 0x05, 0x1a, 0x0b, 0xe9, 0x15, 0x52, 0xe0, 0xf8, 0x44, 0xc0, 0xcc, 0xec, 0x26, 0x21, 0x06, 0xc9, - 0xd0, 0x6e, 0xba, 0xce, 0x93, 0xa9, 0x8d, 0x9e, 0xe2, 0xcb, 0xd8, 0x46, 0x8f, 0xc1, 0x33, 0xb7, - 0x1c, 0x0e, 0x0b, 0xe2, 0xcb, 0xe4, 0x6e, 0xb2, 0x03, 0x3c, 0x19, 0xdd, 0x6d, 0x05, 0xc0, 0xca, - 0xec, 0x4a, 0x4b, 0x8e, 0x31, 0x90, 0x27, 0x70, 0x11, 0x35, 0x2c, 0x03, 0x0c, 0xf7, 0x8d, 0x53, - 0xd8, 0x06, 0x64, 0x84, 0x39, 0x92, 0xc2, 0x82, 0x79, 0xe1, 0x8d, 0x38, 0x88, 0xe4, 0x87, 0x69, - 0xf2, 0x2a, 0x96, 0x27, 0x16, 0xd5, 0xa3, 0x79, 0x3a, 0x55, 0x34, 0x6f, 0x5c, 0x58, 0xcd, 0xe6, - 0xaa, 0xdc, 0xa2, 0x79, 0x64, 0xb8, 0xc8, 0xf0, 0xb5, 0x64, 0xb7, 0xea, 0x97, 0x50, 0x6a, 0x0d, - 0x92, 0x7b, 0x14, 0x4c, 0x5a, 0x29, 0x27, 0x42, 0x39, 0x11, 0x9c, 0x70, 0x81, 0x32, 0x26, 0xbc, - 0xcc, 0x89, 0x80, 0x95, 0xe0, 0x61, 0x52, 0x52, 0x34, 0x43, 0x87, 0x53, 0xd9, 0x8a, 0xb2, 0x0d, - 0xe2, 0xac, 0x03, 0xa7, 0xe3, 0xc2, 0xcd, 0xb8, 0xa4, 0x99, 0x17, 0x68, 0xce, 0xb4, 0x0a, 0x93, - 0x52, 0x61, 0x85, 0x6d, 0xd9, 0x26, 0xfe, 0x0e, 0x57, 0xd5, 0x61, 0x73, 0xa5, 0xa4, 0x95, 0x92, - 0x66, 0xcc, 0xff, 0xc2, 0xb2, 0xc9, 0x79, 0x97, 0x43, 0x3f, 0xbf, 0x06, 0x34, 0x85, 0xe5, 0xe1, - 0x76, 0xad, 0x9d, 0x75, 0xa5, 0x9d, 0xb7, 0xa7, 0xe4, 0xea, 0xea, 0xea, 0x4a, 0xa9, 0xe7, 0x03, - 0x09, 0x69, 0xf5, 0xc3, 0x88, 0x65, 0xe3, 0xbc, 0x38, 0x0a, 0xec, 0x58, 0x14, 0xaf, 0xe9, 0x68, - 0xe4, 0xb8, 0x68, 0xd5, 0x39, 0xc4, 0x62, 0xcf, 0x8f, 0x2f, 0xc6, 0x15, 0x98, 0xba, 0x23, 0x8c, - 0x7d, 0x09, 0xc4, 0x48, 0x46, 0x70, 0xa4, 0x6c, 0x59, 0x53, 0x8e, 0x94, 0x72, 0xa4, 0x38, 0xcf, - 0xe9, 0xa7, 0xa2, 0x5d, 0x40, 0x74, 0xc4, 0x77, 0x6e, 0x3f, 0x0d, 0x0c, 0x4b, 0x03, 0x60, 0x38, - 0xe9, 0x1a, 0x78, 0x60, 0x6b, 0xc5, 0x67, 0xb1, 0x12, 0x1d, 0x1b, 0xb8, 0x96, 0x39, 0xc6, 0x66, - 0xa3, 0x0c, 0xb7, 0x48, 0xb0, 0x4b, 0xae, 0xb3, 0x20, 0x5c, 0x3d, 0x02, 0xb5, 0xec, 0x1f, 0x23, - 0xcb, 0x9e, 0xeb, 0x5b, 0x58, 0xee, 0xe0, 0x24, 0xf0, 0x40, 0x72, 0x5b, 0xac, 0xb7, 0x15, 0x30, - 0x9a, 0x4d, 0x8d, 0x31, 0xab, 0x85, 0x35, 0x62, 0x7d, 0xca, 0x9e, 0x33, 0x5a, 0x3c, 0x4d, 0xec, - 0xbd, 0xb8, 0x41, 0xbe, 0xcc, 0xfa, 0x4e, 0x1a, 0xf2, 0x16, 0xf3, 0xb9, 0xe3, 0x12, 0x6c, 0x22, - 0xc7, 0x46, 0x64, 0x62, 0x79, 0xca, 0xf1, 0x49, 0x19, 0x07, 0xd0, 0x64, 0xed, 0xd7, 0xd5, 0x39, - 0x2d, 0x4d, 0xd2, 0xce, 0xd0, 0x0f, 0x6f, 0xd0, 0xc9, 0xd4, 0x19, 0x1a, 0x53, 0x6d, 0x66, 0x05, - 0x6b, 0x63, 0x62, 0x6f, 0x25, 0x3d, 0x4d, 0x29, 0x52, 0xb6, 0x07, 0xfc, 0xa7, 0xc6, 0x83, 0x2c, - 0x4f, 0xc9, 0x03, 0x50, 0x1e, 0x84, 0x26, 0xef, 0x50, 0x43, 0x01, 0x91, 0x6b, 0x66, 0x69, 0xfc, - 0x69, 0xc9, 0x57, 0xcb, 0x5e, 0xe8, 0x57, 0x87, 0x74, 0x95, 0x6c, 0x84, 0xaf, 0x36, 0x60, 0xc3, - 0x1b, 0x62, 0x96, 0x0d, 0x09, 0x9e, 0x7e, 0xbc, 0x8b, 0xbf, 0x4a, 0x66, 0xc1, 0x97, 0x8c, 0x6d, - 0xbd, 0xaa, 0xdc, 0x4b, 0x62, 0x19, 0xb9, 0x0b, 0xbd, 0x64, 0x2c, 0x52, 0x4e, 0x89, 0x97, 0xf9, - 0x53, 0x8f, 0x52, 0xdd, 0xc5, 0xff, 0xeb, 0x8e, 0x0b, 0xbb, 0x78, 0xfe, 0x64, 0x0d, 0x1f, 0x83, - 0xa8, 0xa0, 0xda, 0x25, 0x5e, 0x12, 0x3d, 0x95, 0x55, 0xec, 0x25, 0x7c, 0xa9, 0x16, 0xbe, 0x94, - 0xbd, 0x9f, 0x35, 0xde, 0xba, 0x62, 0xfb, 0x59, 0x19, 0x0b, 0xc9, 0xeb, 0x10, 0xec, 0x71, 0x67, - 0x2b, 0x7d, 0xa1, 0xc5, 0x6c, 0x1e, 0x73, 0x8f, 0xeb, 0x60, 0x64, 0x6a, 0xe1, 0x5e, 0x19, 0x13, + 0xf1, 0xf1, 0x9c, 0xaf, 0xe7, 0x00, 0x07, 0x7f, 0xbd, 0x42, 0x08, 0xa1, 0xc6, 0xef, 0xc6, 0x0c, + 0x37, 0xae, 0x51, 0x63, 0x84, 0x9f, 0xcc, 0x21, 0x6e, 0x9c, 0x86, 0xbf, 0xfd, 0xcd, 0xb4, 0x47, + 0x8d, 0x6b, 0xd4, 0x59, 0xfd, 0xe7, 0x3b, 0xc7, 0x1e, 0x9b, 0x93, 0xc6, 0x35, 0xd2, 0x57, 0xbf, + 0xf8, 0xd9, 0x74, 0x1b, 0xd7, 0x28, 0x7c, 0x45, 0xf0, 0x8b, 0xe1, 0xd4, 0x31, 0x87, 0xd8, 0x4b, + 0xfc, 0x32, 0xf1, 0xfe, 0x75, 0x83, 0xd3, 0xe4, 0x9f, 0x93, 0x1f, 0x8a, 0x7e, 0xbd, 0xfd, 0xc1, + 0xe8, 0x0f, 0x9f, 0x5c, 0x3c, 0x36, 0xbf, 0xa7, 0x3e, 0x93, 0xf8, 0x94, 0x37, 0x1a, 0x9a, 0xce, + 0xe3, 0xcc, 0x19, 0x61, 0xeb, 0x31, 0xfc, 0xec, 0xd6, 0x57, 0x83, 0xd6, 0x77, 0xce, 0xc2, 0x1d, + 0xe2, 0xcc, 0x37, 0x85, 0x3d, 0xc3, 0xcb, 0x6f, 0x8e, 0xeb, 0x77, 0xae, 0x31, 0x0f, 0x3f, 0x7a, + 0x9a, 0xdd, 0xf0, 0x3f, 0x0d, 0xef, 0xc6, 0x9d, 0x2c, 0x66, 0xd8, 0x26, 0x8d, 0x6b, 0x44, 0xdc, + 0x05, 0xce, 0x69, 0x18, 0x6b, 0x95, 0xd5, 0xc7, 0xd4, 0x43, 0x2f, 0x89, 0xdf, 0xbc, 0x6c, 0xcd, + 0xc4, 0xf6, 0x12, 0x6c, 0x2d, 0xc5, 0xd0, 0xf0, 0x28, 0x83, 0x4b, 0xae, 0x4a, 0xd0, 0x36, 0xa7, + 0xd3, 0xab, 0x05, 0xba, 0xcc, 0xf9, 0x73, 0xde, 0x42, 0x41, 0x16, 0x4c, 0x6c, 0xe1, 0xa0, 0x0b, + 0xc8, 0xbd, 0x90, 0xdc, 0x0b, 0x2a, 0xbc, 0xb0, 0xd9, 0x0b, 0x9c, 0xb3, 0xd0, 0xcc, 0x05, 0xdf, + 0x2c, 0xbc, 0xe1, 0xe1, 0x0e, 0x7b, 0x3e, 0xa2, 0xb5, 0x0f, 0x9a, 0x33, 0x86, 0xb6, 0x5a, 0xfe, + 0x0b, 0x46, 0x33, 0x16, 0x0c, 0x78, 0xe0, 0x50, 0x0c, 0x16, 0xbc, 0xf0, 0x10, 0x86, 0x89, 0x30, + 0x5c, 0x0a, 0xc3, 0x86, 0x0e, 0x1f, 0x06, 0x8c, 0xc0, 0x70, 0xe2, 0x84, 0x95, 0x10, 0xbc, 0x18, + 0x66, 0xa0, 0x30, 0xdc, 0x44, 0x60, 0x27, 0x07, 0x7e, 0xa2, 0x30, 0x2c, 0x0c, 0xc7, 0xc2, 0xb0, + 0x94, 0x06, 0x4f, 0x18, 0x4c, 0x81, 0x70, 0xe5, 0x86, 0x6d, 0x02, 0xbe, 0x1a, 0xb6, 0xf0, 0x8c, + 0x7f, 0x0d, 0xe2, 0x50, 0x0e, 0x5f, 0xc1, 0x39, 0x85, 0x7c, 0xb0, 0x16, 0x86, 0x77, 0x11, 0x98, + 0xcb, 0x85, 0x7b, 0x51, 0xd8, 0x4b, 0x83, 0xbf, 0x34, 0x31, 0x90, 0x2e, 0x0e, 0x7c, 0x62, 0xc1, + 0x29, 0x1e, 0xc2, 0x62, 0x12, 0x3d, 0x28, 0x24, 0x29, 0x29, 0x10, 0x09, 0x08, 0xcb, 0xb6, 0xd0, + 0xe8, 0x82, 0x8f, 0x8b, 0x0a, 0x8f, 0x0c, 0x21, 0x2a, 0x47, 0x98, 0x64, 0x09, 0x95, 0x74, 0xe1, + 0x92, 0x2e, 0x64, 0xa5, 0x09, 0x9b, 0x98, 0xd0, 0x09, 0x0a, 0x5f, 0x34, 0x8a, 0x2f, 0xcb, 0x39, + 0x96, 0x84, 0x23, 0xe2, 0x9a, 0xf6, 0xa4, 0x08, 0x76, 0xd6, 0xa6, 0xe8, 0xc7, 0x57, 0xbb, 0x99, + 0xb7, 0x72, 0xd5, 0xdb, 0x8d, 0x6d, 0x3b, 0xc4, 0x20, 0xa6, 0x63, 0x8b, 0x69, 0x39, 0x6f, 0x38, + 0xc5, 0x33, 0x63, 0x6e, 0x90, 0xa9, 0x3f, 0xbb, 0xed, 0x18, 0xc4, 0xda, 0x2b, 0xa2, 0xa2, 0xbd, + 0x09, 0x8d, 0xdb, 0x81, 0x2b, 0x1b, 0xfb, 0x57, 0x13, 0x54, 0x6f, 0xfe, 0x32, 0x2e, 0x86, 0xc4, + 0x5e, 0x2d, 0xea, 0x9d, 0xff, 0xd5, 0xdb, 0x00, 0xd7, 0xef, 0xc2, 0x8f, 0x3e, 0xbe, 0xf3, 0xbf, + 0x11, 0xfc, 0xfb, 0xde, 0xff, 0xc4, 0xab, 0x72, 0x66, 0x9c, 0x63, 0xb6, 0x1b, 0x96, 0x33, 0x11, + 0x77, 0x9c, 0xfc, 0x87, 0x79, 0x0d, 0x17, 0x1e, 0x1b, 0x0b, 0xcb, 0x17, 0xfc, 0x7b, 0xfe, 0xf9, + 0x1d, 0x1b, 0x96, 0xc7, 0xa9, 0x1d, 0xfa, 0x62, 0x2e, 0x9d, 0xae, 0x5c, 0x3a, 0xe5, 0xd2, 0xed, + 0x56, 0xe7, 0x09, 0x5b, 0x93, 0x08, 0x07, 0x03, 0xc7, 0xb1, 0xb0, 0x61, 0x8b, 0x2c, 0xfe, 0xda, + 0x7c, 0x74, 0xca, 0x52, 0x49, 0x52, 0x83, 0x44, 0xfc, 0x9d, 0xb8, 0x86, 0xb6, 0xb0, 0x3d, 0x62, + 0x0c, 0x2c, 0xbe, 0x49, 0xf3, 0xb1, 0xe9, 0x61, 0x3b, 0x00, 0x3a, 0x9f, 0x06, 0x2a, 0xb0, 0x30, + 0x81, 0x69, 0x41, 0xa6, 0x87, 0x8c, 0x21, 0x31, 0x9f, 0x2a, 0x20, 0x9d, 0xe1, 0x0c, 0x54, 0x49, + 0x3e, 0xb7, 0xa7, 0xa8, 0x6c, 0xe1, 0x04, 0xb7, 0xee, 0x4b, 0x45, 0xae, 0xa0, 0x63, 0x23, 0xee, + 0xd0, 0xf0, 0x70, 0x59, 0x30, 0xf7, 0x05, 0xb6, 0x34, 0x2f, 0x25, 0xd3, 0x9b, 0x9c, 0x13, 0x29, + 0x32, 0x81, 0xf4, 0x81, 0xe6, 0x77, 0x9f, 0xd2, 0xf5, 0x00, 0xe6, 0x5d, 0x3e, 0x0e, 0xbf, 0xab, + 0x38, 0xfc, 0xa2, 0x2e, 0xc6, 0x51, 0x70, 0xf8, 0x5d, 0x31, 0x0e, 0xbf, 0xab, 0x38, 0x7c, 0xc5, + 0xe1, 0xef, 0x99, 0xc3, 0x57, 0x41, 0xa8, 0x0a, 0x42, 0x55, 0x10, 0xaa, 0x82, 0x50, 0x15, 0x84, + 0xaa, 0x20, 0x54, 0x05, 0xa1, 0x07, 0x14, 0x84, 0x76, 0xdb, 0x3c, 0x2e, 0x26, 0x3c, 0x08, 0xed, + 0x1e, 0x4b, 0x10, 0xda, 0x15, 0x0e, 0x42, 0xb9, 0xf6, 0x9e, 0x01, 0x07, 0xc2, 0x37, 0x80, 0xec, + 0xae, 0xbf, 0x70, 0xee, 0x83, 0x64, 0x74, 0x0d, 0xd0, 0xa5, 0xac, 0xdd, 0xa1, 0x2c, 0x94, 0x25, + 0xfb, 0xbe, 0xe9, 0x61, 0xac, 0x77, 0x8d, 0xa1, 0xb3, 0x08, 0xd4, 0x46, 0xde, 0x8e, 0xd8, 0xe0, + 0xcf, 0xbb, 0xde, 0x0f, 0x9b, 0xf1, 0x51, 0x54, 0xb1, 0xed, 0xb0, 0x41, 0x17, 0x65, 0xed, 0x86, + 0xf5, 0xb0, 0xfb, 0x84, 0x5d, 0xf6, 0x4e, 0xd8, 0x55, 0x3b, 0xfa, 0x2e, 0xd8, 0xce, 0xce, 0x77, + 0xc1, 0xe6, 0x2c, 0x17, 0xaf, 0xc5, 0xdd, 0xe7, 0x26, 0xd8, 0xec, 0xe5, 0x14, 0xd3, 0x43, 0xcc, + 0x3d, 0xb0, 0x06, 0x21, 0x2e, 0x9c, 0x3e, 0x0b, 0x5a, 0xc3, 0xd8, 0x33, 0xbd, 0x32, 0xec, 0x19, + 0x03, 0x12, 0x87, 0x40, 0x9e, 0xd1, 0x21, 0x23, 0xc7, 0x36, 0x83, 0x23, 0x19, 0xfe, 0x4d, 0x18, + 0xc0, 0xcd, 0x16, 0x62, 0x0c, 0xf1, 0x57, 0xbc, 0xbc, 0xe1, 0x02, 0xf9, 0xfa, 0x01, 0x85, 0x73, + 0x85, 0xf3, 0xaa, 0xe1, 0x9c, 0x4b, 0xf9, 0xff, 0x86, 0x97, 0x6c, 0x3c, 0x37, 0x3e, 0x9a, 0x1e, + 0x61, 0x0a, 0x48, 0xe3, 0xd6, 0xb4, 0xdf, 0x5b, 0xd8, 0x5f, 0x14, 0x8f, 0x8e, 0xe5, 0xc6, 0xad, + 0xf1, 0x3d, 0xd6, 0xb2, 0xf3, 0xe3, 0xc5, 0x45, 0xef, 0xf5, 0xc5, 0x85, 0xfe, 0xfa, 0xfc, 0xb5, + 0x7e, 0x75, 0x79, 0xd9, 0xe9, 0x75, 0x2e, 0x29, 0x0f, 0xff, 0xe1, 0x8e, 0xb0, 0x8b, 0x47, 0x6f, + 0xfd, 0x8e, 0xdb, 0x0b, 0xcb, 0x82, 0x34, 0xfd, 0x87, 0x17, 0xf8, 0x2b, 0x01, 0x8d, 0xb8, 0x73, + 0x27, 0xdd, 0x47, 0x64, 0x9b, 0xea, 0x0b, 0x31, 0xdc, 0x63, 0xff, 0x05, 0x8f, 0x77, 0xe1, 0x0b, + 0xa4, 0xf8, 0xf7, 0x00, 0x1a, 0xa5, 0x31, 0x5b, 0x78, 0xf9, 0x64, 0x2d, 0xc0, 0xe5, 0x0a, 0x86, + 0xdd, 0x3c, 0x5b, 0x0d, 0xbc, 0x85, 0xde, 0x20, 0x5a, 0x48, 0xda, 0x78, 0xef, 0xba, 0x8e, 0x7b, + 0x8b, 0x3d, 0xcf, 0x98, 0x60, 0xb8, 0x1e, 0xfe, 0x8c, 0x2d, 0x83, 0x98, 0x4f, 0x18, 0x7d, 0x32, + 0xc8, 0x14, 0x69, 0xe8, 0xcb, 0x14, 0xbb, 0x18, 0xf9, 0x5d, 0x47, 0x03, 0x8c, 0xf0, 0x77, 0x63, + 0x48, 0xac, 0x25, 0xea, 0xa2, 0xb0, 0x13, 0x08, 0xdb, 0xc4, 0x35, 0xb1, 0x77, 0xc6, 0xd2, 0xdb, + 0x1c, 0xea, 0x2f, 0xae, 0xfa, 0xb0, 0x3f, 0x06, 0x6d, 0xb6, 0x1a, 0x04, 0x40, 0xd6, 0x45, 0xb4, + 0x5f, 0x42, 0xf3, 0x89, 0x8e, 0x7f, 0x17, 0xba, 0x05, 0x8c, 0x91, 0x84, 0x84, 0x94, 0x04, 0x94, + 0x9b, 0x81, 0xe7, 0x58, 0x0b, 0x72, 0xb4, 0x40, 0x11, 0x1d, 0xbf, 0x5c, 0xa0, 0xbc, 0xa2, 0x73, + 0x6c, 0x32, 0x69, 0x88, 0xbc, 0x98, 0x9c, 0xae, 0x65, 0xc1, 0x14, 0x84, 0x36, 0x72, 0x16, 0x03, + 0x0b, 0x7f, 0x0d, 0xcc, 0x27, 0x8d, 0x8c, 0x88, 0x35, 0x54, 0xb4, 0x84, 0xa2, 0x25, 0x14, 0x2d, + 0xa1, 0x68, 0x09, 0x15, 0xae, 0xa9, 0x70, 0x8d, 0x87, 0x96, 0xe8, 0x70, 0xf3, 0x12, 0x1d, 0x85, + 0x74, 0x85, 0xf4, 0x1a, 0x22, 0xbd, 0xcb, 0x8d, 0xf4, 0xae, 0x42, 0xba, 0x42, 0xfa, 0x21, 0x51, + 0x70, 0x1d, 0xc4, 0x40, 0xb6, 0x22, 0xe3, 0x0a, 0x93, 0x71, 0x9b, 0x98, 0xac, 0x30, 0x2d, 0xf7, + 0xf3, 0xfa, 0x4d, 0x8a, 0x9f, 0x53, 0xfc, 0xdc, 0x41, 0xf3, 0x73, 0x29, 0xa1, 0x51, 0x4c, 0x9d, + 0x62, 0xea, 0x64, 0x31, 0x75, 0xb9, 0x2c, 0x19, 0x8f, 0x0a, 0x86, 0x90, 0x77, 0x00, 0xda, 0x6e, + 0x6f, 0x84, 0x1d, 0x75, 0x0e, 0xaa, 0x43, 0xda, 0x8d, 0xb2, 0xe7, 0xbb, 0x00, 0x71, 0x37, 0x74, + 0x32, 0xf6, 0x74, 0x65, 0x28, 0xa3, 0x5c, 0xc7, 0x79, 0x6f, 0xa4, 0x1d, 0x6d, 0xc9, 0x6a, 0x44, + 0xdc, 0x51, 0x96, 0xb4, 0x24, 0xf2, 0xee, 0xc9, 0xb0, 0x16, 0x3c, 0x85, 0xf5, 0x56, 0xed, 0x6b, + 0x16, 0xec, 0x41, 0xe0, 0x71, 0x00, 0x01, 0x1f, 0x00, 0x3e, 0xc7, 0x4d, 0x6f, 0x04, 0xe8, 0xed, + 0x72, 0xa2, 0xbd, 0xab, 0xd0, 0xae, 0xd0, 0x7e, 0x18, 0x14, 0x87, 0x84, 0xc0, 0x7d, 0x13, 0x7d, + 0x50, 0xfc, 0x00, 0xba, 0xb7, 0xb8, 0x89, 0xd5, 0xdf, 0x39, 0x79, 0x04, 0x55, 0x46, 0xa4, 0x9e, + 0xe1, 0x8c, 0x7d, 0xc5, 0xcb, 0x0e, 0xdb, 0x61, 0x09, 0x5a, 0xd1, 0x1d, 0x16, 0x5d, 0x39, 0x2c, + 0xd5, 0x70, 0x58, 0x98, 0xb2, 0x06, 0x97, 0xb1, 0x8d, 0x6c, 0x51, 0xda, 0x7c, 0x32, 0x08, 0xc1, + 0xae, 0xcd, 0x3c, 0x8b, 0xd6, 0xf8, 0xda, 0xb9, 0x37, 0xb4, 0x3f, 0x6f, 0xb4, 0x7f, 0xe9, 0xda, + 0xd5, 0x59, 0xff, 0x87, 0xfc, 0xf1, 0xf6, 0x8b, 0xe1, 0xb9, 0x0b, 0xc2, 0x73, 0x57, 0xe1, 0x59, + 0xe1, 0xb9, 0x18, 0x9e, 0xbb, 0x3b, 0xc0, 0xf3, 0xcc, 0xb0, 0x47, 0x06, 0x71, 0xd8, 0x90, 0x5e, + 0x37, 0x54, 0xa8, 0xae, 0x05, 0xaa, 0x6f, 0xc3, 0xe5, 0x72, 0x97, 0x94, 0x58, 0xbf, 0x14, 0xe4, + 0x4b, 0x61, 0xf5, 0x37, 0x89, 0x9e, 0x2e, 0xca, 0xf0, 0x0d, 0xe8, 0x89, 0x1d, 0x76, 0x42, 0x47, + 0x28, 0x91, 0x03, 0x48, 0xe0, 0x00, 0x12, 0x37, 0x12, 0xe9, 0x40, 0x31, 0x22, 0x90, 0x8b, 0x03, + 0xc4, 0xb3, 0x39, 0x59, 0x0e, 0x1d, 0x7b, 0x9c, 0xcf, 0x01, 0x6e, 0x9a, 0x64, 0x73, 0x80, 0x7a, + 0x49, 0x1c, 0x60, 0xa5, 0x99, 0x3f, 0x5e, 0xbe, 0x2f, 0x57, 0x12, 0x93, 0xd3, 0x9c, 0x35, 0xe6, + 0xb5, 0xe0, 0x9d, 0x03, 0x96, 0xd3, 0x1c, 0x61, 0x9b, 0x98, 0x64, 0xe9, 0x62, 0xca, 0x82, 0xc6, + 0x1b, 0xed, 0x8c, 0xd6, 0x5d, 0x7f, 0xb4, 0xaa, 0xab, 0x1a, 0xf5, 0x4f, 0x1a, 0x91, 0xeb, 0x2e, + 0xe7, 0xc4, 0xb9, 0x01, 0x70, 0xb9, 0xab, 0x86, 0x95, 0xb1, 0xbb, 0x94, 0xa5, 0xaa, 0x85, 0xc1, + 0xcd, 0x5f, 0xca, 0xb2, 0xfd, 0xc7, 0x7c, 0xc9, 0xca, 0x94, 0x32, 0xda, 0x66, 0x82, 0x0f, 0xab, + 0x57, 0xbd, 0xa5, 0xdd, 0xb3, 0x93, 0x83, 0x25, 0xcd, 0xb0, 0x58, 0xc4, 0x47, 0xe3, 0x9f, 0x86, + 0xb5, 0x08, 0xee, 0x53, 0x62, 0xd7, 0xff, 0xe0, 0x2c, 0xea, 0x35, 0xda, 0x3e, 0xbd, 0x2e, 0xc0, + 0xf7, 0x88, 0x7d, 0xf6, 0x7c, 0x1f, 0xdf, 0x75, 0xc8, 0x14, 0xbb, 0x37, 0xd6, 0xc4, 0xd9, 0xc7, + 0xc7, 0x5d, 0xcf, 0x28, 0xca, 0xbc, 0xf5, 0x0b, 0xa7, 0x43, 0xb3, 0xa2, 0x8e, 0x10, 0x88, 0x6f, + 0xa1, 0xda, 0xef, 0xad, 0xd2, 0x7e, 0x4a, 0xfb, 0x29, 0xed, 0xa7, 0xb4, 0x5f, 0x0d, 0xb5, 0x5f, + 0x59, 0xd1, 0x1f, 0x5d, 0x9c, 0xf2, 0xe3, 0xbf, 0x0f, 0xb1, 0xe7, 0x20, 0x21, 0x83, 0x4d, 0xa2, + 0xf6, 0xd4, 0xbd, 0x20, 0xdb, 0x0d, 0x55, 0xe8, 0x50, 0x6e, 0xe8, 0x00, 0xb5, 0x9d, 0xca, 0x74, + 0x2a, 0xd3, 0xa9, 0x4c, 0xa7, 0x32, 0x9d, 0x87, 0x11, 0x38, 0x8c, 0xb0, 0x37, 0x74, 0xcd, 0x39, + 0x35, 0x3f, 0x1e, 0x5f, 0xba, 0xa8, 0xb1, 0xd2, 0x82, 0xb5, 0xd7, 0x82, 0x7b, 0x4a, 0x42, 0x64, + 0x9a, 0x51, 0x95, 0x81, 0x08, 0x7c, 0x50, 0xaa, 0xcf, 0xc7, 0xf0, 0x43, 0xb7, 0x9e, 0x05, 0xfa, + 0xa2, 0xee, 0xd8, 0xc8, 0x90, 0x8c, 0x84, 0x17, 0x1a, 0x36, 0xd9, 0xf1, 0x8e, 0x64, 0x73, 0x5c, + 0xed, 0xad, 0xc8, 0xe6, 0x58, 0x9a, 0xff, 0x69, 0x8c, 0x66, 0xa6, 0xad, 0x79, 0xc4, 0x20, 0x80, + 0x2b, 0xbe, 0xe3, 0x8d, 0xf3, 0x76, 0xc0, 0x02, 0xca, 0x9d, 0x37, 0xb0, 0x1d, 0x9c, 0xef, 0xc9, + 0x6c, 0xd0, 0xaf, 0x58, 0x4e, 0xda, 0x1c, 0xd7, 0x3b, 0x19, 0x9d, 0x85, 0x95, 0xb2, 0x95, 0x3b, + 0x1b, 0x27, 0x09, 0x31, 0xbe, 0xa0, 0x1d, 0xa6, 0xb1, 0x17, 0xec, 0x3b, 0x44, 0x1b, 0x5f, 0x9c, + 0xbb, 0xd0, 0x9e, 0x80, 0xb6, 0x0d, 0x76, 0x82, 0x1c, 0x5c, 0x88, 0x41, 0xc0, 0x9e, 0xc1, 0x6e, + 0xe0, 0x7e, 0x98, 0x5e, 0x3e, 0x66, 0x81, 0xbe, 0x5b, 0xe3, 0x8b, 0xf3, 0x81, 0xb2, 0xe3, 0x3f, + 0xe9, 0x1b, 0xad, 0xbe, 0x77, 0x8d, 0xba, 0x80, 0x2e, 0xae, 0x06, 0x73, 0x8d, 0x3a, 0x3b, 0xd9, + 0xd7, 0xc8, 0x51, 0x8e, 0x9c, 0x7e, 0x5e, 0x0f, 0xee, 0xd1, 0x46, 0xd8, 0x6a, 0x36, 0xcf, 0xd0, + 0x1b, 0x74, 0x12, 0x8e, 0xf8, 0xa4, 0x85, 0x0c, 0x7b, 0x84, 0x3c, 0x62, 0xb8, 0xc4, 0xd3, 0xbe, + 0x99, 0x64, 0xda, 0x3c, 0x3b, 0x6b, 0xfb, 0x06, 0xea, 0x14, 0x9d, 0x78, 0x4b, 0x8f, 0xe0, 0x99, + 0x7e, 0xd2, 0x6a, 0x21, 0xc7, 0x45, 0xb6, 0x43, 0x9a, 0xac, 0x76, 0x10, 0x34, 0x70, 0x9d, 0xee, + 0xa2, 0x49, 0xc6, 0xe6, 0x6c, 0x13, 0x14, 0x89, 0x48, 0xb0, 0x2e, 0x7a, 0x91, 0x83, 0x5d, 0xc2, + 0x0a, 0x29, 0x57, 0x39, 0x51, 0xa6, 0x60, 0x37, 0x85, 0xb6, 0xf3, 0xb7, 0x5e, 0x51, 0xf4, 0x90, + 0x8b, 0xc7, 0xd8, 0x05, 0x95, 0xd1, 0xe7, 0xc0, 0xf2, 0xe7, 0x5f, 0xde, 0xa1, 0xee, 0x8f, 0xbd, + 0xf3, 0x6b, 0xf4, 0x65, 0x8a, 0xd1, 0x87, 0xb5, 0xbb, 0xe3, 0xa1, 0x5f, 0x5d, 0x67, 0x31, 0x47, + 0xb7, 0x1f, 0xde, 0x22, 0x0d, 0x99, 0xe3, 0x1b, 0x7f, 0xc6, 0xee, 0x88, 0x41, 0x16, 0x5e, 0xc9, + 0x5b, 0xcc, 0x37, 0xa3, 0xdc, 0xe5, 0x2e, 0x73, 0x81, 0x69, 0x28, 0x0f, 0x03, 0x47, 0x1d, 0xcf, + 0x2a, 0x97, 0x47, 0xd4, 0xe5, 0x61, 0x2f, 0x27, 0x82, 0xee, 0x29, 0xfd, 0x88, 0xed, 0x49, 0x10, + 0x9f, 0x15, 0xd6, 0x33, 0xb7, 0x26, 0xfc, 0xd6, 0x85, 0x90, 0xec, 0xe3, 0xb8, 0x74, 0xeb, 0x17, + 0xd7, 0x18, 0xfa, 0xc3, 0xfd, 0xd9, 0x9c, 0x98, 0xac, 0x62, 0x0b, 0xc9, 0x29, 0xc3, 0x93, 0xe0, + 0x70, 0x38, 0xb5, 0x16, 0x02, 0x27, 0x25, 0xe6, 0x87, 0xe0, 0xfc, 0x43, 0xed, 0x5e, 0x5e, 0x56, + 0x6f, 0xb0, 0x7b, 0x50, 0x5c, 0x51, 0xa0, 0xad, 0x11, 0x1a, 0xe0, 0xd3, 0x81, 0x79, 0xd8, 0x5e, + 0xa9, 0xaf, 0xda, 0xab, 0xaf, 0x12, 0xe8, 0xb8, 0xac, 0xfd, 0xe9, 0x64, 0x01, 0xd8, 0x9b, 0x4e, + 0x16, 0x0a, 0x51, 0xf5, 0x47, 0xd4, 0xc2, 0xb4, 0x49, 0xa7, 0x07, 0x40, 0x54, 0x8f, 0xd2, 0xe4, + 0xb3, 0x61, 0x4f, 0xf0, 0xde, 0x2c, 0xa1, 0x7e, 0x3c, 0x96, 0xb0, 0x77, 0x79, 0x79, 0xae, 0x6c, + 0xa1, 0xdf, 0xc9, 0x15, 0xbb, 0xcd, 0x50, 0x52, 0x41, 0x2b, 0xa5, 0xa5, 0x6a, 0xaf, 0xa5, 0x36, + 0x9e, 0x0c, 0x65, 0x45, 0xeb, 0xe2, 0xb9, 0x9f, 0x1f, 0x91, 0xe7, 0xae, 0x1f, 0x8e, 0xb2, 0x92, + 0x71, 0xbc, 0xb0, 0x39, 0x9b, 0xcc, 0x88, 0xfe, 0xbc, 0xa2, 0x32, 0x9f, 0x2d, 0xa7, 0xa9, 0x3f, + 0x77, 0xee, 0x75, 0xed, 0xaa, 0x1f, 0xfc, 0xf3, 0xdc, 0x6d, 0xde, 0xeb, 0xda, 0xc5, 0xea, 0x3f, + 0x2e, 0xef, 0x75, 0xed, 0xb2, 0xdf, 0x7a, 0xbe, 0xef, 0x44, 0x7f, 0x0f, 0x7e, 0x6c, 0x3d, 0x63, + 0x32, 0xc5, 0xae, 0x8d, 0x89, 0xd6, 0x0c, 0x7e, 0xd1, 0x7c, 0x78, 0x18, 0xb5, 0xfe, 0xd2, 0x4f, + 0x3b, 0x2f, 0xcd, 0xf6, 0xbd, 0x31, 0x18, 0x8e, 0xfa, 0xad, 0x9f, 0x9a, 0xed, 0xad, 0x3f, 0xb5, + 0x7e, 0x6a, 0x37, 0xb7, 0x9b, 0xb7, 0x9e, 0x9b, 0xfe, 0xd7, 0x3b, 0x7d, 0xff, 0x37, 0xcf, 0xcd, + 0x4e, 0xf7, 0x5e, 0xd7, 0x7e, 0xec, 0xb7, 0x5a, 0xad, 0x67, 0xd3, 0x1d, 0x88, 0x75, 0xcd, 0x32, + 0x26, 0x5b, 0x9f, 0xe9, 0x06, 0x9f, 0xd1, 0x75, 0xbd, 0xd5, 0x6a, 0x95, 0x73, 0x9e, 0xd2, 0x5b, + 0x0c, 0xf2, 0xd3, 0x94, 0x69, 0x5d, 0x1c, 0x6f, 0x5d, 0xb1, 0x82, 0x3d, 0xca, 0x36, 0xac, 0xbf, + 0xc0, 0xae, 0xaf, 0x0d, 0xc8, 0x88, 0x0a, 0x66, 0xbc, 0x10, 0xef, 0x85, 0xd0, 0x0d, 0x08, 0x31, + 0xde, 0xaf, 0x59, 0xbd, 0x14, 0x2a, 0x16, 0x65, 0xb0, 0xd8, 0x7b, 0x2e, 0x94, 0x42, 0xc3, 0x2a, + 0xd0, 0x58, 0x48, 0xaf, 0x90, 0x02, 0xc7, 0x27, 0x02, 0x66, 0x66, 0x37, 0x09, 0x31, 0x48, 0x86, + 0x76, 0xd3, 0x75, 0x9e, 0x4c, 0x6d, 0xf4, 0x14, 0x5f, 0xc6, 0x36, 0x7a, 0x0c, 0x9e, 0xb9, 0xe5, + 0x70, 0x58, 0x10, 0x5f, 0x26, 0x77, 0x93, 0x1d, 0xe0, 0xc9, 0xe8, 0x6e, 0x2b, 0x00, 0x56, 0x66, + 0x57, 0x5a, 0x72, 0x8c, 0x81, 0x3c, 0x81, 0x8b, 0xa8, 0x61, 0x19, 0x60, 0xb8, 0x6f, 0x9c, 0xc2, + 0x36, 0x20, 0x23, 0xcc, 0x91, 0x14, 0x16, 0xcc, 0x0b, 0x6f, 0xc4, 0x41, 0x24, 0x3f, 0x4c, 0x93, + 0x57, 0xb1, 0x3c, 0xb1, 0xa8, 0x1e, 0xcd, 0xd3, 0xa9, 0xa2, 0x79, 0xe3, 0xc2, 0x6a, 0x36, 0x57, + 0xe5, 0x16, 0xcd, 0x23, 0xc3, 0x45, 0x86, 0xaf, 0x25, 0xbb, 0x55, 0xbf, 0x84, 0x52, 0x6b, 0x90, + 0xdc, 0xa3, 0x60, 0xd2, 0x4a, 0x39, 0x11, 0xca, 0x89, 0xe0, 0x84, 0x0b, 0x94, 0x31, 0xe1, 0x65, + 0x4e, 0x04, 0xac, 0x04, 0x0f, 0x93, 0x92, 0xa2, 0x19, 0x3a, 0x9c, 0xca, 0x56, 0x94, 0x6d, 0x10, + 0x67, 0x1d, 0x38, 0x1d, 0x17, 0x6e, 0xc6, 0x25, 0xcd, 0xbc, 0x40, 0x73, 0xa6, 0x55, 0x98, 0x94, + 0x0a, 0x2b, 0x6c, 0xd3, 0x1e, 0xe1, 0xef, 0x70, 0x55, 0x1d, 0x36, 0x57, 0x4a, 0x5a, 0x29, 0x69, + 0xc6, 0xfc, 0x2f, 0x4c, 0x9b, 0x9c, 0x77, 0x39, 0xf4, 0xf3, 0x6b, 0x40, 0x53, 0x58, 0x1e, 0x6e, + 0xd7, 0xda, 0x59, 0x57, 0xda, 0x79, 0x7b, 0x4a, 0xae, 0xae, 0xae, 0xae, 0x94, 0x7a, 0x3e, 0x90, + 0x90, 0x56, 0x3f, 0x8c, 0x58, 0x36, 0xce, 0x8b, 0xa3, 0xc0, 0x8e, 0x45, 0xf1, 0x9a, 0x8e, 0xc6, + 0x8e, 0x8b, 0x56, 0x9d, 0x43, 0x2c, 0xf6, 0xfc, 0xf8, 0x62, 0x5c, 0x81, 0xa9, 0x3b, 0xc2, 0xd8, + 0x97, 0x40, 0x8c, 0x64, 0x04, 0x47, 0xca, 0x96, 0x35, 0xe5, 0x48, 0x29, 0x47, 0x8a, 0xf3, 0x9c, + 0x7e, 0x2a, 0xda, 0x05, 0x44, 0x47, 0x7c, 0xe7, 0xf6, 0xd3, 0xc0, 0x30, 0x35, 0x00, 0x86, 0x93, + 0xae, 0x81, 0x07, 0xb6, 0x56, 0x7c, 0x16, 0x2b, 0xd1, 0xb1, 0x81, 0x6b, 0x8e, 0x26, 0x78, 0xd4, + 0x28, 0xc3, 0x2d, 0x12, 0xec, 0x92, 0xeb, 0x2c, 0x08, 0x57, 0x8f, 0x40, 0x2d, 0xfb, 0xc7, 0xc8, + 0xb2, 0xe7, 0xfa, 0x16, 0xa6, 0x3b, 0x38, 0x09, 0x3c, 0x90, 0xdc, 0x16, 0xeb, 0x6d, 0x05, 0x8c, + 0x66, 0x96, 0x31, 0x61, 0xb5, 0x30, 0xc7, 0xac, 0x4f, 0xd9, 0x73, 0x46, 0x8b, 0xa7, 0xa9, 0xbd, + 0x17, 0x37, 0xc8, 0x97, 0x59, 0xdf, 0x49, 0x43, 0xde, 0x62, 0x3e, 0x77, 0x5c, 0x82, 0x47, 0xc8, + 0xb1, 0x11, 0x99, 0x9a, 0x9e, 0x72, 0x7c, 0x52, 0xc6, 0x01, 0x34, 0x59, 0xfb, 0x75, 0x75, 0x4e, + 0x4b, 0x93, 0xb4, 0x33, 0xf4, 0xb7, 0x37, 0xe8, 0xc4, 0x72, 0x86, 0x86, 0xa5, 0xcd, 0xcc, 0x60, + 0x6d, 0x46, 0xd8, 0x5b, 0x49, 0x4f, 0x53, 0x8a, 0x94, 0xed, 0x01, 0xff, 0xa9, 0xf1, 0x20, 0xd3, + 0x53, 0xf2, 0x00, 0x94, 0x07, 0xa1, 0xc9, 0x3b, 0xd4, 0x50, 0x40, 0xe4, 0x9a, 0x59, 0x1a, 0x7f, + 0x5a, 0xf2, 0xd5, 0xb2, 0x17, 0xfa, 0xd5, 0x21, 0x5d, 0x25, 0x1b, 0xe1, 0xab, 0x0d, 0xd8, 0xf0, + 0x86, 0x98, 0x65, 0x43, 0x82, 0xa7, 0x1f, 0xef, 0xe2, 0xaf, 0x92, 0x59, 0xf0, 0x25, 0x63, 0x5b, + 0xaf, 0x2a, 0xf7, 0x92, 0x58, 0x46, 0xee, 0x42, 0x2f, 0x19, 0x8b, 0x94, 0x53, 0xe2, 0x65, 0xfe, + 0xd4, 0xa3, 0x54, 0x77, 0xf1, 0xff, 0xba, 0xe3, 0xc2, 0x2e, 0x9e, 0x3f, 0x59, 0xc3, 0xc7, 0x20, + 0x2a, 0xa8, 0x76, 0x89, 0x97, 0x44, 0x4f, 0x65, 0x15, 0x7b, 0x09, 0x5f, 0xaa, 0x85, 0x2f, 0x65, + 0xef, 0x67, 0x8d, 0xb7, 0xae, 0xd8, 0x7e, 0x56, 0xc6, 0x42, 0xf2, 0x3a, 0x04, 0x7b, 0xdc, 0xd9, + 0x4a, 0x5f, 0x68, 0x31, 0x9b, 0xc7, 0xdc, 0xe3, 0x3a, 0x18, 0x8f, 0xb4, 0x70, 0xaf, 0xcc, 0x08, 0x4e, 0x9c, 0xc5, 0x1f, 0x92, 0xb9, 0xc7, 0x35, 0x50, 0x73, 0x87, 0xb5, 0xc5, 0x15, 0x08, 0xcf, - 0x03, 0x60, 0xee, 0x60, 0xf0, 0xdd, 0x17, 0x87, 0x37, 0x70, 0x9c, 0x29, 0x36, 0xb8, 0x76, 0xab, - 0x74, 0xca, 0xc8, 0xf5, 0xcf, 0x9f, 0x7a, 0xda, 0x1c, 0x86, 0xa9, 0x84, 0x7d, 0xd4, 0x60, 0xca, - 0x48, 0xc1, 0x5f, 0xc1, 0x3f, 0xdb, 0x82, 0x73, 0xdf, 0x8b, 0x09, 0x68, 0x0b, 0x3d, 0x94, 0x14, - 0x3d, 0x10, 0xae, 0x91, 0x76, 0xf6, 0xb7, 0xc6, 0x5e, 0xd2, 0x43, 0x36, 0xfe, 0x4e, 0xb4, 0x89, - 0x33, 0x87, 0x8b, 0x5e, 0xf4, 0x84, 0x92, 0x3b, 0x25, 0x77, 0xf5, 0x95, 0x3b, 0x1f, 0xc6, 0x13, - 0x67, 0xbe, 0x37, 0xc1, 0x73, 0xbe, 0xd9, 0xd8, 0x85, 0x4b, 0x5d, 0xd8, 0x5c, 0x89, 0x9c, 0x12, - 0xb9, 0xfa, 0x8a, 0x5c, 0x80, 0xe1, 0x72, 0x05, 0x4e, 0x84, 0xfd, 0x0c, 0xba, 0x85, 0x62, 0x1e, - 0x25, 0x62, 0x98, 0xb8, 0x92, 0x29, 0x51, 0x18, 0x67, 0x56, 0x5f, 0x8a, 0x74, 0xfe, 0xd4, 0x6b, - 0x03, 0xe8, 0x13, 0x06, 0xd7, 0x36, 0x7f, 0xea, 0x3d, 0xde, 0x05, 0x6f, 0xf9, 0x9c, 0x2f, 0x74, - 0xbb, 0xbc, 0xa5, 0x24, 0xcd, 0xd8, 0xb1, 0x47, 0x00, 0x21, 0x0a, 0xa7, 0xd8, 0x18, 0x4d, 0x2d, - 0x8f, 0xe4, 0x93, 0x85, 0x51, 0x8b, 0x1d, 0x13, 0x86, 0x39, 0xdf, 0xad, 0x18, 0x59, 0x18, 0xf5, - 0x52, 0x16, 0x51, 0x88, 0x6d, 0xe2, 0x2e, 0xd9, 0x0c, 0x61, 0xd8, 0xac, 0x62, 0x65, 0x50, 0x28, - 0x4b, 0x56, 0x23, 0x5a, 0x30, 0x7f, 0x49, 0xc5, 0x0c, 0xc1, 0x0e, 0x4b, 0x81, 0x49, 0xb4, 0x28, - 0x5d, 0xb0, 0x45, 0x39, 0xdf, 0xb9, 0xf9, 0x00, 0x55, 0x90, 0xf8, 0x66, 0x91, 0x89, 0x66, 0x46, - 0x6c, 0x28, 0x43, 0x9e, 0x12, 0xad, 0x8b, 0x14, 0x58, 0x1f, 0x39, 0x0e, 0x6d, 0x01, 0x07, 0x86, - 0x5b, 0x8f, 0xe2, 0xeb, 0x4a, 0x96, 0x0f, 0x47, 0x96, 0x6b, 0xe6, 0x1d, 0xee, 0xce, 0xad, 0xa2, - 0xf9, 0x18, 0xb9, 0xae, 0xd5, 0xc7, 0x4c, 0x4c, 0xe5, 0xbb, 0x57, 0x2e, 0x1e, 0x69, 0x4e, 0x70, - 0x74, 0xd3, 0x98, 0xd2, 0xdd, 0xac, 0x44, 0xcb, 0xdd, 0x5e, 0x03, 0xfe, 0xb8, 0xfa, 0x7e, 0xf5, - 0xbd, 0xad, 0xd4, 0x85, 0x7b, 0x48, 0xc2, 0xb5, 0xe0, 0x94, 0xd1, 0xaf, 0xc5, 0xf4, 0x75, 0xd6, - 0xb6, 0x81, 0xd5, 0x62, 0x7d, 0xb0, 0x3d, 0x62, 0x84, 0x15, 0xca, 0xb3, 0x47, 0xe5, 0x87, 0xb3, - 0x21, 0xfc, 0x36, 0x7b, 0x38, 0x02, 0x68, 0x41, 0x30, 0x64, 0x79, 0x44, 0x9b, 0x59, 0xf6, 0x2c, - 0xe3, 0x74, 0xd2, 0x66, 0x00, 0xb1, 0x46, 0xbb, 0x76, 0xd4, 0x2d, 0x8f, 0x3c, 0x66, 0x7e, 0xba, - 0x6a, 0xe8, 0x89, 0x75, 0x54, 0xa6, 0xbb, 0x6e, 0x05, 0x7b, 0xbc, 0x01, 0x0e, 0xbb, 0xdf, 0xb0, - 0x62, 0xd9, 0x7c, 0xfa, 0xe2, 0xd5, 0xc9, 0xd2, 0xd3, 0x16, 0x57, 0xcc, 0xd8, 0xb3, 0xeb, 0x55, - 0x11, 0xe2, 0x76, 0x38, 0x2a, 0x55, 0x05, 0xcd, 0x6b, 0xc6, 0xb2, 0xc2, 0x00, 0x72, 0x00, 0x24, - 0x2b, 0x08, 0x40, 0x35, 0xe2, 0x58, 0x4b, 0xc8, 0x2a, 0xf8, 0x00, 0xee, 0xf2, 0xe1, 0xbd, 0xab, - 0xf0, 0xae, 0xf0, 0x5e, 0x57, 0xbc, 0x7f, 0xc5, 0x4b, 0x66, 0xa8, 0x95, 0xe8, 0xf6, 0xfa, 0x01, - 0x85, 0x79, 0x85, 0xf9, 0x2a, 0x63, 0x5e, 0x24, 0x91, 0x45, 0xc7, 0xf6, 0xc1, 0x71, 0x8c, 0xe5, - 0xa5, 0xa8, 0x62, 0xa1, 0x5a, 0x9b, 0x1e, 0x15, 0x30, 0x78, 0x08, 0xcb, 0x23, 0xb7, 0xc1, 0x6b, - 0x1e, 0xdf, 0xaf, 0x5e, 0xb3, 0x77, 0x3e, 0x25, 0x37, 0x0a, 0x85, 0x0e, 0x85, 0x33, 0x20, 0xd6, - 0x4c, 0x67, 0x31, 0x98, 0x62, 0x8d, 0x7a, 0xa5, 0x7e, 0x4e, 0x7b, 0x15, 0x26, 0xab, 0x30, 0x59, - 0x85, 0xc9, 0x3b, 0x0a, 0x93, 0xf9, 0xa2, 0x06, 0xe5, 0x40, 0x29, 0x07, 0xaa, 0xe6, 0x41, 0x43, - 0x87, 0x3b, 0x6a, 0x50, 0xd4, 0x90, 0x42, 0x7d, 0xdd, 0x51, 0xdf, 0xe5, 0x46, 0xbd, 0x22, 0x88, - 0x14, 0xea, 0x0f, 0x36, 0x58, 0xee, 0x20, 0x06, 0xca, 0x55, 0xd8, 0x2c, 0x12, 0x5b, 0xc6, 0xa2, - 0x38, 0x49, 0x11, 0xf4, 0x2f, 0xc1, 0x0b, 0x7f, 0xc7, 0xcb, 0x2a, 0x86, 0xd2, 0xf9, 0x41, 0x2b, - 0xf7, 0xf0, 0xc0, 0xe1, 0xf5, 0xc2, 0xb6, 0xfe, 0x67, 0x81, 0x19, 0x31, 0xf5, 0xaa, 0xd1, 0x3e, - 0x02, 0xe9, 0xcc, 0x4f, 0x57, 0x31, 0x90, 0x5e, 0x75, 0x54, 0xda, 0x39, 0x72, 0xec, 0x3e, 0x51, - 0x4e, 0x76, 0x6c, 0x26, 0x2c, 0x6c, 0x57, 0xc5, 0x30, 0x3a, 0x77, 0xe9, 0xea, 0x16, 0x46, 0xe7, - 0x2d, 0x6d, 0x49, 0x61, 0xb4, 0x35, 0xe7, 0x39, 0xc1, 0x5a, 0x4b, 0xb7, 0x8a, 0x09, 0x8d, 0x43, - 0x71, 0xab, 0x58, 0xd0, 0x39, 0xee, 0x60, 0x82, 0x7a, 0x11, 0x68, 0xaa, 0xcf, 0x8c, 0xeb, 0x23, - 0x15, 0xda, 0x15, 0xda, 0x2b, 0x8d, 0xf6, 0xb9, 0xe3, 0x12, 0x38, 0xda, 0x83, 0xd6, 0x0a, 0xed, - 0x0a, 0xed, 0x42, 0xd7, 0x13, 0x50, 0xaf, 0x05, 0xdf, 0xc6, 0x4f, 0x4f, 0x5d, 0x4f, 0x00, 0x7d, - 0x41, 0xf5, 0xaf, 0x27, 0xe0, 0xb8, 0x66, 0xbc, 0x0a, 0xd3, 0x52, 0xc7, 0x42, 0x87, 0xb4, 0x9b, - 0xc9, 0x8f, 0xf7, 0x50, 0x2f, 0x47, 0x45, 0xe5, 0x46, 0xc4, 0x3a, 0x14, 0xbe, 0xc5, 0x7b, 0x13, - 0x08, 0x21, 0x80, 0xc5, 0x2c, 0x6a, 0x76, 0xc0, 0xb6, 0x4d, 0x9e, 0xd9, 0x59, 0x0f, 0x6c, 0xd7, - 0x37, 0xf9, 0x97, 0x4c, 0xf0, 0x85, 0x33, 0xd9, 0xa6, 0xd2, 0x17, 0x6c, 0xd6, 0xeb, 0x1f, 0xc1, - 0x5b, 0x1e, 0xef, 0xc2, 0xb7, 0x54, 0x82, 0xca, 0xcb, 0x27, 0xac, 0x00, 0x23, 0x81, 0xd0, 0x76, - 0x33, 0xc3, 0x36, 0x0d, 0xe2, 0xb8, 0x4b, 0xea, 0x5e, 0x98, 0x44, 0xab, 0x1d, 0x13, 0x77, 0x94, - 0x6f, 0x57, 0x8c, 0xb9, 0x4b, 0xf4, 0x54, 0x16, 0x75, 0x37, 0x74, 0x6c, 0xc0, 0x41, 0xd4, 0xa0, - 0x55, 0xc5, 0x68, 0x3b, 0xc6, 0xc2, 0xd5, 0x88, 0xb7, 0xa3, 0x2f, 0x6c, 0x49, 0xc4, 0xdd, 0x93, - 0xef, 0x82, 0x71, 0xec, 0x07, 0x58, 0xb5, 0xaf, 0x59, 0x90, 0x07, 0x04, 0xc9, 0x01, 0x44, 0x79, - 0x30, 0x10, 0x1d, 0x37, 0xa9, 0x11, 0x60, 0xb8, 0xcb, 0x89, 0xf9, 0xae, 0xc2, 0xbc, 0xc2, 0xfc, - 0x21, 0xed, 0x06, 0x90, 0xe0, 0x15, 0xc7, 0x27, 0xbe, 0x4d, 0x71, 0x0e, 0xe8, 0xae, 0xe4, 0x6d, - 0xec, 0x2d, 0x8f, 0xef, 0xfc, 0xb7, 0x14, 0xa8, 0xac, 0xf1, 0x15, 0x2f, 0x3b, 0x6c, 0x47, 0x26, - 0x68, 0x55, 0xb1, 0xa2, 0x16, 0xca, 0x91, 0xa1, 0xa1, 0x35, 0x02, 0x09, 0xc5, 0xc7, 0x2c, 0xa5, - 0x00, 0x06, 0xa5, 0x0d, 0xb4, 0x0c, 0x5c, 0xe3, 0xde, 0xd0, 0xfe, 0xec, 0xff, 0xd8, 0xe0, 0x0d, - 0x67, 0x41, 0x80, 0x5f, 0x4d, 0x36, 0x1b, 0xf3, 0xeb, 0x86, 0x0a, 0xf6, 0x0a, 0xf6, 0x82, 0x26, - 0x48, 0xf0, 0xb2, 0x8d, 0x0c, 0x6d, 0xab, 0x2e, 0xdb, 0xd8, 0xb6, 0x9f, 0x5c, 0xec, 0xcb, 0x6d, - 0x2e, 0xd4, 0x72, 0xf8, 0x97, 0xf1, 0x8c, 0x68, 0x9b, 0xab, 0x3d, 0xf2, 0x19, 0x98, 0x64, 0xbb, - 0x1d, 0x73, 0x30, 0xfe, 0xcc, 0x57, 0xbc, 0xc8, 0x4b, 0xd8, 0x45, 0x59, 0xac, 0x0b, 0x75, 0x2f, - 0x01, 0x64, 0x0f, 0xc1, 0xde, 0xb4, 0x76, 0xde, 0x52, 0xd5, 0x48, 0x5d, 0xe7, 0x2c, 0xa5, 0x98, - 0x9e, 0x86, 0xeb, 0xe0, 0xfc, 0x6a, 0x3e, 0x29, 0x39, 0x7b, 0x4d, 0xf7, 0x3d, 0xd8, 0x35, 0x7b, - 0xf8, 0x3c, 0x09, 0xea, 0x7d, 0xb9, 0x90, 0x7b, 0x72, 0x15, 0x1a, 0x15, 0x1a, 0xd7, 0x68, 0xbc, - 0xf7, 0xd1, 0xf8, 0x66, 0xb8, 0x70, 0x5d, 0x6c, 0x93, 0x66, 0x6b, 0x7d, 0xd1, 0x60, 0x7f, 0xd3, - 0x22, 0xbc, 0xad, 0xb6, 0xb4, 0x74, 0x1d, 0xe8, 0xe2, 0x53, 0x8e, 0x30, 0x3e, 0x58, 0xa0, 0xe6, - 0x59, 0x30, 0x12, 0xc3, 0x9c, 0x59, 0xb6, 0xe6, 0x11, 0x83, 0x60, 0xf4, 0x06, 0x3d, 0x34, 0xc2, - 0xeb, 0x6f, 0x1e, 0x1a, 0x90, 0x20, 0x5f, 0xe8, 0xd6, 0xc4, 0xa8, 0x17, 0x5f, 0x26, 0x18, 0xcd, - 0x0c, 0xdb, 0x18, 0x07, 0xae, 0xcf, 0xe6, 0x52, 0x3f, 0x34, 0x34, 0x7c, 0x37, 0x04, 0x0d, 0x30, - 0x32, 0x2d, 0x2f, 0xb8, 0x8c, 0xe7, 0x0c, 0xca, 0xc7, 0x08, 0xdc, 0x8e, 0x28, 0xe3, 0x56, 0xc4, + 0x03, 0x60, 0xee, 0x60, 0xf0, 0xdd, 0x17, 0x87, 0x37, 0x70, 0x1c, 0x0b, 0x1b, 0x5c, 0xbb, 0x55, + 0x3a, 0x65, 0xe4, 0xfa, 0xe7, 0x4f, 0x3d, 0x6d, 0x0e, 0xc3, 0x54, 0xc2, 0x3e, 0x6a, 0x30, 0x65, + 0xa4, 0xe0, 0xaf, 0xe0, 0x9f, 0x6d, 0xc1, 0xb9, 0xef, 0xc5, 0x04, 0xb4, 0x85, 0x1e, 0x4a, 0x8a, + 0x1e, 0x08, 0xd7, 0x48, 0x3b, 0xfb, 0x7b, 0x63, 0x2f, 0xe9, 0x21, 0x1b, 0x7f, 0x27, 0xda, 0xd4, + 0x99, 0xc3, 0x45, 0x2f, 0x7a, 0x42, 0xc9, 0x9d, 0x92, 0xbb, 0xfa, 0xca, 0x9d, 0x0f, 0xe3, 0xa9, + 0x33, 0xdf, 0x9b, 0xe0, 0x39, 0xdf, 0x6c, 0xec, 0xc2, 0xa5, 0x2e, 0x6c, 0xae, 0x44, 0x4e, 0x89, + 0x5c, 0x7d, 0x45, 0x2e, 0xc0, 0x70, 0xb9, 0x02, 0x27, 0xc2, 0x7e, 0x06, 0xdd, 0x42, 0x31, 0x8f, + 0x12, 0x31, 0x4c, 0x5c, 0xc9, 0x94, 0x28, 0x8c, 0x33, 0xab, 0x2f, 0x45, 0x3a, 0x7f, 0xea, 0xb5, + 0x01, 0xf4, 0x09, 0x83, 0x6b, 0x9b, 0x3f, 0xf5, 0x1e, 0xef, 0x82, 0xb7, 0x7c, 0xce, 0x17, 0xba, + 0x5d, 0xde, 0x52, 0x92, 0x66, 0xec, 0xd8, 0x23, 0x80, 0x10, 0x85, 0x16, 0x36, 0xc6, 0x96, 0xe9, + 0x91, 0x7c, 0xb2, 0x30, 0x6a, 0xb1, 0x63, 0xc2, 0x30, 0xe7, 0xbb, 0x15, 0x23, 0x0b, 0xa3, 0x5e, + 0xca, 0x22, 0x0a, 0xb1, 0x4d, 0xdc, 0x25, 0x9b, 0x21, 0x0c, 0x9b, 0x55, 0xac, 0x0c, 0x0a, 0x65, + 0xc9, 0x6a, 0x44, 0x0b, 0xe6, 0x2f, 0xa9, 0x98, 0x21, 0xd8, 0x61, 0x29, 0x30, 0x89, 0x16, 0xa5, + 0x0b, 0xb6, 0x28, 0xe7, 0x3b, 0x37, 0x1f, 0xa0, 0x0a, 0x12, 0xdf, 0x4c, 0x32, 0xd5, 0x46, 0x11, + 0x1b, 0xca, 0x90, 0xa7, 0x44, 0xeb, 0x22, 0x05, 0xd6, 0xc7, 0x8e, 0x43, 0x5b, 0xc0, 0x81, 0xe1, + 0xd6, 0xa3, 0xf8, 0xba, 0x92, 0xe5, 0xc3, 0x91, 0xe5, 0x9a, 0x79, 0x87, 0xbb, 0x73, 0xab, 0x68, + 0x3e, 0x46, 0xae, 0x6b, 0xf5, 0x31, 0x13, 0x53, 0xf9, 0xee, 0x95, 0x8b, 0xc7, 0x9a, 0x13, 0x1c, + 0xdd, 0x34, 0x2c, 0xba, 0x9b, 0x95, 0x68, 0xb9, 0xdb, 0x6b, 0xc0, 0x1f, 0x57, 0xdf, 0xaf, 0xbe, + 0xb7, 0x95, 0xba, 0x70, 0x0f, 0x49, 0xb8, 0x16, 0x9c, 0x32, 0xfa, 0xb5, 0x98, 0xbe, 0xce, 0xda, + 0x36, 0xb0, 0x5a, 0xac, 0x0f, 0xb6, 0x47, 0x8c, 0xb0, 0x42, 0x79, 0xf6, 0xa8, 0xfc, 0x70, 0x36, + 0x84, 0xdf, 0x66, 0x0f, 0x47, 0x00, 0x2d, 0x08, 0x86, 0x4c, 0x8f, 0x68, 0x33, 0xd3, 0x9e, 0x65, + 0x9c, 0x4e, 0xda, 0x0c, 0x20, 0xd6, 0x68, 0xd7, 0x8e, 0xba, 0xe9, 0x91, 0xc7, 0xcc, 0x4f, 0x57, + 0x0d, 0x3d, 0xb1, 0x8e, 0xca, 0x74, 0xd7, 0xcd, 0x60, 0x8f, 0x37, 0xc0, 0x61, 0xf7, 0x1b, 0x56, + 0x2c, 0x9b, 0x4f, 0x5f, 0xbc, 0x3a, 0x59, 0x7a, 0xda, 0xe2, 0x8a, 0x19, 0x7b, 0x76, 0xbd, 0x2a, + 0x42, 0xdc, 0x0e, 0x47, 0xa5, 0xaa, 0xa0, 0x79, 0xcd, 0x58, 0x56, 0x18, 0x40, 0x0e, 0x80, 0x64, + 0x05, 0x01, 0xa8, 0x46, 0x1c, 0x6b, 0x09, 0x59, 0x05, 0x1f, 0xc0, 0x5d, 0x3e, 0xbc, 0x77, 0x15, + 0xde, 0x15, 0xde, 0xeb, 0x8a, 0xf7, 0xaf, 0x78, 0xc9, 0x0c, 0xb5, 0x12, 0xdd, 0x5e, 0x3f, 0xa0, + 0x30, 0xaf, 0x30, 0x5f, 0x65, 0xcc, 0x8b, 0x24, 0xb2, 0xe8, 0xd8, 0x3e, 0x38, 0x8e, 0xb1, 0xbc, + 0x14, 0x55, 0x2c, 0x54, 0x6b, 0xd3, 0xa3, 0x02, 0x06, 0x0f, 0x61, 0x7a, 0xe4, 0x36, 0x78, 0xcd, + 0xe3, 0xfb, 0xd5, 0x6b, 0xf6, 0xce, 0xa7, 0xe4, 0x46, 0xa1, 0xd0, 0xa1, 0x70, 0x06, 0xc4, 0xda, + 0xc8, 0x59, 0x0c, 0x2c, 0xac, 0x51, 0xaf, 0xd4, 0xcf, 0x69, 0xaf, 0xc2, 0x64, 0x15, 0x26, 0xab, + 0x30, 0x79, 0x47, 0x61, 0x32, 0x5f, 0xd4, 0xa0, 0x1c, 0x28, 0xe5, 0x40, 0xd5, 0x3c, 0x68, 0xe8, + 0x70, 0x47, 0x0d, 0x8a, 0x1a, 0x52, 0xa8, 0xaf, 0x3b, 0xea, 0xbb, 0xdc, 0xa8, 0x57, 0x04, 0x91, + 0x42, 0xfd, 0xc1, 0x06, 0xcb, 0x1d, 0xc4, 0x40, 0xb9, 0x0a, 0x9b, 0x45, 0x62, 0xcb, 0x58, 0x14, + 0x27, 0x29, 0x82, 0xfe, 0x39, 0x78, 0xe1, 0x6f, 0x78, 0x59, 0xc5, 0x50, 0x3a, 0x3f, 0x68, 0xe5, + 0x1e, 0x1e, 0x38, 0xbc, 0x5e, 0xd8, 0xe6, 0xff, 0x2c, 0x30, 0x23, 0xa6, 0x5e, 0x35, 0xda, 0x47, + 0x20, 0x9d, 0xf9, 0xe9, 0x2a, 0x06, 0xd2, 0xab, 0x8e, 0x4a, 0x3b, 0x47, 0x8e, 0xdd, 0x27, 0xca, + 0xc9, 0x8e, 0xcd, 0x84, 0x85, 0xed, 0xaa, 0x18, 0x46, 0xe7, 0x2e, 0x5d, 0xdd, 0xc2, 0xe8, 0xbc, + 0xa5, 0x2d, 0x29, 0x8c, 0x36, 0xe7, 0x3c, 0x27, 0x58, 0x6b, 0xe9, 0x56, 0x31, 0xa1, 0x71, 0x28, + 0x6e, 0x15, 0x0b, 0x3a, 0xc7, 0x1d, 0x4c, 0x50, 0x2f, 0x02, 0x4d, 0xf5, 0x99, 0x71, 0x7d, 0xa4, + 0x42, 0xbb, 0x42, 0x7b, 0xa5, 0xd1, 0x3e, 0x77, 0x5c, 0x02, 0x47, 0x7b, 0xd0, 0x5a, 0xa1, 0x5d, + 0xa1, 0x5d, 0xe8, 0x7a, 0x02, 0xea, 0xb5, 0xe0, 0xdb, 0xf8, 0xe9, 0xa9, 0xeb, 0x09, 0xa0, 0x2f, + 0xa8, 0xfe, 0xf5, 0x04, 0x1c, 0xd7, 0x8c, 0x57, 0x61, 0x5a, 0xea, 0x58, 0xe8, 0x90, 0x76, 0x33, + 0xf9, 0xf1, 0x1e, 0xea, 0xe5, 0xa8, 0xa8, 0xdc, 0x88, 0x58, 0x87, 0xc2, 0xb7, 0x78, 0x6f, 0x02, + 0x21, 0x04, 0xb0, 0x98, 0x45, 0xcd, 0x0e, 0xd8, 0xb6, 0xc9, 0x33, 0x3b, 0xeb, 0x81, 0xed, 0xfa, + 0x26, 0xff, 0x92, 0x09, 0xbe, 0x70, 0x26, 0xdb, 0x54, 0xfa, 0x82, 0xcd, 0x7a, 0xfd, 0x23, 0x78, + 0xcb, 0xe3, 0x5d, 0xf8, 0x96, 0x4a, 0x50, 0x79, 0xf9, 0x84, 0x15, 0x60, 0x24, 0x10, 0xda, 0x6e, + 0x66, 0xd8, 0x23, 0x83, 0x38, 0xee, 0x92, 0xba, 0x17, 0x26, 0xd1, 0x6a, 0xc7, 0xc4, 0x1d, 0xe5, + 0xdb, 0x15, 0x63, 0xee, 0x12, 0x3d, 0x95, 0x45, 0xdd, 0x0d, 0x1d, 0x1b, 0x70, 0x10, 0x35, 0x68, + 0x55, 0x31, 0xda, 0x8e, 0xb1, 0x70, 0x35, 0xe2, 0xed, 0xe8, 0x0b, 0x5b, 0x12, 0x71, 0xf7, 0xe4, + 0xbb, 0x60, 0x1c, 0xfb, 0x01, 0x56, 0xed, 0x6b, 0x16, 0xe4, 0x01, 0x41, 0x72, 0x00, 0x51, 0x1e, + 0x0c, 0x44, 0xc7, 0x4d, 0x6a, 0x04, 0x18, 0xee, 0x72, 0x62, 0xbe, 0xab, 0x30, 0xaf, 0x30, 0x7f, + 0x48, 0xbb, 0x01, 0x24, 0x78, 0xc5, 0xf1, 0x89, 0x6f, 0x53, 0x9c, 0x03, 0xba, 0x2b, 0x79, 0x1b, + 0x7b, 0xcb, 0xe3, 0x3b, 0xff, 0x2d, 0x05, 0x2a, 0x6b, 0x7c, 0xc5, 0xcb, 0x0e, 0xdb, 0x91, 0x09, + 0x5a, 0x55, 0xac, 0xa8, 0x85, 0x72, 0x64, 0x68, 0x68, 0x8d, 0x40, 0x42, 0xf1, 0x31, 0x4b, 0x29, + 0x80, 0x41, 0x69, 0x03, 0x2d, 0x03, 0xd7, 0xb8, 0x37, 0xb4, 0x3f, 0xfb, 0x3f, 0x34, 0x78, 0xc3, + 0x59, 0x10, 0xe0, 0x57, 0x93, 0xcd, 0xc6, 0xfc, 0xba, 0xa1, 0x82, 0xbd, 0x82, 0xbd, 0xa0, 0x09, + 0x12, 0xbc, 0x6c, 0x23, 0x43, 0xdb, 0xaa, 0xcb, 0x36, 0xb6, 0xed, 0x27, 0x17, 0xfb, 0x72, 0x9b, + 0x0b, 0xb5, 0x1c, 0xfe, 0x65, 0x32, 0x23, 0xda, 0xe6, 0x6a, 0x8f, 0x7c, 0x06, 0x26, 0xd9, 0x6e, + 0xc7, 0x1c, 0x8c, 0x3f, 0xf3, 0x15, 0x2f, 0xf2, 0x12, 0x76, 0x51, 0x16, 0xeb, 0x42, 0xdd, 0x4b, + 0x00, 0xd9, 0x43, 0xb0, 0x37, 0xad, 0x9d, 0xb7, 0x54, 0x35, 0x52, 0xd7, 0x39, 0x4b, 0x29, 0xa6, + 0xa7, 0xe1, 0x3a, 0x38, 0xbf, 0x9a, 0x4f, 0x4a, 0xce, 0x5e, 0xd3, 0x7d, 0x0f, 0x76, 0xcd, 0x1e, + 0x3e, 0x4f, 0x82, 0x7a, 0x5f, 0x2e, 0xe4, 0x9e, 0x5c, 0x85, 0x46, 0x85, 0xc6, 0x35, 0x1a, 0xef, + 0x7d, 0x34, 0xbe, 0x19, 0x2e, 0x5c, 0x17, 0xdb, 0xa4, 0xd9, 0x5a, 0x5f, 0x34, 0xd8, 0xdf, 0xb4, + 0x08, 0x6f, 0xab, 0x2d, 0x2d, 0x5d, 0x07, 0xba, 0xf8, 0x94, 0x23, 0x8c, 0x0f, 0x16, 0xa8, 0x79, + 0x16, 0x8c, 0xc4, 0x18, 0xcd, 0x4c, 0x5b, 0xf3, 0x88, 0x41, 0x30, 0x7a, 0x83, 0x1e, 0x1a, 0xe1, + 0xf5, 0x37, 0x0f, 0x0d, 0x48, 0x90, 0x2f, 0x74, 0x6b, 0x62, 0xd4, 0x8b, 0x2f, 0x53, 0x8c, 0x66, + 0x86, 0x6d, 0x4c, 0x02, 0xd7, 0x67, 0x73, 0xa9, 0x1f, 0x1a, 0x1a, 0xbe, 0x1b, 0x82, 0x06, 0x18, + 0x8d, 0x4c, 0x2f, 0xb8, 0x8c, 0xe7, 0x0c, 0xca, 0xc7, 0x08, 0xdc, 0x8e, 0x28, 0xe3, 0x56, 0xc4, 0x42, 0xb7, 0x21, 0x26, 0xe4, 0x88, 0x6b, 0x52, 0x24, 0x25, 0xef, 0x25, 0x67, 0x2e, 0x77, 0x97, - 0xf0, 0xa3, 0xba, 0x78, 0x0c, 0xaf, 0x73, 0x3c, 0x23, 0x5c, 0x37, 0xbd, 0xd9, 0x98, 0x7c, 0x73, - 0xdc, 0xaf, 0x9a, 0xb5, 0x29, 0x41, 0x97, 0xe3, 0x78, 0xa6, 0x5a, 0xee, 0xd8, 0xf5, 0xb4, 0xad, - 0x6a, 0xfb, 0x9d, 0xb6, 0x25, 0xcd, 0xe9, 0x8c, 0x29, 0x2f, 0xb6, 0xb5, 0x8f, 0x37, 0x2e, 0x52, + 0xf0, 0xa3, 0xba, 0x78, 0x0c, 0xaf, 0x73, 0x32, 0x23, 0x5c, 0x37, 0xbd, 0xd9, 0x98, 0x7c, 0x73, + 0xdc, 0xaf, 0x9a, 0xb9, 0x29, 0x41, 0x97, 0xe3, 0x78, 0xa6, 0x5a, 0xee, 0xd8, 0xf5, 0xb4, 0xcd, + 0x6a, 0xfb, 0x9d, 0xb6, 0x29, 0xcd, 0xe9, 0x8c, 0x29, 0x2f, 0xb6, 0xb5, 0x8f, 0x37, 0x2e, 0x52, 0x79, 0x36, 0xd4, 0x90, 0xf5, 0xa8, 0x2e, 0x9b, 0x89, 0x85, 0x1a, 0xb9, 0x12, 0x59, 0x58, 0x29, 0xdb, 0x8f, 0x60, 0xe3, 0x24, 0x21, 0xc6, 0x17, 0x94, 0x36, 0xef, 0xed, 0xc5, 0x8c, 0x3d, 0xbd, 0x5f, 0x9c, 0xbb, 0x90, 0xcc, 0x00, 0x71, 0xf1, 0x9d, 0xb0, 0x7c, 0x43, 0x80, 0x41, 0x80, 0x8d, 0xee, 0x06, 0x06, 0x3f, 0x34, 0x19, 0x8d, 0x62, 0xe9, 0x02, 0xe7, 0x83, 0x4d, 0x60, 0x7d, 0x5c, - 0x7f, 0x8f, 0x7a, 0x86, 0x6e, 0x5b, 0xa0, 0xae, 0x51, 0x47, 0x6e, 0xb2, 0x00, 0x14, 0x29, 0x98, - 0xd8, 0x1b, 0xba, 0xd6, 0x9c, 0x9a, 0x3b, 0x88, 0x39, 0x4e, 0x9b, 0xc6, 0x4a, 0xd2, 0x6b, 0x2f, - 0xe9, 0xec, 0xe5, 0x04, 0xf3, 0xe7, 0x1f, 0xb1, 0x3d, 0x0e, 0x3c, 0x94, 0xc2, 0x4e, 0x3a, 0xcf, - 0x86, 0xdc, 0x68, 0xd7, 0x69, 0x07, 0xe8, 0x1f, 0x8b, 0xee, 0x34, 0xe5, 0xdf, 0x61, 0x0a, 0xd8, - 0x70, 0xcb, 0xb5, 0xd1, 0x36, 0x1a, 0x6a, 0xf7, 0xf2, 0xb2, 0x7a, 0x83, 0x2d, 0xdb, 0x83, 0xce, - 0x50, 0x5c, 0xf9, 0x14, 0x68, 0x0a, 0xe7, 0xac, 0x2b, 0xac, 0xf7, 0xb6, 0xe1, 0x49, 0x29, 0x2d, - 0x96, 0x8b, 0x9b, 0x5e, 0x6e, 0xcd, 0xc5, 0x23, 0x8e, 0x03, 0x8a, 0x89, 0xc7, 0x60, 0xdb, 0x3e, - 0x3a, 0x55, 0xd9, 0xf6, 0x41, 0x85, 0x87, 0x68, 0xcc, 0x5f, 0xa1, 0xcd, 0x1e, 0x34, 0xf8, 0x00, - 0x55, 0x0b, 0xeb, 0x96, 0x62, 0xcb, 0x85, 0x4d, 0x3f, 0x5b, 0x9b, 0x08, 0x6b, 0x17, 0x4e, 0x17, - 0x49, 0x18, 0x6e, 0x22, 0xb0, 0x2b, 0x0e, 0xbf, 0x22, 0xd4, 0x53, 0x21, 0x38, 0x4a, 0xe1, 0x9e, - 0x0a, 0xc3, 0x13, 0xce, 0x32, 0x21, 0xf8, 0x29, 0x1c, 0xf8, 0xce, 0x24, 0x01, 0x12, 0x58, 0x84, - 0x14, 0x2e, 0x96, 0xb2, 0x10, 0x9d, 0x05, 0x0e, 0x8a, 0x58, 0x8c, 0x32, 0xe6, 0xf7, 0x4e, 0x73, - 0xe7, 0xfd, 0xec, 0xac, 0xed, 0x2d, 0x06, 0x1b, 0xba, 0xf2, 0x61, 0xa1, 0xeb, 0xe7, 0xf8, 0x0d, - 0xd2, 0x39, 0xd1, 0x2c, 0xce, 0x2b, 0xe7, 0x8b, 0x74, 0xbc, 0x5f, 0xfe, 0xbc, 0xa0, 0x01, 0x46, - 0xc3, 0x40, 0xa9, 0x2c, 0x5c, 0x6c, 0xa2, 0x6f, 0x13, 0x6c, 0xc7, 0x88, 0x56, 0xcb, 0x43, 0x1e, - 0x26, 0x02, 0xdd, 0x2e, 0xa2, 0x05, 0x64, 0x13, 0xd2, 0xd2, 0x95, 0x44, 0xbe, 0xc2, 0x10, 0x9a, - 0x5d, 0xa1, 0x6f, 0xbf, 0xbc, 0x2a, 0xf7, 0x89, 0x97, 0xd3, 0x5d, 0x49, 0x8b, 0xed, 0x90, 0xa6, - 0x47, 0x0c, 0x97, 0x78, 0xda, 0x37, 0x8b, 0x4c, 0x9a, 0x67, 0xa7, 0x27, 0x53, 0xe7, 0xa4, 0x85, - 0x0c, 0xdb, 0x44, 0x67, 0x67, 0xed, 0xf0, 0x7f, 0x64, 0x39, 0xc7, 0xe8, 0x0d, 0x3a, 0x89, 0xab, - 0xe4, 0xa1, 0x33, 0x9b, 0x39, 0xf6, 0xf5, 0xcc, 0x18, 0x6a, 0x4f, 0xee, 0xe8, 0xa4, 0xb5, 0x7f, - 0xe9, 0xfa, 0xe8, 0x38, 0xf3, 0x81, 0x31, 0xfc, 0xba, 0x59, 0x63, 0x2f, 0x96, 0xab, 0x88, 0x01, - 0xc1, 0xb1, 0xd1, 0x36, 0x33, 0x8e, 0x9c, 0x11, 0x0a, 0x46, 0xb9, 0x1a, 0x8f, 0x12, 0x3a, 0xa0, - 0xd0, 0x49, 0x9d, 0x74, 0x25, 0x8b, 0xdb, 0xb2, 0xe8, 0x2d, 0x3d, 0x82, 0x67, 0x99, 0xf2, 0xf8, - 0x43, 0xb6, 0x40, 0xae, 0x6e, 0xb4, 0xab, 0x82, 0x40, 0xde, 0x05, 0x9d, 0xdf, 0x42, 0x06, 0x72, - 0xec, 0xe9, 0x92, 0x03, 0x1b, 0xf4, 0x1b, 0xfa, 0x94, 0x40, 0x6e, 0x0b, 0xa4, 0xd4, 0x49, 0x3f, - 0x72, 0x81, 0xcc, 0xdd, 0x78, 0xd1, 0x4f, 0xb8, 0x98, 0xf7, 0x96, 0x6d, 0xe2, 0xef, 0xc9, 0x6d, - 0x19, 0xf1, 0xbf, 0xf7, 0x37, 0x02, 0x3b, 0x75, 0x86, 0xc6, 0x54, 0x9b, 0x59, 0x01, 0x8c, 0x4c, - 0xec, 0x91, 0x93, 0x8a, 0x79, 0xa5, 0xbe, 0xe2, 0x09, 0x31, 0x90, 0xea, 0x2a, 0x58, 0xaf, 0x2b, - 0x59, 0x15, 0xf1, 0x58, 0xa5, 0xcc, 0x7c, 0x35, 0x05, 0x16, 0xdc, 0xba, 0x2f, 0x6b, 0x1f, 0x09, - 0x80, 0xac, 0x8b, 0x4f, 0x3e, 0x3f, 0xd5, 0x94, 0x78, 0x5a, 0xb1, 0x4d, 0x8a, 0x6d, 0x3a, 0x56, - 0xb6, 0x29, 0x63, 0x4b, 0x62, 0xcc, 0xf0, 0xc5, 0xc5, 0xa4, 0x1d, 0x98, 0xc9, 0xa3, 0x26, 0xa6, - 0x78, 0x55, 0x46, 0x89, 0x56, 0x9f, 0x4d, 0x95, 0x24, 0xac, 0x93, 0xe2, 0xa2, 0xb8, 0xd4, 0x89, - 0xd8, 0xec, 0x2a, 0xeb, 0x0d, 0x68, 0xc1, 0x4a, 0x34, 0x01, 0x4f, 0xe0, 0x6e, 0xfc, 0x80, 0xfc, - 0x5d, 0x9d, 0xdb, 0xee, 0x55, 0x3b, 0xae, 0xcb, 0xe0, 0xd9, 0x4c, 0x44, 0xdd, 0xff, 0xf9, 0x47, - 0xf8, 0x91, 0xf5, 0x85, 0xc2, 0x8f, 0xd1, 0x5e, 0xd0, 0xcd, 0x4f, 0x9f, 0x31, 0x83, 0x1b, 0x51, - 0x45, 0x37, 0x55, 0xda, 0xb6, 0x1c, 0x69, 0xe2, 0x3f, 0x99, 0xbf, 0x2d, 0x34, 0x5a, 0xa8, 0xe9, - 0x46, 0x1a, 0x00, 0x40, 0x08, 0xb8, 0xc9, 0x29, 0x6a, 0x0b, 0xdc, 0xec, 0xc4, 0x6f, 0xbd, 0x8b, - 0x55, 0x23, 0xec, 0xa8, 0x6a, 0x84, 0xdb, 0x53, 0x02, 0xde, 0x2c, 0x55, 0x85, 0x49, 0xd9, 0x4d, - 0x2d, 0x42, 0x86, 0xe8, 0x09, 0xf8, 0xb6, 0x7c, 0x3e, 0xad, 0x40, 0x98, 0x11, 0xf7, 0x61, 0x7d, - 0xcb, 0xb7, 0xf9, 0x2f, 0xe4, 0xb8, 0x88, 0x9d, 0x4c, 0xca, 0xca, 0x24, 0xd9, 0x96, 0x50, 0x16, - 0xa9, 0x98, 0x3f, 0x5c, 0x85, 0xac, 0x51, 0x11, 0xb7, 0x58, 0xa6, 0x3b, 0x2c, 0xc5, 0x0d, 0xae, - 0x48, 0x56, 0xe8, 0x65, 0x3f, 0x91, 0xfb, 0x2e, 0x44, 0x29, 0x23, 0x17, 0x94, 0x99, 0x08, 0xb2, - 0x2d, 0xa1, 0x24, 0x90, 0x24, 0x79, 0xda, 0x63, 0xd2, 0xe7, 0x70, 0xe5, 0x69, 0x7f, 0x49, 0x9d, - 0x43, 0x91, 0xa7, 0x6d, 0xc2, 0xca, 0x5b, 0x0c, 0xc2, 0xba, 0x1d, 0xda, 0x00, 0x8f, 0x1c, 0x17, - 0x37, 0x23, 0x06, 0xeb, 0x14, 0x9d, 0x9c, 0x9d, 0x64, 0x67, 0x78, 0x36, 0x0f, 0x19, 0x23, 0x82, - 0xdd, 0xf4, 0x33, 0x72, 0xf2, 0x3c, 0x92, 0x24, 0x71, 0xdf, 0x79, 0x9d, 0xc3, 0x15, 0xc7, 0x3d, - 0xe7, 0x6d, 0x5e, 0x54, 0x51, 0x6d, 0x55, 0x54, 0x7b, 0x97, 0xa7, 0xf4, 0x29, 0x9e, 0xc6, 0xc4, - 0x81, 0xe9, 0xb6, 0x82, 0x27, 0xf4, 0x3f, 0x64, 0xf9, 0x91, 0x86, 0x69, 0x62, 0x13, 0x11, 0x07, - 0x19, 0x6b, 0x81, 0x42, 0x29, 0xa3, 0xe7, 0xf7, 0xef, 0x68, 0x8e, 0xec, 0x17, 0x9b, 0xa5, 0x6a, - 0x9e, 0xe1, 0x2f, 0xaf, 0xce, 0x62, 0x3e, 0xbb, 0x2b, 0x54, 0x70, 0x31, 0x97, 0xc7, 0x2d, 0x52, - 0x3d, 0xa6, 0xbe, 0xb5, 0x8c, 0xd4, 0x81, 0x2a, 0x30, 0x85, 0x1a, 0xcd, 0x9f, 0x8b, 0x7d, 0xf7, - 0x72, 0x48, 0xb0, 0xc9, 0x22, 0x4b, 0xd5, 0x49, 0xd0, 0x6a, 0x9d, 0x04, 0xbd, 0x78, 0x7d, 0x38, - 0x27, 0x41, 0xa5, 0xd4, 0xe6, 0xbc, 0xd1, 0xfe, 0x65, 0x68, 0x7f, 0xea, 0xda, 0xd5, 0x0f, 0xff, - 0xf1, 0xff, 0xfe, 0xff, 0xbf, 0xfd, 0xd7, 0xc3, 0x42, 0xd7, 0xbb, 0xbd, 0x66, 0xeb, 0xf9, 0xc7, - 0x37, 0xff, 0xfd, 0xbf, 0x67, 0xa7, 0x27, 0x8f, 0xd7, 0xff, 0xfe, 0xb3, 0xd6, 0xdf, 0xb4, 0x42, - 0xb4, 0x66, 0x7f, 0x2b, 0xa7, 0xc6, 0xe7, 0xdc, 0x75, 0x88, 0x33, 0x74, 0xa6, 0x6c, 0xfd, 0x1a, - 0xb5, 0x54, 0x87, 0x56, 0xeb, 0x7a, 0x68, 0x75, 0x30, 0xe6, 0xb8, 0x4b, 0xd3, 0x6f, 0xac, 0x0e, - 0xa8, 0xaa, 0x03, 0xaa, 0xd9, 0x0d, 0x21, 0xa5, 0x7e, 0x72, 0xd7, 0x0c, 0x56, 0xd2, 0x25, 0xd9, - 0x31, 0x40, 0x29, 0xa0, 0x74, 0xb8, 0x68, 0xb3, 0xcb, 0xac, 0x00, 0x34, 0x3e, 0x87, 0x2b, 0x29, - 0x8c, 0x7d, 0x11, 0x19, 0x28, 0x2e, 0x0b, 0x45, 0x59, 0x2a, 0xb5, 0x7f, 0x51, 0xc4, 0xd5, 0x95, - 0x28, 0x17, 0x08, 0x58, 0xfa, 0x28, 0xcd, 0x44, 0x40, 0x4a, 0x21, 0xa5, 0x87, 0xc6, 0x53, 0x1a, - 0x29, 0xf5, 0x34, 0x5f, 0xa9, 0xa4, 0xd4, 0xe3, 0xf0, 0xd2, 0x49, 0x82, 0x2b, 0x17, 0x1b, 0x26, - 0xb4, 0xb4, 0x52, 0xea, 0x51, 0xae, 0x52, 0x4b, 0x79, 0x0a, 0x8b, 0x55, 0x7a, 0xa9, 0x22, 0xf9, - 0x89, 0x5a, 0x6d, 0x3c, 0x45, 0xcd, 0x33, 0xf4, 0x06, 0x9d, 0xac, 0xd6, 0xe7, 0xa4, 0x85, 0x1c, - 0xf7, 0xc1, 0x46, 0xcd, 0xb3, 0xb3, 0xb6, 0x31, 0xb2, 0x34, 0xcf, 0x18, 0x59, 0xf7, 0xeb, 0x1f, - 0x82, 0xb8, 0xf3, 0xcd, 0x89, 0x35, 0x7f, 0xba, 0xd0, 0x16, 0xb6, 0x35, 0x34, 0x3c, 0x72, 0xd2, - 0xdf, 0xaa, 0xcc, 0x79, 0x12, 0x2e, 0x15, 0xf0, 0x3d, 0x3d, 0x19, 0xef, 0xc1, 0x4f, 0x73, 0xbb, - 0x60, 0x3f, 0x2e, 0xb4, 0xa9, 0x31, 0xc0, 0x53, 0x6c, 0x4a, 0x1b, 0x97, 0xc4, 0xf7, 0x4d, 0xcf, - 0x9f, 0xe6, 0xb6, 0x26, 0x6b, 0xd6, 0xa3, 0xb7, 0x49, 0x99, 0x7b, 0xd7, 0x59, 0x10, 0xac, 0x11, - 0xc3, 0x1d, 0x63, 0xca, 0x7b, 0xf6, 0xbf, 0xa1, 0xf9, 0xef, 0x76, 0xc8, 0x67, 0x4e, 0x02, 0xca, - 0xd3, 0xc5, 0x9e, 0x87, 0x46, 0xc6, 0xcc, 0x9a, 0x5a, 0xd8, 0x8b, 0x36, 0xe1, 0x86, 0xbd, 0x05, - 0x17, 0x71, 0x95, 0x99, 0xd1, 0x2a, 0x23, 0xb3, 0x25, 0x35, 0xc3, 0x95, 0xe9, 0x56, 0x70, 0x4d, - 0xaa, 0xda, 0xbe, 0x0c, 0x66, 0x99, 0xd6, 0x32, 0x26, 0x10, 0x40, 0xac, 0x9f, 0xe4, 0x73, 0xda, - 0x3b, 0xca, 0x69, 0x57, 0x4e, 0x7b, 0xb1, 0xc0, 0xb7, 0x50, 0x00, 0x2c, 0xc9, 0xe1, 0x17, 0x0e, - 0x88, 0x53, 0x5e, 0x2a, 0xd7, 0x93, 0x7d, 0xce, 0x1e, 0xf2, 0x45, 0xca, 0xc2, 0xc2, 0x57, 0x44, - 0x08, 0xe5, 0x09, 0xa3, 0x2c, 0xeb, 0x58, 0x58, 0x38, 0xa5, 0x9b, 0x43, 0x29, 0xc2, 0x2a, 0x68, - 0xb6, 0x78, 0xe3, 0x36, 0xde, 0xc8, 0x5b, 0xa2, 0x40, 0x8a, 0x46, 0xe2, 0xc5, 0x22, 0x72, 0x39, - 0x91, 0xb9, 0xa4, 0x08, 0x5d, 0x42, 0xa4, 0x2e, 0xb8, 0xf2, 0x12, 0x22, 0x77, 0x39, 0x11, 0x7c, - 0xd1, 0x48, 0xbe, 0x7a, 0x0e, 0x21, 0xcf, 0xd9, 0x88, 0x44, 0xd0, 0x54, 0xc0, 0x2a, 0x26, 0x5e, - 0xa3, 0xac, 0x8e, 0xb2, 0x3a, 0x07, 0x6e, 0x75, 0x2c, 0x13, 0xdb, 0xc4, 0x22, 0x4b, 0xbe, 0xb3, - 0xeb, 0x29, 0xab, 0x73, 0x29, 0xf0, 0xec, 0x87, 0xd5, 0xa7, 0xdf, 0x1a, 0x9e, 0x04, 0xfa, 0x61, - 0x30, 0x9e, 0x6b, 0xab, 0x10, 0x59, 0x0b, 0x42, 0xe4, 0xa5, 0x28, 0x8e, 0x82, 0x4d, 0x05, 0x9e, - 0x90, 0x77, 0x2b, 0x4e, 0x16, 0x66, 0x8e, 0x09, 0x3f, 0xcd, 0xed, 0x86, 0xf0, 0xab, 0x5e, 0x4e, - 0xf7, 0xdd, 0xff, 0x2c, 0xda, 0xaf, 0xf6, 0xe3, 0x39, 0x8c, 0x71, 0xf4, 0x0e, 0x6c, 0x5d, 0x7a, - 0x87, 0x30, 0x8e, 0x34, 0x0d, 0x7d, 0x10, 0xa3, 0x39, 0x88, 0xb5, 0x89, 0xd3, 0xf0, 0x05, 0xc6, - 0x21, 0xf4, 0x64, 0xbf, 0x62, 0x6e, 0x47, 0x81, 0xe4, 0x5b, 0xf4, 0x0e, 0xa1, 0x24, 0x5c, 0xf1, - 0x35, 0x4d, 0x14, 0x7b, 0x0c, 0xd2, 0x72, 0x41, 0x6e, 0xab, 0x15, 0x9c, 0x51, 0xda, 0xde, 0x91, - 0x1c, 0x1c, 0x55, 0x42, 0x6f, 0x50, 0xa2, 0xba, 0x4e, 0xf4, 0xbf, 0xf0, 0xe6, 0xbf, 0xf5, 0x89, - 0xda, 0xf5, 0x91, 0xbf, 0x22, 0xc1, 0xab, 0x94, 0x84, 0x4c, 0x6a, 0xa4, 0xef, 0xff, 0xf9, 0xe9, - 0x0f, 0x64, 0x79, 0xc8, 0x76, 0x08, 0xf2, 0x16, 0xf3, 0xb9, 0xe3, 0x12, 0x6c, 0x22, 0xcb, 0x4e, - 0x6d, 0x47, 0xf7, 0x90, 0x43, 0x26, 0xd8, 0x45, 0x64, 0x62, 0xd8, 0x05, 0xeb, 0x3e, 0xca, 0x8a, - 0x0e, 0xf2, 0x22, 0x05, 0x59, 0x99, 0x1b, 0xe9, 0xc1, 0x43, 0x6e, 0x20, 0x51, 0x6c, 0x15, 0x0a, - 0xf5, 0xe5, 0x65, 0xc7, 0xea, 0x4a, 0x50, 0x59, 0xcb, 0x14, 0xea, 0x44, 0xd2, 0xf4, 0x90, 0x85, - 0x3b, 0x3e, 0x50, 0x25, 0xe4, 0xfb, 0x16, 0x72, 0x39, 0xab, 0xa1, 0x84, 0x9d, 0x4f, 0xd8, 0x33, - 0x76, 0x6f, 0xb4, 0xd0, 0x01, 0xcb, 0xfc, 0xc7, 0x73, 0x65, 0xd1, 0x2b, 0x20, 0xec, 0x05, 0x97, - 0x41, 0x49, 0xb9, 0xa0, 0x94, 0xf7, 0x62, 0x52, 0xae, 0x84, 0x5c, 0x09, 0xb9, 0x12, 0xf2, 0xc3, - 0x11, 0xf2, 0xcc, 0x8d, 0xa2, 0x07, 0x2d, 0xe6, 0xfe, 0x58, 0xb5, 0x0f, 0x9f, 0x9e, 0x2e, 0x94, - 0xac, 0xef, 0x5d, 0xd6, 0x25, 0xac, 0x85, 0x12, 0x78, 0x6e, 0x81, 0xef, 0x1d, 0xa7, 0xc0, 0xf7, - 0x94, 0xc0, 0x57, 0x46, 0xe0, 0x7b, 0xc7, 0x22, 0xf0, 0xaf, 0xca, 0x4d, 0x3d, 0xec, 0xeb, 0x34, - 0xd0, 0xaa, 0x72, 0x91, 0xc8, 0xee, 0x1d, 0x58, 0x49, 0xa3, 0xd4, 0x53, 0xe0, 0x12, 0x47, 0xe9, - 0x27, 0x0b, 0x94, 0x3c, 0x4a, 0xbd, 0x0c, 0x5e, 0x02, 0x29, 0xff, 0x51, 0x66, 0x49, 0x24, 0xd1, - 0x55, 0xe1, 0xac, 0x04, 0x1e, 0x3d, 0xc7, 0x53, 0x33, 0x66, 0x5d, 0x1a, 0xa0, 0x3d, 0x18, 0xcf, - 0xdb, 0x9c, 0x7b, 0xf3, 0x11, 0x57, 0x3d, 0x99, 0x4f, 0xab, 0x2f, 0x3d, 0xbe, 0x1d, 0xcf, 0x1f, - 0x6f, 0x46, 0xd6, 0x9d, 0xff, 0xa1, 0x5d, 0x1e, 0x59, 0x58, 0x10, 0xc7, 0x76, 0x66, 0xce, 0xc2, - 0xd3, 0xc2, 0x4a, 0x94, 0x02, 0x67, 0x17, 0x52, 0xaf, 0x50, 0x27, 0x8f, 0xd5, 0x21, 0x06, 0x29, - 0xb2, 0x7e, 0x6b, 0xd8, 0xa6, 0x41, 0x1c, 0x77, 0xc9, 0x71, 0xd6, 0xa5, 0xc0, 0x69, 0x65, 0x4f, - 0xb3, 0x17, 0xb3, 0x01, 0x76, 0x05, 0xce, 0x2a, 0xf3, 0x5c, 0xb7, 0xf2, 0xd9, 0xb0, 0x03, 0xdf, - 0xaf, 0xf4, 0xa3, 0xa2, 0x22, 0xd5, 0xce, 0xa3, 0x87, 0x05, 0xab, 0x9e, 0x47, 0xcf, 0x17, 0x2d, - 0xf4, 0xbd, 0x59, 0x1e, 0xd1, 0x82, 0xdf, 0x05, 0x02, 0x12, 0xa1, 0xaa, 0xe8, 0xa9, 0xa9, 0xbb, - 0xe8, 0x5e, 0x5d, 0x5c, 0xf5, 0x5e, 0x77, 0xaf, 0x2e, 0xeb, 0x3f, 0x87, 0x07, 0x70, 0x3c, 0x2f, - 0xc8, 0xa3, 0xb9, 0x9a, 0x65, 0xf2, 0xdb, 0xb8, 0xcd, 0xa3, 0xca, 0xb6, 0x29, 0xdb, 0x56, 0x43, - 0xdb, 0x66, 0x45, 0xfb, 0x98, 0x45, 0x0a, 0x71, 0x5c, 0x71, 0x3c, 0xb3, 0xea, 0xe3, 0xce, 0xea, - 0x20, 0x04, 0xe4, 0x2e, 0xff, 0xd8, 0x52, 0x63, 0xfc, 0x49, 0xe0, 0x59, 0x68, 0xe5, 0xb4, 0xdc, - 0x17, 0x34, 0x9b, 0xf7, 0xba, 0x76, 0xd5, 0x7f, 0xbe, 0xef, 0x68, 0x57, 0xfd, 0xf0, 0xc7, 0x4e, - 0xf0, 0x7f, 0xe1, 0xcf, 0xdd, 0x7b, 0x5d, 0xbb, 0x58, 0xff, 0x7c, 0x79, 0xaf, 0x6b, 0x97, 0xfd, - 0xd6, 0xc3, 0xc3, 0x59, 0xeb, 0xaf, 0xf3, 0x17, 0xfe, 0x07, 0x1b, 0x65, 0x87, 0xe9, 0xa7, 0x3b, - 0x5c, 0xf2, 0x5e, 0x8d, 0x97, 0xfc, 0xfa, 0xd9, 0x5f, 0x18, 0x43, 0x1b, 0xdd, 0x68, 0xbf, 0xf6, - 0xff, 0xd2, 0x4f, 0x2f, 0x5e, 0x5a, 0xd7, 0xad, 0xe6, 0xf6, 0xef, 0xae, 0x5b, 0x7f, 0xe9, 0xa7, - 0x97, 0x2f, 0xcd, 0x66, 0xc6, 0x5f, 0x7e, 0xce, 0x7a, 0x47, 0xeb, 0xb9, 0xd9, 0x6c, 0xae, 0x16, - 0x3b, 0x01, 0x80, 0x7b, 0xbd, 0xd3, 0xff, 0x39, 0xf8, 0x31, 0xfc, 0x37, 0x82, 0x10, 0xa8, 0x71, - 0xab, 0x7c, 0xe0, 0xd4, 0xec, 0x2e, 0xb2, 0x2a, 0x5e, 0xe1, 0x62, 0x3b, 0xa4, 0x09, 0xe6, 0xb6, - 0x13, 0xac, 0xf6, 0x43, 0x63, 0x75, 0x25, 0xc7, 0x43, 0x63, 0x0f, 0x17, 0x4b, 0xbc, 0xfd, 0xed, - 0x53, 0x54, 0x45, 0x3d, 0x20, 0x75, 0xd6, 0x9c, 0xe9, 0xdc, 0xf1, 0x3c, 0x6b, 0x30, 0xc5, 0x90, - 0xcb, 0x44, 0x90, 0xaa, 0x61, 0xbf, 0x72, 0x6d, 0x24, 0x4d, 0xe7, 0xe1, 0x5e, 0x26, 0xc1, 0xb8, - 0x67, 0x1f, 0x5a, 0xf8, 0xbc, 0x04, 0x29, 0x48, 0x26, 0x0a, 0x82, 0xfb, 0x07, 0x72, 0x17, 0x8a, - 0xa3, 0xfe, 0xf9, 0x71, 0x60, 0x9e, 0x7b, 0xf2, 0xaa, 0x7e, 0x35, 0x03, 0x20, 0x9c, 0x9d, 0xbb, - 0xd8, 0xc3, 0xf6, 0x10, 0x97, 0x69, 0x56, 0xde, 0xad, 0x2f, 0xb8, 0x40, 0x6f, 0x7f, 0xfb, 0xb4, - 0xfb, 0x48, 0x31, 0x1c, 0xdf, 0x3e, 0x63, 0xc5, 0xe4, 0x04, 0xd4, 0xec, 0xae, 0xb9, 0x32, 0x2f, - 0x4d, 0x8d, 0xa7, 0x48, 0xa4, 0xde, 0x94, 0x1a, 0xcf, 0x88, 0x34, 0x76, 0x72, 0x43, 0x49, 0x19, - 0xb7, 0x0e, 0x30, 0x8a, 0x4b, 0x8b, 0x4d, 0x49, 0x91, 0x3b, 0x07, 0x08, 0x8d, 0x3a, 0x88, 0xe4, - 0x3d, 0x68, 0x95, 0x57, 0x8d, 0x19, 0x50, 0xa0, 0xa7, 0x41, 0x4d, 0x60, 0xf7, 0xd5, 0x6d, 0x06, - 0xb5, 0xbf, 0xcd, 0x00, 0x76, 0xb4, 0x1f, 0x72, 0x84, 0x9f, 0xef, 0xa8, 0xfe, 0x26, 0xd2, 0xb1, - 0x34, 0x0a, 0x4a, 0x93, 0x1c, 0x3c, 0xec, 0xbc, 0x3d, 0x27, 0x17, 0x0c, 0xde, 0xa3, 0x01, 0x70, - 0x64, 0x39, 0x3f, 0x0d, 0x73, 0x5d, 0xe4, 0x7f, 0xd7, 0x9a, 0xc3, 0xee, 0x6c, 0x94, 0xff, 0x65, - 0xf0, 0x75, 0x91, 0x74, 0x5b, 0xda, 0xe7, 0x95, 0x93, 0x57, 0xf4, 0xdf, 0x6c, 0x0d, 0x94, 0x72, - 0xc9, 0x15, 0x7d, 0x27, 0x08, 0x7b, 0xc7, 0x87, 0xd0, 0xce, 0x0e, 0xc0, 0x0e, 0x0e, 0xc0, 0x4e, - 0x8d, 0xed, 0x41, 0x32, 0xec, 0x24, 0x8f, 0x7d, 0xcc, 0x90, 0x5e, 0xb0, 0x39, 0x4c, 0x62, 0x61, - 0xb3, 0x2e, 0xb1, 0xee, 0x36, 0xe6, 0x21, 0x4b, 0x47, 0xb0, 0x97, 0x2e, 0x9e, 0xb4, 0xb9, 0x00, - 0x22, 0xd6, 0x68, 0x7b, 0x3d, 0x33, 0x2d, 0x52, 0xae, 0x25, 0xa2, 0x59, 0xa0, 0x2c, 0xcb, 0x93, - 0x35, 0x7c, 0x86, 0xb9, 0x01, 0x9b, 0x19, 0xb0, 0x79, 0xc9, 0x33, 0x2b, 0x0d, 0x4e, 0xec, 0xe7, - 0xda, 0x8e, 0xcd, 0xc8, 0xc3, 0x4a, 0x5e, 0x19, 0x83, 0xa6, 0x30, 0xb1, 0xac, 0x8b, 0x6e, 0xe8, - 0x37, 0xc5, 0xb1, 0xed, 0xc9, 0x3a, 0x49, 0xcb, 0xc8, 0xdc, 0x73, 0xe7, 0x60, 0xe1, 0xb9, 0xd6, - 0x17, 0xfa, 0x15, 0x76, 0xf0, 0x21, 0x74, 0xf4, 0xfd, 0x8d, 0x01, 0xa8, 0x3e, 0x33, 0x5c, 0x3f, - 0x26, 0x95, 0xde, 0x98, 0x18, 0xd3, 0xa9, 0x83, 0x62, 0x44, 0x77, 0xc6, 0x9d, 0x31, 0x7d, 0x80, - 0x36, 0x70, 0x0d, 0x7b, 0x8c, 0x7d, 0x31, 0xff, 0x88, 0x8d, 0xd1, 0xd4, 0xa2, 0xe9, 0x84, 0x74, - 0x53, 0xa5, 0x19, 0x4a, 0xd4, 0x0c, 0x0b, 0xcb, 0x26, 0xe7, 0x5d, 0x8a, 0x66, 0xc8, 0x90, 0x4e, - 0xc6, 0xfe, 0x19, 0x59, 0x7a, 0x61, 0x9f, 0x42, 0x25, 0x4b, 0x31, 0x9c, 0xeb, 0x95, 0xd3, 0x0c, - 0xa7, 0xa5, 0x2e, 0xda, 0xa5, 0xae, 0x1f, 0xc0, 0xb2, 0x5d, 0xea, 0xdd, 0x23, 0x5b, 0xb7, 0xab, - 0xab, 0xab, 0xab, 0xfa, 0xaf, 0xdb, 0x7e, 0x47, 0x01, 0xb5, 0xc4, 0x54, 0x95, 0x5d, 0xe3, 0x68, - 0x85, 0x6e, 0xfa, 0x3d, 0x6b, 0x6c, 0x63, 0x13, 0x60, 0xf8, 0x57, 0x0d, 0x95, 0xd9, 0x2f, 0xd1, - 0xec, 0xb3, 0xac, 0xfe, 0xf9, 0xde, 0xac, 0xfe, 0x79, 0xb9, 0x06, 0xc4, 0x9f, 0xfc, 0xf2, 0xf5, - 0x50, 0x6f, 0x7f, 0x43, 0xd8, 0x8f, 0xd1, 0xaf, 0xfd, 0x8a, 0x5d, 0x1e, 0xd7, 0x8a, 0x9d, 0x77, - 0x6b, 0xbf, 0x62, 0x9d, 0xa3, 0x5a, 0x30, 0x15, 0x0c, 0xed, 0xc5, 0x39, 0xa3, 0xbb, 0x34, 0x0b, - 0x1b, 0xec, 0xd4, 0x44, 0x4d, 0x95, 0x5b, 0xa3, 0xd8, 0x0c, 0x25, 0xc0, 0x8a, 0xcd, 0x50, 0x6c, - 0x86, 0x62, 0x33, 0x0e, 0x90, 0xcd, 0xc8, 0x34, 0x98, 0x0b, 0xdb, 0x72, 0xec, 0x29, 0x36, 0x46, - 0x2e, 0x1e, 0xd1, 0x33, 0x82, 0xa9, 0x96, 0xca, 0x5c, 0x96, 0x69, 0x2e, 0xfd, 0xd9, 0xa6, 0x65, - 0x05, 0x33, 0xe0, 0x47, 0x3f, 0x5e, 0x04, 0xd8, 0xbf, 0xb2, 0x5a, 0x5d, 0xc8, 0xde, 0x15, 0x4a, - 0x66, 0xb0, 0xf1, 0x69, 0x9d, 0xe5, 0xb6, 0x6c, 0x82, 0xdd, 0x91, 0x31, 0xc4, 0xc1, 0xae, 0xfa, - 0x86, 0x24, 0x75, 0xc3, 0xcc, 0x9c, 0xa6, 0x33, 0xa8, 0xf2, 0x05, 0xc6, 0x1e, 0x93, 0x09, 0x48, - 0x5e, 0xa2, 0x86, 0x4a, 0x5c, 0x0e, 0x4d, 0x5c, 0x38, 0x00, 0x48, 0x69, 0xc3, 0x48, 0xe5, 0xb3, - 0xfb, 0xc3, 0x65, 0x87, 0x53, 0x96, 0x0c, 0x78, 0x38, 0x5f, 0xf8, 0xb8, 0x35, 0xff, 0xf1, 0x6a, - 0xc8, 0x71, 0x65, 0x9e, 0x23, 0xe8, 0x60, 0x3f, 0x7f, 0x1f, 0x63, 0x15, 0xdc, 0xc4, 0xdb, 0x97, - 0xac, 0x4b, 0x73, 0xa3, 0x33, 0x40, 0x94, 0x06, 0x8c, 0xd6, 0xca, 0x06, 0xb2, 0x7e, 0x3c, 0x40, - 0xe6, 0xad, 0x9d, 0x50, 0x5f, 0x40, 0x8b, 0x9b, 0x68, 0xd0, 0x2e, 0xb7, 0x54, 0x4b, 0x65, 0xa4, - 0x95, 0x91, 0xce, 0x71, 0x69, 0x41, 0x27, 0x9b, 0x41, 0xdb, 0xb2, 0x94, 0x12, 0x57, 0x4a, 0x5c, - 0x29, 0x71, 0x88, 0x12, 0x8f, 0x38, 0x7a, 0x86, 0x0a, 0xdf, 0xb4, 0x53, 0x0a, 0xfc, 0xd0, 0x14, - 0xf8, 0x01, 0x68, 0xb5, 0xce, 0x11, 0xa9, 0x35, 0x66, 0x22, 0xe2, 0x88, 0xa3, 0xac, 0x1d, 0x32, - 0x56, 0xaf, 0x62, 0x3d, 0xcd, 0x3b, 0x9c, 0xd2, 0xb0, 0xbc, 0x5f, 0x8d, 0xaf, 0xf8, 0xb3, 0xe3, - 0xa4, 0x15, 0xcc, 0xf6, 0x81, 0x95, 0x46, 0xfc, 0x4f, 0x89, 0xc3, 0x28, 0xbf, 0xe0, 0x27, 0x6b, - 0x7d, 0xf0, 0xe4, 0xe5, 0xd5, 0xcb, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0xff, - 0xff, 0x8b, 0x36, 0x32, 0x25, 0xad, 0x09, 0x02, 0x00, + 0x7f, 0x8f, 0x7a, 0x86, 0x6e, 0x5b, 0xa0, 0xae, 0x51, 0x47, 0x6e, 0xb2, 0x00, 0x14, 0x29, 0x8c, + 0xb0, 0x37, 0x74, 0xcd, 0x39, 0x35, 0x77, 0x10, 0x73, 0x9c, 0x36, 0x8d, 0x95, 0xa4, 0xd7, 0x5e, + 0xd2, 0xd9, 0xcb, 0x09, 0xe6, 0xcf, 0x3f, 0x62, 0x7b, 0x12, 0x78, 0x28, 0x85, 0x9d, 0x74, 0x9e, + 0x0d, 0xb9, 0xd1, 0xae, 0xd3, 0x0e, 0xd0, 0x3f, 0x16, 0xdd, 0x69, 0xca, 0xbf, 0xc3, 0x14, 0xb0, + 0xe1, 0x96, 0x6b, 0xa3, 0x6d, 0x34, 0xd4, 0xee, 0xe5, 0x65, 0xf5, 0x06, 0x5b, 0xb6, 0x07, 0x9d, + 0xa1, 0xb8, 0xf2, 0x29, 0xd0, 0x14, 0xce, 0x59, 0x57, 0x58, 0xef, 0x6d, 0xc3, 0x93, 0x52, 0x5a, + 0x2c, 0x17, 0x37, 0xbd, 0xdc, 0x9a, 0x8b, 0xc7, 0x1c, 0x07, 0x14, 0x13, 0x8f, 0xc1, 0xb6, 0x7d, + 0x74, 0xaa, 0xb2, 0xed, 0x83, 0x0a, 0x0f, 0xd1, 0x98, 0xbf, 0x42, 0x9b, 0x3d, 0x68, 0xf0, 0x01, + 0xaa, 0x16, 0xd6, 0x2d, 0xc5, 0xa6, 0x0b, 0x9b, 0x7e, 0xb6, 0x36, 0x11, 0xd6, 0x2e, 0x9c, 0x2e, + 0x92, 0x30, 0xdc, 0x44, 0x60, 0x57, 0x1c, 0x7e, 0x45, 0xa8, 0xa7, 0x42, 0x70, 0x94, 0xc2, 0x3d, + 0x15, 0x86, 0x27, 0x9c, 0x65, 0x42, 0xf0, 0x53, 0x38, 0xf0, 0x9d, 0x49, 0x02, 0x24, 0xb0, 0x08, + 0x29, 0x5c, 0x2c, 0x65, 0x21, 0x3a, 0x0b, 0x1c, 0x14, 0xb1, 0x18, 0x65, 0xcc, 0xef, 0x9d, 0xe6, + 0xce, 0xfb, 0xd9, 0x59, 0xdb, 0x5b, 0x0c, 0x36, 0x74, 0xe5, 0xc3, 0x42, 0xd7, 0xcf, 0xf1, 0x1b, + 0xa4, 0x73, 0xa2, 0x59, 0x9c, 0x57, 0xce, 0x17, 0xe9, 0x78, 0xbf, 0xfc, 0x79, 0x41, 0x03, 0x8c, + 0x86, 0x81, 0x52, 0x59, 0xb8, 0x78, 0x84, 0xbe, 0x4d, 0xb1, 0x1d, 0x23, 0x5a, 0x4d, 0x0f, 0x79, + 0x98, 0x08, 0x74, 0xbb, 0x88, 0x16, 0x90, 0x4d, 0x48, 0x4b, 0x57, 0x12, 0xf9, 0x0a, 0x43, 0x68, + 0x76, 0x85, 0xbe, 0xfd, 0xf2, 0xaa, 0xdc, 0x27, 0x5e, 0x4e, 0x77, 0x25, 0x2d, 0xb6, 0x43, 0x9a, + 0x1e, 0x31, 0x5c, 0xe2, 0x69, 0xdf, 0x4c, 0x32, 0x6d, 0x9e, 0x9d, 0x9e, 0x58, 0xce, 0x49, 0x0b, + 0x19, 0xf6, 0x08, 0x9d, 0x9d, 0xb5, 0xc3, 0xff, 0x91, 0xe5, 0x1c, 0xa3, 0x37, 0xe8, 0x24, 0xae, + 0x92, 0x87, 0xce, 0x6c, 0xe6, 0xd8, 0xd7, 0x33, 0x63, 0xa8, 0x3d, 0xb9, 0xe3, 0x93, 0xd6, 0xfe, + 0xa5, 0xeb, 0xa3, 0xe3, 0xcc, 0x07, 0xc6, 0xf0, 0xeb, 0x66, 0x8d, 0xbd, 0x58, 0xae, 0x22, 0x06, + 0x04, 0xc7, 0x46, 0xdb, 0xcc, 0x38, 0x72, 0xc6, 0x28, 0x18, 0xe5, 0x6a, 0x3c, 0x4a, 0xe8, 0x80, + 0x42, 0x27, 0x75, 0xd2, 0x95, 0x2c, 0x6e, 0xcb, 0xa2, 0xb7, 0xf4, 0x08, 0x9e, 0x65, 0xca, 0xe3, + 0xdf, 0xb2, 0x05, 0x72, 0x75, 0xa3, 0x5d, 0x15, 0x04, 0xf2, 0x2e, 0xe8, 0xfc, 0x16, 0x32, 0x90, + 0x63, 0x5b, 0x4b, 0x0e, 0x6c, 0xd0, 0x6f, 0xe8, 0x53, 0x02, 0xb9, 0x2d, 0x90, 0x52, 0x27, 0xfd, + 0xc8, 0x05, 0x32, 0x77, 0xe3, 0x45, 0x3f, 0xe1, 0x62, 0xde, 0x9b, 0xf6, 0x08, 0x7f, 0x4f, 0x6e, + 0xcb, 0x88, 0xff, 0xbd, 0xbf, 0x11, 0x58, 0xcb, 0x19, 0x1a, 0x96, 0x36, 0x33, 0x03, 0x18, 0x8d, + 0xb0, 0x47, 0x4e, 0x2a, 0xe6, 0x95, 0xfa, 0x8a, 0x27, 0xc4, 0x40, 0xaa, 0xab, 0x60, 0xbd, 0xae, + 0x64, 0x55, 0xc4, 0x63, 0x95, 0x32, 0xf3, 0xd5, 0x14, 0x58, 0x70, 0xeb, 0xbe, 0xac, 0x7d, 0x24, + 0x00, 0xb2, 0x2e, 0x3e, 0xf9, 0xfc, 0x54, 0x53, 0xe2, 0x69, 0xc5, 0x36, 0x29, 0xb6, 0xe9, 0x58, + 0xd9, 0xa6, 0x8c, 0x2d, 0x89, 0x31, 0xc3, 0x17, 0x17, 0x93, 0x76, 0x60, 0x26, 0x8f, 0x9a, 0x98, + 0xe2, 0x55, 0x19, 0x25, 0x5a, 0x7d, 0x36, 0x55, 0x92, 0xb0, 0x4e, 0x8a, 0x8b, 0xe2, 0x52, 0x27, + 0x62, 0xb3, 0xab, 0xac, 0x37, 0xa0, 0x05, 0x2b, 0xd1, 0x04, 0x3c, 0x81, 0xbb, 0xf1, 0x03, 0xf2, + 0x77, 0x75, 0x6e, 0xbb, 0x57, 0xed, 0xb8, 0x2e, 0x83, 0x67, 0x33, 0x11, 0x75, 0xff, 0xe7, 0xef, + 0xe1, 0x47, 0xd6, 0x17, 0x0a, 0x3f, 0x46, 0x7b, 0x41, 0x37, 0x3f, 0x7d, 0xc6, 0x0c, 0x6e, 0x44, + 0x15, 0xdd, 0x54, 0x69, 0xdb, 0x72, 0xa4, 0x89, 0xff, 0x64, 0xfe, 0xb6, 0xd0, 0x68, 0xa1, 0xa6, + 0x1b, 0x6b, 0x00, 0x00, 0x21, 0xe0, 0x26, 0xa7, 0xa8, 0x2d, 0x70, 0xb3, 0x13, 0xbf, 0xf5, 0x2e, + 0x56, 0x8d, 0xb0, 0xa3, 0xaa, 0x11, 0x6e, 0x4f, 0x09, 0x78, 0xb3, 0x54, 0x15, 0x26, 0x65, 0x37, + 0xb5, 0x08, 0x19, 0xa2, 0x27, 0xe0, 0xdb, 0xf2, 0xf9, 0xb4, 0x02, 0x61, 0x46, 0xdc, 0x87, 0xf5, + 0x2d, 0xdf, 0xe6, 0xbf, 0x90, 0xe3, 0x22, 0x76, 0x32, 0x29, 0x2b, 0x93, 0x64, 0x9b, 0x42, 0x59, + 0xa4, 0x62, 0xfe, 0x70, 0x15, 0xb2, 0x46, 0x45, 0xdc, 0x62, 0x99, 0xee, 0xb0, 0x14, 0x37, 0xb8, + 0x22, 0x59, 0xa1, 0x97, 0xfd, 0x44, 0xee, 0xbb, 0x10, 0xa5, 0x8c, 0x5c, 0x50, 0x66, 0x22, 0xc8, + 0x36, 0x85, 0x92, 0x40, 0x92, 0xe4, 0x69, 0x8f, 0x49, 0x9f, 0xc3, 0x95, 0xa7, 0xfd, 0x25, 0x75, + 0x0e, 0x45, 0x9e, 0xb6, 0x09, 0x2b, 0x6f, 0x31, 0x08, 0xeb, 0x76, 0x68, 0x03, 0x3c, 0x76, 0x5c, + 0xdc, 0x8c, 0x18, 0xac, 0x53, 0x74, 0x72, 0x76, 0x92, 0x9d, 0xe1, 0xd9, 0x3c, 0x64, 0x8c, 0x09, + 0x76, 0xd3, 0xcf, 0xc8, 0xc9, 0xf3, 0x48, 0x92, 0xc4, 0x7d, 0xe7, 0x75, 0x0e, 0x57, 0x1c, 0xf7, + 0x9c, 0xb7, 0x79, 0x51, 0x45, 0xb5, 0x55, 0x51, 0xed, 0x5d, 0x9e, 0xd2, 0xa7, 0x78, 0x1a, 0x53, + 0x07, 0xa6, 0xdb, 0x0a, 0x9e, 0xd0, 0xff, 0x90, 0xe5, 0x47, 0x1a, 0xa3, 0x11, 0x1e, 0x21, 0xe2, + 0x20, 0x63, 0x2d, 0x50, 0x28, 0x65, 0xf4, 0xfc, 0xfe, 0x1d, 0xcd, 0x91, 0xfd, 0x62, 0xb3, 0x54, + 0xcd, 0x33, 0xfc, 0xe5, 0xd5, 0x59, 0xcc, 0x67, 0x77, 0x85, 0x0a, 0x2e, 0xe6, 0xf2, 0xb8, 0x45, + 0xaa, 0xc7, 0xd4, 0xb7, 0x96, 0x91, 0x3a, 0x50, 0x05, 0xa6, 0x50, 0xa3, 0xf9, 0x73, 0xb1, 0xef, + 0x5e, 0x0e, 0x09, 0x1e, 0xb1, 0xc8, 0x52, 0x75, 0x12, 0xb4, 0x5a, 0x27, 0x41, 0x2f, 0x5e, 0x1f, + 0xce, 0x49, 0x50, 0x29, 0xb5, 0x39, 0x6f, 0xb4, 0x7f, 0x19, 0xda, 0x9f, 0xba, 0x76, 0xf5, 0xb7, + 0xff, 0xf8, 0x7f, 0xff, 0xff, 0xdf, 0xfe, 0xeb, 0x61, 0xa1, 0xeb, 0xdd, 0x5e, 0xb3, 0xf5, 0xfc, + 0xc3, 0x9b, 0xff, 0xfe, 0xdf, 0xb3, 0xd3, 0x93, 0xc7, 0xeb, 0x7f, 0xff, 0x49, 0xeb, 0x6f, 0x5a, + 0x21, 0x5a, 0xb3, 0xbf, 0x97, 0x53, 0xe3, 0x73, 0xee, 0x3a, 0xc4, 0x19, 0x3a, 0x16, 0x5b, 0xbf, + 0x46, 0x2d, 0xd5, 0xa1, 0xd5, 0xba, 0x1e, 0x5a, 0x1d, 0x4c, 0x38, 0xee, 0xd2, 0xf4, 0x1b, 0xab, + 0x03, 0xaa, 0xea, 0x80, 0x6a, 0x76, 0x43, 0x48, 0xa9, 0x9f, 0xdc, 0x35, 0x83, 0x95, 0x74, 0x49, + 0x76, 0x0c, 0x50, 0x0a, 0x28, 0x1d, 0x2e, 0xda, 0xec, 0x32, 0x2b, 0x00, 0x8d, 0xcf, 0xe1, 0x4a, + 0x0a, 0x63, 0x5f, 0x44, 0x06, 0x8a, 0xcb, 0x42, 0x51, 0x96, 0x4a, 0xed, 0x5f, 0x14, 0x71, 0x75, + 0x25, 0xca, 0x05, 0x02, 0x96, 0x3e, 0x4a, 0x33, 0x11, 0x90, 0x52, 0x48, 0xe9, 0xa1, 0xf1, 0x94, + 0x46, 0x4a, 0x3d, 0xcd, 0x57, 0x2a, 0x29, 0xf5, 0x38, 0xbc, 0x74, 0x92, 0xe0, 0xca, 0xc5, 0x86, + 0x09, 0x2d, 0xad, 0x94, 0x7a, 0x94, 0xab, 0xd4, 0x52, 0x9e, 0xc2, 0x62, 0x95, 0x5e, 0xaa, 0x48, + 0x7e, 0xa2, 0x56, 0x1b, 0x4f, 0x51, 0xf3, 0x0c, 0xbd, 0x41, 0x27, 0xab, 0xf5, 0x39, 0x69, 0x21, + 0xc7, 0x7d, 0xb0, 0x51, 0xf3, 0xec, 0xac, 0x6d, 0x8c, 0x4d, 0xcd, 0x33, 0xc6, 0xe6, 0xfd, 0xfa, + 0x87, 0x20, 0xee, 0x7c, 0x73, 0x62, 0xce, 0x9f, 0x2e, 0xb4, 0x85, 0x6d, 0x0e, 0x0d, 0x8f, 0x9c, + 0xf4, 0xb7, 0x2a, 0x73, 0x9e, 0x84, 0x4b, 0x05, 0x7c, 0x4f, 0x4f, 0xc6, 0x7b, 0xf0, 0xd3, 0xdc, + 0x2e, 0xd8, 0x8f, 0x0b, 0xcd, 0x32, 0x06, 0xd8, 0xc2, 0x23, 0x69, 0xe3, 0x92, 0xf8, 0x3e, 0xeb, + 0xfc, 0x69, 0x6e, 0x6b, 0xb2, 0x66, 0x3d, 0x7a, 0x9b, 0x94, 0xb9, 0x77, 0x9d, 0x05, 0xc1, 0x1a, + 0x31, 0xdc, 0x09, 0xa6, 0xbc, 0x67, 0xff, 0x1b, 0x9a, 0xff, 0xb0, 0x43, 0x3e, 0x73, 0x1a, 0x50, + 0x9e, 0x2e, 0xf6, 0x3c, 0x34, 0x36, 0x66, 0xa6, 0x65, 0x62, 0x2f, 0xda, 0x84, 0x1b, 0xf6, 0x16, + 0x5c, 0xc4, 0x55, 0x66, 0x46, 0xab, 0x8c, 0xcc, 0x96, 0xd4, 0x0c, 0x57, 0xa6, 0x5b, 0xc1, 0x35, + 0xa9, 0x6a, 0xfb, 0x32, 0x98, 0x65, 0x5a, 0xcb, 0x98, 0x40, 0x00, 0xb1, 0x7e, 0x92, 0xcf, 0x69, + 0xef, 0x28, 0xa7, 0x5d, 0x39, 0xed, 0xc5, 0x02, 0xdf, 0x42, 0x01, 0xb0, 0x24, 0x87, 0x5f, 0x38, + 0x20, 0x4e, 0x79, 0xa9, 0x5c, 0x4f, 0xf6, 0x39, 0x7b, 0xc8, 0x17, 0x29, 0x0b, 0x0b, 0x5f, 0x11, + 0x21, 0x94, 0x27, 0x8c, 0xb2, 0xac, 0x63, 0x61, 0xe1, 0x94, 0x6e, 0x0e, 0xa5, 0x08, 0xab, 0xa0, + 0xd9, 0xe2, 0x8d, 0xdb, 0x78, 0x23, 0x6f, 0x89, 0x02, 0x29, 0x1a, 0x89, 0x17, 0x8b, 0xc8, 0xe5, + 0x44, 0xe6, 0x92, 0x22, 0x74, 0x09, 0x91, 0xba, 0xe0, 0xca, 0x4b, 0x88, 0xdc, 0xe5, 0x44, 0xf0, + 0x45, 0x23, 0xf9, 0xea, 0x39, 0x84, 0x3c, 0x67, 0x23, 0x12, 0x41, 0x53, 0x01, 0xab, 0x98, 0x78, + 0x8d, 0xb2, 0x3a, 0xca, 0xea, 0x1c, 0xb8, 0xd5, 0x31, 0x47, 0xd8, 0x26, 0x26, 0x59, 0xf2, 0x9d, + 0x5d, 0x4f, 0x59, 0x9d, 0x4b, 0x81, 0x67, 0x3f, 0xac, 0x3e, 0xfd, 0xd6, 0xf0, 0x24, 0xd0, 0x0f, + 0x83, 0xc9, 0x5c, 0x5b, 0x85, 0xc8, 0x5a, 0x10, 0x22, 0x2f, 0x45, 0x71, 0x14, 0x6c, 0x2a, 0xf0, + 0x84, 0xbc, 0x5b, 0x71, 0xb2, 0x30, 0x73, 0x4c, 0xf8, 0x69, 0x6e, 0x37, 0x84, 0x5f, 0xf5, 0x72, + 0xba, 0xef, 0xfe, 0x67, 0xd1, 0x7e, 0xb5, 0x1f, 0xcf, 0x61, 0x8c, 0xa3, 0x77, 0x60, 0xeb, 0xd2, + 0x3b, 0x84, 0x71, 0xa4, 0x69, 0xe8, 0x83, 0x18, 0xcd, 0x41, 0xac, 0x4d, 0x9c, 0x86, 0x2f, 0x30, + 0x0e, 0xa1, 0x27, 0xfb, 0x15, 0x73, 0x3b, 0x0a, 0x24, 0xdf, 0xa2, 0x77, 0x08, 0x25, 0xe1, 0x8a, + 0xaf, 0x69, 0xa2, 0xd8, 0x63, 0x90, 0x96, 0x0b, 0x72, 0x5b, 0xad, 0xe0, 0x8c, 0xd2, 0xf6, 0x8e, + 0xe4, 0xe0, 0xa8, 0x12, 0x7a, 0x83, 0x12, 0xd5, 0x75, 0xa2, 0xff, 0x85, 0x37, 0xff, 0xad, 0x4f, + 0xd4, 0xae, 0x8f, 0xfc, 0x15, 0x09, 0x5e, 0xa5, 0x24, 0x64, 0x52, 0x23, 0x7d, 0xff, 0xcf, 0x4f, + 0xbf, 0x23, 0xd3, 0x43, 0xb6, 0x43, 0x90, 0xb7, 0x98, 0xcf, 0x1d, 0x97, 0xe0, 0x11, 0x32, 0xed, + 0xd4, 0x76, 0x74, 0x0f, 0x39, 0x64, 0x8a, 0x5d, 0x44, 0xa6, 0x86, 0x5d, 0xb0, 0xee, 0xa3, 0xac, + 0xe8, 0x20, 0x2f, 0x52, 0x90, 0x95, 0xb9, 0x91, 0x1e, 0x3c, 0xe4, 0x06, 0x12, 0xc5, 0x56, 0xa1, + 0x50, 0x5f, 0x5e, 0x76, 0xac, 0xae, 0x04, 0x95, 0xb5, 0x4c, 0xa1, 0x4e, 0x24, 0x4d, 0x0f, 0x59, + 0xb8, 0xe3, 0x03, 0x55, 0x42, 0xbe, 0x6f, 0x21, 0x97, 0xb3, 0x1a, 0x4a, 0xd8, 0xf9, 0x84, 0x3d, + 0x63, 0xf7, 0x46, 0x0b, 0x1d, 0xb0, 0xcc, 0x7f, 0x3c, 0x57, 0x16, 0xbd, 0x02, 0xc2, 0x5e, 0x70, + 0x19, 0x94, 0x94, 0x0b, 0x4a, 0x79, 0x2f, 0x26, 0xe5, 0x4a, 0xc8, 0x95, 0x90, 0x2b, 0x21, 0x3f, + 0x1c, 0x21, 0xcf, 0xdc, 0x28, 0x7a, 0xd0, 0x62, 0xee, 0x8f, 0x55, 0xfb, 0xf0, 0xe9, 0xe9, 0x42, + 0xc9, 0xfa, 0xde, 0x65, 0x5d, 0xc2, 0x5a, 0x28, 0x81, 0xe7, 0x16, 0xf8, 0xde, 0x71, 0x0a, 0x7c, + 0x4f, 0x09, 0x7c, 0x65, 0x04, 0xbe, 0x77, 0x2c, 0x02, 0xff, 0xaa, 0xdc, 0xd4, 0xc3, 0xbe, 0x4e, + 0x03, 0xad, 0x2a, 0x17, 0x89, 0xec, 0xde, 0x81, 0x95, 0x34, 0x4a, 0x3d, 0x05, 0x2e, 0x71, 0x94, + 0x7e, 0xb2, 0x40, 0xc9, 0xa3, 0xd4, 0xcb, 0xe0, 0x25, 0x90, 0xf2, 0x1f, 0x65, 0x96, 0x44, 0x12, + 0x5d, 0x15, 0xce, 0x4a, 0xe0, 0xd1, 0x73, 0x3c, 0x35, 0x63, 0xd6, 0xa5, 0x01, 0xda, 0x83, 0xc9, + 0xbc, 0xcd, 0xb9, 0x37, 0x1f, 0x71, 0xd5, 0x93, 0xf9, 0xb4, 0xfa, 0xd2, 0xe3, 0xdb, 0xc9, 0xfc, + 0xf1, 0x66, 0x6c, 0xde, 0xf9, 0x1f, 0xda, 0xe5, 0x91, 0x85, 0x05, 0x71, 0x6c, 0x67, 0xe6, 0x2c, + 0x3c, 0x2d, 0xac, 0x44, 0x29, 0x70, 0x76, 0x21, 0xf5, 0x0a, 0x75, 0xf2, 0x58, 0x1d, 0x62, 0x90, + 0x22, 0xeb, 0xb7, 0x86, 0x3d, 0x32, 0x88, 0xe3, 0x2e, 0x39, 0xce, 0xba, 0x14, 0x38, 0xad, 0xec, + 0x69, 0xf6, 0x62, 0x36, 0xc0, 0xae, 0xc0, 0x59, 0x65, 0x9e, 0xeb, 0x56, 0x3e, 0x1b, 0x76, 0xe0, + 0xfb, 0x95, 0x7e, 0x54, 0x54, 0xa4, 0xda, 0x79, 0xf4, 0xb0, 0x60, 0xd5, 0xf3, 0xe8, 0xf9, 0xa2, + 0x85, 0xbe, 0x37, 0xcb, 0x23, 0x5a, 0xf0, 0xbb, 0x40, 0x40, 0x22, 0x54, 0x15, 0x3d, 0x35, 0x75, + 0x17, 0xdd, 0xab, 0x8b, 0xab, 0xde, 0xeb, 0xee, 0xd5, 0x65, 0xfd, 0xe7, 0xf0, 0x00, 0x8e, 0xe7, + 0x05, 0x79, 0x34, 0x57, 0x33, 0x47, 0xfc, 0x36, 0x6e, 0xf3, 0xa8, 0xb2, 0x6d, 0xca, 0xb6, 0xd5, + 0xd0, 0xb6, 0x99, 0xd1, 0x3e, 0x66, 0x91, 0x42, 0x1c, 0x57, 0x1c, 0xcf, 0xac, 0xfa, 0xb8, 0xb3, + 0x3a, 0x08, 0x01, 0xb9, 0xcb, 0x3f, 0xb6, 0xd4, 0x18, 0x7f, 0x14, 0x78, 0x16, 0x5a, 0x39, 0x2d, + 0xf7, 0x05, 0xcd, 0xe6, 0xbd, 0xae, 0x5d, 0xf5, 0x9f, 0xef, 0x3b, 0xda, 0x55, 0x3f, 0xfc, 0xb1, + 0x13, 0xfc, 0x5f, 0xf8, 0x73, 0xf7, 0x5e, 0xd7, 0x2e, 0xd6, 0x3f, 0x5f, 0xde, 0xeb, 0xda, 0x65, + 0xbf, 0xf5, 0xf0, 0x70, 0xd6, 0xfa, 0xeb, 0xfc, 0x85, 0xff, 0xc1, 0x46, 0xd9, 0x61, 0xfa, 0xe9, + 0x0e, 0x97, 0xbc, 0x57, 0xe3, 0x25, 0xbf, 0x7e, 0xf6, 0x17, 0xc6, 0xd0, 0xc6, 0x37, 0xda, 0x2f, + 0xfd, 0xbf, 0xf4, 0xd3, 0x8b, 0x97, 0xd6, 0x75, 0xab, 0xb9, 0xfd, 0xbb, 0xeb, 0xd6, 0x5f, 0xfa, + 0xe9, 0xe5, 0x4b, 0xb3, 0x99, 0xf1, 0x97, 0x9f, 0xb2, 0xde, 0xd1, 0x7a, 0x6e, 0x36, 0x9b, 0xab, + 0xc5, 0x4e, 0x00, 0xe0, 0x5e, 0xef, 0xf4, 0x7f, 0x0a, 0x7e, 0x0c, 0xff, 0x8d, 0x20, 0x04, 0x6a, + 0xdc, 0x2a, 0x1f, 0x38, 0x35, 0xbb, 0x8b, 0xac, 0x8a, 0x57, 0xb8, 0xd8, 0x0e, 0x69, 0x82, 0xb9, + 0xed, 0x04, 0xab, 0xfd, 0xd0, 0x58, 0x5d, 0xc9, 0xf1, 0xd0, 0xd8, 0xc3, 0xc5, 0x12, 0x6f, 0x7f, + 0xfd, 0x14, 0x55, 0x51, 0x0f, 0x48, 0x9d, 0x35, 0x67, 0x3a, 0x77, 0x3c, 0xcf, 0x1c, 0x58, 0x18, + 0x72, 0x99, 0x08, 0x52, 0x35, 0xec, 0x57, 0xae, 0x8d, 0xa4, 0xe9, 0x3c, 0xdc, 0xcb, 0x24, 0x18, + 0xf7, 0xec, 0x43, 0x0b, 0x9f, 0x97, 0x20, 0x05, 0xc9, 0x44, 0x41, 0x70, 0xff, 0x40, 0xee, 0x42, + 0x71, 0xd4, 0x3f, 0x3f, 0x0e, 0xcc, 0x73, 0x4f, 0x5e, 0xd5, 0xaf, 0x66, 0x00, 0x84, 0xb3, 0x73, + 0x17, 0x7b, 0xd8, 0x1e, 0xe2, 0x32, 0xcd, 0xca, 0xbb, 0xf5, 0x05, 0x17, 0xe8, 0xed, 0xaf, 0x9f, + 0x76, 0x1f, 0x29, 0x86, 0xe3, 0xdb, 0x67, 0xac, 0x98, 0x9c, 0x80, 0x9a, 0xdd, 0x35, 0x57, 0xe6, + 0xa5, 0xa9, 0xf1, 0x14, 0x89, 0xd4, 0x9b, 0x52, 0xe3, 0x19, 0x91, 0xc6, 0x4e, 0x6e, 0x28, 0x29, + 0xe3, 0xd6, 0x01, 0x46, 0x71, 0x69, 0xb1, 0x29, 0x29, 0x72, 0xe7, 0x00, 0xa1, 0x51, 0x07, 0x91, + 0xbc, 0x07, 0xad, 0xf2, 0xaa, 0x31, 0x03, 0x0a, 0xf4, 0x34, 0xa8, 0x09, 0xec, 0xbe, 0xba, 0xcd, + 0xa0, 0xf6, 0xb7, 0x19, 0xc0, 0x8e, 0xf6, 0x43, 0x8e, 0xf0, 0xf3, 0x1d, 0xd5, 0xdf, 0x44, 0x3a, + 0xa6, 0x46, 0x41, 0x69, 0x92, 0x83, 0x87, 0x9d, 0xb7, 0xe7, 0xe4, 0x82, 0xc1, 0x7b, 0x34, 0x00, + 0x8e, 0x2c, 0xe7, 0xa7, 0x61, 0xae, 0x8b, 0xfc, 0xef, 0x9a, 0x73, 0xd8, 0x9d, 0x8d, 0xf2, 0xbf, + 0x0c, 0xbe, 0x2e, 0x92, 0x6e, 0x4b, 0xfb, 0xbc, 0x72, 0xf2, 0x8a, 0xfe, 0x9b, 0xad, 0x81, 0x52, + 0x2e, 0xb9, 0xa2, 0xef, 0x04, 0x61, 0xef, 0xf8, 0x10, 0xda, 0xd9, 0x01, 0xd8, 0xc1, 0x01, 0xd8, + 0xa9, 0xb1, 0x3d, 0x48, 0x86, 0x9d, 0xe4, 0xb1, 0x8f, 0x19, 0xd2, 0x0b, 0x36, 0x87, 0x49, 0x2c, + 0x6c, 0xd6, 0x25, 0xd6, 0xdd, 0xc6, 0x3c, 0x64, 0xe9, 0x08, 0xf6, 0xd2, 0xc5, 0x93, 0x36, 0x17, + 0x40, 0xc4, 0x1a, 0x6d, 0xaf, 0x67, 0xa6, 0x45, 0xca, 0xb5, 0x44, 0x34, 0x0b, 0x94, 0x65, 0x79, + 0xb2, 0x86, 0xcf, 0x30, 0x37, 0x60, 0x33, 0x03, 0x36, 0x2f, 0x79, 0x66, 0xa5, 0xc1, 0x89, 0xfd, + 0x5c, 0xdb, 0xb1, 0x19, 0x79, 0x58, 0xc9, 0x2b, 0x63, 0xd0, 0x14, 0x26, 0x96, 0x75, 0xd1, 0x0d, + 0xfd, 0xa6, 0x38, 0xb6, 0x3d, 0x59, 0x27, 0x69, 0x19, 0x99, 0x7b, 0xee, 0x1c, 0x2c, 0x3c, 0xd7, + 0xfa, 0x42, 0xbf, 0xc2, 0x0e, 0x3e, 0x84, 0x8e, 0xbe, 0xbf, 0x31, 0x00, 0xd5, 0x67, 0x86, 0xeb, + 0xc7, 0xa4, 0xd2, 0x1b, 0x53, 0xc3, 0xb2, 0x1c, 0x14, 0x23, 0xba, 0x33, 0xee, 0x8c, 0xe9, 0x03, + 0xb4, 0x81, 0x6b, 0xd8, 0x13, 0xec, 0x8b, 0xf9, 0x47, 0x6c, 0x8c, 0x2d, 0x93, 0xa6, 0x13, 0xd2, + 0x4d, 0x95, 0x66, 0x28, 0x51, 0x33, 0x2c, 0x4c, 0x9b, 0x9c, 0x77, 0x29, 0x9a, 0x21, 0x43, 0x3a, + 0x19, 0xfb, 0x67, 0x64, 0xe9, 0x85, 0x7d, 0x0a, 0x95, 0x2c, 0xc5, 0x70, 0xae, 0x57, 0x4e, 0x33, + 0x9c, 0x96, 0xba, 0x68, 0x97, 0xba, 0x7e, 0x00, 0xcb, 0x76, 0xa9, 0x77, 0x8f, 0x6c, 0xdd, 0xae, + 0xae, 0xae, 0xae, 0xea, 0xbf, 0x6e, 0xfb, 0x1d, 0x05, 0xd4, 0x12, 0x53, 0x55, 0x76, 0x8d, 0xa3, + 0x15, 0xba, 0xe9, 0xf7, 0xcc, 0x89, 0x8d, 0x47, 0x00, 0xc3, 0xbf, 0x6a, 0xa8, 0xcc, 0x7e, 0x89, + 0x66, 0x9f, 0x65, 0xf5, 0xcf, 0xf7, 0x66, 0xf5, 0xcf, 0xcb, 0x35, 0x20, 0xfe, 0xe4, 0x97, 0xaf, + 0x87, 0x7a, 0xfb, 0x1b, 0xc2, 0x7e, 0x8c, 0x7e, 0xed, 0x57, 0xec, 0xf2, 0xb8, 0x56, 0xec, 0xbc, + 0x5b, 0xfb, 0x15, 0xeb, 0x1c, 0xd5, 0x82, 0xa9, 0x60, 0x68, 0x2f, 0xce, 0x19, 0xdd, 0xa5, 0x59, + 0xd8, 0x60, 0xa7, 0x26, 0x6a, 0xaa, 0xdc, 0x1a, 0xc5, 0x66, 0x28, 0x01, 0x56, 0x6c, 0x86, 0x62, + 0x33, 0x14, 0x9b, 0x71, 0x80, 0x6c, 0x46, 0xa6, 0xc1, 0x5c, 0xd8, 0xa6, 0x63, 0x5b, 0xd8, 0x18, + 0xbb, 0x78, 0x4c, 0xcf, 0x08, 0xa6, 0x5a, 0x2a, 0x73, 0x59, 0xa6, 0xb9, 0xf4, 0x67, 0x9b, 0x96, + 0x15, 0xcc, 0x80, 0x1f, 0xfd, 0x78, 0x11, 0x60, 0xff, 0xca, 0x6a, 0x75, 0x21, 0x7b, 0x57, 0x28, + 0x99, 0xc1, 0xc6, 0xa7, 0x75, 0x96, 0xdb, 0xb4, 0x09, 0x76, 0xc7, 0xc6, 0x10, 0x07, 0xbb, 0xea, + 0x1b, 0x92, 0xd4, 0x0d, 0x33, 0x73, 0x9a, 0xce, 0xa0, 0xca, 0x17, 0x18, 0x7b, 0x42, 0xa6, 0x20, + 0x79, 0x89, 0x1a, 0x2a, 0x71, 0x39, 0x34, 0x71, 0xe1, 0x00, 0x20, 0xa5, 0x0d, 0x23, 0x95, 0xcf, + 0xee, 0x0f, 0x97, 0x1d, 0x4e, 0x59, 0x32, 0xe0, 0xe1, 0x7c, 0xe1, 0xe3, 0xd6, 0xfc, 0xc7, 0xab, + 0x21, 0xc7, 0x95, 0x79, 0x8e, 0xa0, 0x83, 0xfd, 0xfc, 0x7d, 0x8c, 0x55, 0x70, 0x13, 0x6f, 0x5f, + 0xb2, 0x2e, 0xcd, 0x8d, 0xce, 0x00, 0x51, 0x1a, 0x30, 0x5a, 0x2b, 0x1b, 0xc8, 0xfa, 0xf1, 0x00, + 0x99, 0xb7, 0x76, 0x42, 0x7d, 0x01, 0x2d, 0x6e, 0xa2, 0x9d, 0xb9, 0x3f, 0x60, 0xc3, 0x82, 0xfb, + 0xb6, 0x59, 0x4f, 0x28, 0xa3, 0xad, 0x7c, 0xdc, 0xcc, 0x36, 0x7f, 0xac, 0xc0, 0x12, 0x6d, 0xbc, + 0x64, 0x6d, 0x56, 0x3f, 0x02, 0xaf, 0x18, 0xb4, 0xb1, 0x34, 0xd5, 0x52, 0x89, 0x98, 0xf2, 0x8b, + 0x73, 0xe4, 0x05, 0x54, 0x4c, 0x00, 0xb4, 0x13, 0x52, 0xf9, 0x4d, 0xca, 0x6f, 0x52, 0x7e, 0x13, + 0x44, 0x89, 0x47, 0x69, 0x31, 0x86, 0x0a, 0xdf, 0xb4, 0x53, 0x0a, 0xfc, 0xd0, 0x14, 0xf8, 0x01, + 0x68, 0xb5, 0xce, 0x11, 0xa9, 0x35, 0x66, 0xee, 0xef, 0x88, 0x89, 0x8d, 0x1d, 0xba, 0xc3, 0xaf, + 0x62, 0x3d, 0xcd, 0x3b, 0x0f, 0xd6, 0x30, 0xbd, 0x5f, 0x8c, 0xaf, 0xf8, 0xb3, 0xe3, 0xa4, 0x15, + 0xcc, 0xf6, 0x19, 0xb1, 0x46, 0xfc, 0x4f, 0x89, 0xf3, 0x5f, 0x3f, 0xe3, 0x27, 0x73, 0x7d, 0xd6, + 0xeb, 0xe5, 0xd5, 0xcb, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x01, 0x00, 0x00, 0xff, 0xff, 0x3d, + 0x60, 0x76, 0xf1, 0x20, 0x0d, 0x02, 0x00, } ) From 2736e24684b6b8b0ed908fe05afaba6b67bcad32 Mon Sep 17 00:00:00 2001 From: steiler Date: Thu, 21 May 2026 11:18:00 +0200 Subject: [PATCH 4/4] Resolve union members on import and defaults per RFC 7950 - 9.12 InferUnionMemberFromTypedValue narrows unions from wire TypedValues (lexical re-parse + structural fallback); proto and XML importers and leaf defaults attach the matched branch so validation and EffectiveLeafType align. Adds ingress/parity tests and schema fixtures; drops the PR #423 supplement note. --- docs/pr-423-supplement.txt | 14 - pkg/tree/importer/import_config_adapter.go | 2 +- .../importer/proto/proto_tree_importer.go | 9 +- .../proto/proto_tree_importer_test.go | 7 +- pkg/tree/importer/union_member.go | 132 ++++++++ pkg/tree/importer/union_member_test.go | 30 ++ pkg/tree/importer/xml/xml_tree_importer.go | 13 +- .../importer/xml/xml_tree_importer_test.go | 17 + pkg/tree/ops/default_value.go | 18 +- pkg/tree/ops/default_value_test.go | 28 ++ ...dation_expand_union_ingress_matrix_test.go | 314 ++++++++++++++++++ .../validation_intent_expand_union_test.go | 135 ++++++++ .../validation_test_helpers_test.go | 98 +++++- ... validation_union_proto_roundtrip_test.go} | 39 +-- .../validation_union_xml_json_parity_test.go | 106 ++++++ pkg/tree/types/update.go | 10 +- pkg/tree/types/update_slice.go | 6 +- pkg/tree/types/update_slice_test.go | 29 ++ pkg/tree/types/update_test.go | 16 +- pkg/utils/converter.go | 82 +++-- pkg/utils/converter_test.go | 91 ++++- pkg/utils/testhelper/testhelpers.go | 10 +- tests/schema/sdcio_model.yang | 1 + 23 files changed, 1096 insertions(+), 111 deletions(-) delete mode 100644 docs/pr-423-supplement.txt create mode 100644 pkg/tree/importer/union_member.go create mode 100644 pkg/tree/importer/union_member_test.go create mode 100644 pkg/tree/ops/validation/validation_expand_union_ingress_matrix_test.go create mode 100644 pkg/tree/ops/validation/validation_intent_expand_union_test.go rename pkg/tree/ops/validation/{validation_union_proto_fallback_test.go => validation_union_proto_roundtrip_test.go} (67%) create mode 100644 pkg/tree/ops/validation/validation_union_xml_json_parity_test.go create mode 100644 pkg/tree/types/update_slice_test.go diff --git a/docs/pr-423-supplement.txt b/docs/pr-423-supplement.txt deleted file mode 100644 index e8c73cdd..00000000 --- a/docs/pr-423-supplement.txt +++ /dev/null @@ -1,14 +0,0 @@ -Paste into GitHub PR #423 description (append or merge with existing text). - ---- - -Closes https://github.com/sdcio/data-server/issues/177 (when merged). - -Follow-ups in this branch: -- navigateLeafRef / FollowLeafRef now use EffectiveLeafType so XPath and must evaluation agree with validateLeafRefs for union leaves whose matched branch is a leafref. -- Leaf-list range validation still uses the outer member type only; per-element union branch metadata is not modeled (see comment in validation_entry_range.go). -- Proto-imported config cannot recover which union branch matched; validators fall back to the outer union type (see validation_union_proto_fallback_test.go). Documented for reviewers. - -Tests added: union optional-instance leafref (warning vs error), navigateLeafRef + FollowLeafRef on union leafref, Update.Equal / String edge cases. - -Note: Regex compile-error path in validatePattern remains difficult to exercise with pyang-valid YANG patterns; consider a targeted refactor if coverage is required. diff --git a/pkg/tree/importer/import_config_adapter.go b/pkg/tree/importer/import_config_adapter.go index e4960f7b..7c74ee2d 100644 --- a/pkg/tree/importer/import_config_adapter.go +++ b/pkg/tree/importer/import_config_adapter.go @@ -31,7 +31,7 @@ type ImportConfigAdapterElement interface { // The String value is typically used for the keys. GetKeyValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (string, error) // GetTVValue returns the TypedValue based value defined via the SchemaLeafType. Can also only be called on Leafs or LeafLists. - // For union-typed leaves the second return value is the matched branch SchemaLeafType; for non-union or proto-loaded values it is nil. + // For union-typed leaves the second return value is the matched branch SchemaLeafType; for non-union leaves it is nil. Proto/XML import set it via InferUnionMemberFromTypedValue (RFC 7950 §9.12 first match). GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, *sdcpb.SchemaLeafType, error) // returns the name of the actual Level. GetName() string diff --git a/pkg/tree/importer/proto/proto_tree_importer.go b/pkg/tree/importer/proto/proto_tree_importer.go index 3ae5c901..cdeba505 100644 --- a/pkg/tree/importer/proto/proto_tree_importer.go +++ b/pkg/tree/importer/proto/proto_tree_importer.go @@ -87,16 +87,19 @@ func (p *ProtoTreeImporterElement) GetKeyValue(ctx context.Context, slt *sdcpb.S return tv.ToString(), nil } -// GetTVValue unmarshals the proto-serialized TypedValue. No original lexical context is available, -// so the matched union branch type is always nil (graceful fallback to outer schema type in validators). +// GetTVValue unmarshals the proto-serialized TypedValue. For union-typed leaves, +// InferUnionMemberFromTypedValue attaches the matched branch per RFC 7950 §9.12 +// (first matching member in schema order; same narrowing as TVFromStringWithType). func (p *ProtoTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, *sdcpb.SchemaLeafType, error) { result := &sdcpb.TypedValue{} err := proto.Unmarshal(p.data.LeafVariant, result) if err != nil { return nil, nil, err } - return result, nil, nil + matched := importer.InferUnionMemberFromTypedValue(result, slt) + return result, matched, nil } + func (p *ProtoTreeImporterElement) GetName() string { return p.data.Name } diff --git a/pkg/tree/importer/proto/proto_tree_importer_test.go b/pkg/tree/importer/proto/proto_tree_importer_test.go index ff6ca3de..c0fbd146 100644 --- a/pkg/tree/importer/proto/proto_tree_importer_test.go +++ b/pkg/tree/importer/proto/proto_tree_importer_test.go @@ -179,10 +179,9 @@ func TestProtoTreeImporter(t *testing.T) { } } -func TestProtoTreeImporterElement_GetTVValue_NilMatchedType(t *testing.T) { +func TestProtoTreeImporterElement_GetTVValue_UnionInfersUniqueBranch(t *testing.T) { ctx := context.Background() - // Proto importer has no original lexical context, so matched type must always be nil. tv := &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "hello"}} raw, err := proto.Marshal(tv) if err != nil { @@ -206,7 +205,7 @@ func TestProtoTreeImporterElement_GetTVValue_NilMatchedType(t *testing.T) { if err != nil { t.Fatalf("GetTVValue() unexpected error: %v", err) } - if matchedType != nil { - t.Errorf("proto importer should return nil matchedType, got %v", matchedType) + if matchedType == nil || matchedType.Type != "string" { + t.Fatalf("GetTVValue() matchedType = %v, want non-nil branch type string", matchedType) } } diff --git a/pkg/tree/importer/union_member.go b/pkg/tree/importer/union_member.go new file mode 100644 index 00000000..f9995076 --- /dev/null +++ b/pkg/tree/importer/union_member.go @@ -0,0 +1,132 @@ +package importer + +import sdcpb "github.com/sdcio/sdc-protos/sdcpb" + +// InferUnionMemberFromTypedValue returns the effective union branch type for a +// unmarshaled TypedValue and declared union schema. +// +// Resolution follows RFC 7950 §9.12: union member types are considered in schema +// declaration order (UnionTypes order). Lexically, the first branch for which +// TVFromStringWithType(tv.ToString()) succeeds and the result is TypedValue.Equal +// to tv wins (same narrowing as validating XML text against each member in order). +// +// If no branch matches lexically (for example pattern/length checks fail during +// lexical parse even though the wire TypedValue was produced from a structured +// union member), a structural fallback runs: among branches whose declared type +// matches tv's protobuf oneof shape (see tvShapeCompatibleWithBranch), the first +// such member in declaration order wins. +func InferUnionMemberFromTypedValue(tv *sdcpb.TypedValue, declared *sdcpb.SchemaLeafType) *sdcpb.SchemaLeafType { + if tv == nil || declared == nil || declared.Type != "union" { + return nil + } + if m := inferUnionLexicalEqual(tv, declared); m != nil { + return m + } + return inferUnionStructuralUnique(tv, declared) +} + +// inferUnionLexicalEqual narrows the union by re-parsing tv's canonical string +// through each branch type in declaration order. The first branch for which +// TVFromStringWithType succeeds and the result is Equal to tv wins (RFC 7950 §9.12). +// If none match, callers may fall back to structural inference. +func inferUnionLexicalEqual(tv *sdcpb.TypedValue, declared *sdcpb.SchemaLeafType) *sdcpb.SchemaLeafType { + lexical := tv.ToString() + ts := tv.GetTimestamp() // forwarded for branches whose lexical parse depends on time context + for _, branch := range declared.UnionTypes { + if branch == nil { + continue + } + refTV, matched, err := sdcpb.TVFromStringWithType(branch, lexical, ts) + if err != nil { + continue + } + if !refTV.Equal(tv) { + continue + } + return matched + } + return nil +} + +// inferUnionStructuralUnique picks a branch when lexical equality cannot decide: +// it considers branches whose declared type is compatible with tv's protobuf oneof +// "shape" (see tvShapeCompatibleWithBranch). Nested union branches recurse until +// a concrete leaf type matches or inner inference returns nil. The first +// compatible member in union declaration order wins (RFC 7950 §9.12). +func inferUnionStructuralUnique(tv *sdcpb.TypedValue, declared *sdcpb.SchemaLeafType) *sdcpb.SchemaLeafType { + var matches []*sdcpb.SchemaLeafType + for _, branch := range declared.UnionTypes { + if branch == nil { + continue + } + if !tvShapeCompatibleWithBranch(tv, branch) { + continue + } + if branch.Type == "union" { + // Record the resolved inner leaf (or sub-union member), not the nested union node itself. + if inner := InferUnionMemberFromTypedValue(tv, branch); inner != nil { + matches = append(matches, inner) + } + continue + } + matches = append(matches, branch) + } + if len(matches) == 0 { + return nil + } + return matches[0] +} + +// tvShapeCompatibleWithBranch is a coarse wire-type filter: it checks whether the +// TypedValue's protobuf variant could plausibly hold a value of branch's YANG type. +// It is not full validation (ranges, patterns, identities). Unknown or unmapped +// combinations return false; leafref recurses to the target type. +func tvShapeCompatibleWithBranch(tv *sdcpb.TypedValue, branch *sdcpb.SchemaLeafType) bool { + if tv == nil || branch == nil { + return false + } + if branch.Type == "union" { + for _, sub := range branch.UnionTypes { + if tvShapeCompatibleWithBranch(tv, sub) { + return true + } + } + return false + } + switch tv.Value.(type) { + case *sdcpb.TypedValue_StringVal: + switch branch.Type { + case "string", "enumeration", "bits", "binary", "identityref", "instance-identifier": + return true + case "leafref": + return branch.LeafrefTargetType != nil && tvShapeCompatibleWithBranch(tv, branch.LeafrefTargetType) + default: + return false + } + case *sdcpb.TypedValue_UintVal: + switch branch.Type { + case "uint8", "uint16", "uint32", "uint64": + return true + default: + return false + } + case *sdcpb.TypedValue_IntVal: + switch branch.Type { + case "int8", "int16", "int32", "int64": + return true + default: + return false + } + case *sdcpb.TypedValue_BoolVal: + return branch.Type == "boolean" + case *sdcpb.TypedValue_DecimalVal: + return branch.Type == "decimal64" + case *sdcpb.TypedValue_EmptyVal: + return branch.Type == "empty" + case *sdcpb.TypedValue_IdentityrefVal: + return branch.Type == "identityref" + default: + // e.g. JsonVal, JsonIetfVal, LeaflistVal — not handled by this wire-shape union filter. + return false + } +} diff --git a/pkg/tree/importer/union_member_test.go b/pkg/tree/importer/union_member_test.go new file mode 100644 index 00000000..c45ac47f --- /dev/null +++ b/pkg/tree/importer/union_member_test.go @@ -0,0 +1,30 @@ +package importer_test + +import ( + "testing" + + "github.com/sdcio/data-server/pkg/tree/importer" + sdcpb "github.com/sdcio/sdc-protos/sdcpb" +) + +func TestInferUnionMemberFromTypedValue_TwoStringBranchesRFCFirstMember(t *testing.T) { + tv := &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "hello"}} + first := &sdcpb.SchemaLeafType{Type: "string"} + second := &sdcpb.SchemaLeafType{Type: "string"} + decl := &sdcpb.SchemaLeafType{ + Type: "union", + UnionTypes: []*sdcpb.SchemaLeafType{first, second}, + } + m := importer.InferUnionMemberFromTypedValue(tv, decl) + if m != first { + t.Fatalf("RFC 7950 §9.12 first matching member: want first branch pointer, got %v (first=%p second=%p)", m, first, second) + } +} + +func TestInferUnionMemberFromTypedValue_NonUnion(t *testing.T) { + tv := &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "x"}} + decl := &sdcpb.SchemaLeafType{Type: "string"} + if m := importer.InferUnionMemberFromTypedValue(tv, decl); m != nil { + t.Fatalf("non-union declared type: want nil, got %v", m) + } +} diff --git a/pkg/tree/importer/xml/xml_tree_importer.go b/pkg/tree/importer/xml/xml_tree_importer.go index 52e791e8..69ad452a 100644 --- a/pkg/tree/importer/xml/xml_tree_importer.go +++ b/pkg/tree/importer/xml/xml_tree_importer.go @@ -4,6 +4,7 @@ import ( "context" "github.com/beevik/etree" + "github.com/sdcio/data-server/pkg/tree/importer" sdcpb "github.com/sdcio/sdc-protos/sdcpb" ) @@ -81,8 +82,18 @@ func (x *XmlTreeImporterElement) GetKeyValue(ctx context.Context, slt *sdcpb.Sch return tv.ToString(), nil } +// GetTVValue parses the element text with TVFromStringWithType. For union-typed +// leaves, InferUnionMemberFromTypedValue selects the matched branch using RFC 7950 +// §9.12 member order (same rules as proto tree import). func (x *XmlTreeImporterElement) GetTVValue(ctx context.Context, slt *sdcpb.SchemaLeafType) (*sdcpb.TypedValue, *sdcpb.SchemaLeafType, error) { - return sdcpb.TVFromStringWithType(slt, x.elem.Text(), 0) + tv, matched, err := sdcpb.TVFromStringWithType(slt, x.elem.Text(), 0) + if err != nil { + return nil, nil, err + } + if slt != nil && slt.Type == "union" { + return tv, importer.InferUnionMemberFromTypedValue(tv, slt), nil + } + return tv, matched, nil } func (x *XmlTreeImporterElement) GetName() string { diff --git a/pkg/tree/importer/xml/xml_tree_importer_test.go b/pkg/tree/importer/xml/xml_tree_importer_test.go index 0d4e81f2..01f872b3 100644 --- a/pkg/tree/importer/xml/xml_tree_importer_test.go +++ b/pkg/tree/importer/xml/xml_tree_importer_test.go @@ -241,11 +241,15 @@ func TestXmlTreeImporterElement_IdentityRef(t *testing.T) { func TestXmlTreeImporterElement_GetTVValue_MatchedType(t *testing.T) { ctx := context.Background() + firstStr := &sdcpb.SchemaLeafType{Type: "string"} + secondStr := &sdcpb.SchemaLeafType{Type: "string"} + tests := []struct { name string xmlText string slt *sdcpb.SchemaLeafType wantMatchedType string // expected matched branch TypeName ("" = nil expected) + wantSamePtrAs *sdcpb.SchemaLeafType // optional: assert matched branch is this union member (RFC order) wantErr bool }{ { @@ -278,6 +282,16 @@ func TestXmlTreeImporterElement_GetTVValue_MatchedType(t *testing.T) { }, wantMatchedType: "uint32", }, + { + name: "union_two_string_branches_rfc_first_member", + xmlText: "hello", + slt: &sdcpb.SchemaLeafType{ + Type: "union", + UnionTypes: []*sdcpb.SchemaLeafType{firstStr, secondStr}, + }, + wantMatchedType: "string", + wantSamePtrAs: firstStr, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -298,6 +312,9 @@ func TestXmlTreeImporterElement_GetTVValue_MatchedType(t *testing.T) { t.Errorf("matchedType = %v, want type %q", matchedType, tt.wantMatchedType) } } + if tt.wantSamePtrAs != nil && matchedType != tt.wantSamePtrAs { + t.Errorf("matchedType pointer %p, want same union member as %p (RFC 7950 §9.12 order)", matchedType, tt.wantSamePtrAs) + } }) } } diff --git a/pkg/tree/ops/default_value.go b/pkg/tree/ops/default_value.go index 30dd2478..cf95c179 100644 --- a/pkg/tree/ops/default_value.go +++ b/pkg/tree/ops/default_value.go @@ -20,41 +20,43 @@ func DefaultValueExists(schema *sdcpb.SchemaElem) bool { } func DefaultValueRetrieve(ctx context.Context, schema *sdcpb.SchemaElem, path *sdcpb.Path) (*types.Update, error) { - var tv *sdcpb.TypedValue - var err error switch schem := schema.GetSchema().(type) { case *sdcpb.SchemaElem_Field: defaultVal := schem.Field.GetDefault() if defaultVal == "" { return nil, fmt.Errorf("no defaults defined for schema path: %s", path.ToXPath(false)) } - tv, err = sdcpb.TVFromString(schem.Field.GetType(), defaultVal, 0) + tv, matched, err := sdcpb.TVFromStringWithType(schem.Field.GetType(), defaultVal, 0) if err != nil { return nil, err } + upd := types.NewUpdate(nil, tv, consts.DefaultValuesPrio, consts.DefaultsIntentName, 0) + if matched != nil { + upd.WithMatchedType(matched) + } + return upd, nil case *sdcpb.SchemaElem_Leaflist: listDefaults := schem.Leaflist.GetDefaults() if len(listDefaults) == 0 { return nil, fmt.Errorf("no defaults defined for schema path: %s", path.ToXPath(false)) } tvlist := make([]*sdcpb.TypedValue, 0, len(listDefaults)) - for _, dv := range schem.Leaflist.GetDefaults() { - tvelem, err := sdcpb.TVFromString(schem.Leaflist.GetType(), dv, 0) + for _, dv := range listDefaults { + tvelem, _, err := sdcpb.TVFromStringWithType(schem.Leaflist.GetType(), dv, 0) if err != nil { return nil, fmt.Errorf("error converting default to typed value for %s, type: %s ; value: %s; err: %v", path.ToXPath(false), schem.Leaflist.GetType().GetTypeName(), dv, err) } tvlist = append(tvlist, tvelem) } - tv = &sdcpb.TypedValue{ + tv := &sdcpb.TypedValue{ Value: &sdcpb.TypedValue_LeaflistVal{ LeaflistVal: &sdcpb.ScalarArray{ Element: tvlist, }, }, } + return types.NewUpdate(nil, tv, consts.DefaultValuesPrio, consts.DefaultsIntentName, 0), nil default: return nil, fmt.Errorf("no defaults defined for schema path: %s", path.ToXPath(false)) } - - return types.NewUpdate(nil, tv, consts.DefaultValuesPrio, consts.DefaultsIntentName, 0), nil } diff --git a/pkg/tree/ops/default_value_test.go b/pkg/tree/ops/default_value_test.go index 74484c58..5704f268 100644 --- a/pkg/tree/ops/default_value_test.go +++ b/pkg/tree/ops/default_value_test.go @@ -195,3 +195,31 @@ func TestDefaultValueRetrieve(t *testing.T) { }) } } + +// TestDefaultValueRetrieve_unionLeafDefault_attachsMatchedMemberType verifies PRD 004: schema-driven +// default strings for union leaves use TVFromStringWithType so EffectiveLeafType sees the resolved branch. +func TestDefaultValueRetrieve_unionLeafDefault_attachsMatchedMemberType(t *testing.T) { + ctx := context.Background() + sc, schema, err := testhelper.InitSDCIOSchema() + if err != nil { + t.Fatal(err) + } + scb := schemaClient.NewSchemaClientBound(schema, sc) + path := &sdcpb.Path{Elem: []*sdcpb.PathElem{sdcpb.NewPathElem("unionpatterntest", nil)}} + rsp, err := scb.GetSchemaSdcpbPath(ctx, path) + if err != nil { + t.Fatal(err) + } + upd, err := ops.DefaultValueRetrieve(ctx, rsp.GetSchema(), path) + if err != nil { + t.Fatalf("DefaultValueRetrieve: %v", err) + } + if got := upd.Value().GetUintVal(); got != 99 { + t.Fatalf("default typed value: want uint 99, got %#v", upd.Value()) + } + fallback := rsp.GetSchema().GetField().GetType() + eff := upd.EffectiveLeafType(fallback) + if eff == nil || eff.GetType() != "uint32" { + t.Fatalf("EffectiveLeafType: want uint32 branch, got %v (fallback type=%q)", eff, fallback.GetType()) + } +} diff --git a/pkg/tree/ops/validation/validation_expand_union_ingress_matrix_test.go b/pkg/tree/ops/validation/validation_expand_union_ingress_matrix_test.go new file mode 100644 index 00000000..75931708 --- /dev/null +++ b/pkg/tree/ops/validation/validation_expand_union_ingress_matrix_test.go @@ -0,0 +1,314 @@ +package validation_test + +import ( + "context" + "strings" + "testing" + + "github.com/openconfig/ygot/ygot" + "github.com/sdcio/data-server/pkg/config" + "github.com/sdcio/data-server/pkg/pool" + "github.com/sdcio/data-server/pkg/tree" + "github.com/sdcio/data-server/pkg/tree/consts" + "github.com/sdcio/data-server/pkg/tree/ops/validation" + "github.com/sdcio/data-server/pkg/utils/testhelper" + sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" + "go.uber.org/mock/gomock" +) + +// unionIngressMatrixRow is one observable validation scenario for issue 008 / PRD 11–12. +// Maintainer-facing ingress summary: docs/prd/union-member-resolution-validation/UNION-INGRESS.md +// +// Structured JSON import (ygot → tree) is the oracle; expansion paths must match error counts +// when the submitted encoding determinately selects a union member (inferable cases). +// +// Ambiguous shapes (e.g. two string union branches) are not present on the integration device +// model; see pkg/tree/importer/union_member_test.go and XML/proto importer tests for RFC 7950 +// §9.12 first-member union matching there. +type unionIngressMatrixRow struct { + name string + category string // pattern | range | length | leafref — PRD-named constraint families + req *sdcio_schema.Device + wantErrors int +} + +func unionIngressMatrixCases() []unionIngressMatrixRow { + return []unionIngressMatrixRow{ + // Pattern (union string branch carries regex; uint32 branch does not) + { + name: "inferable_pattern_string_branch_ok", + category: "pattern", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hallo AB"}, + }, + wantErrors: 0, + }, + { + name: "inferable_pattern_string_branch_violation", + category: "pattern", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hello AB"}, + }, + wantErrors: 1, + }, + { + name: "inferable_pattern_uint32_branch_no_pattern", + category: "pattern", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_Uint32{Uint32: 42}, + }, + wantErrors: 0, + }, + // Range + { + name: "inferable_range_uint32_in_range", + category: "range", + req: &sdcio_schema.Device{ + Unionrangetest: &sdcio_schema.SdcioModel_Unionrangetest_Union_Uint32{Uint32: 100}, + }, + wantErrors: 0, + }, + { + name: "inferable_range_uint32_out_of_range", + category: "range", + req: &sdcio_schema.Device{ + Unionrangetest: &sdcio_schema.SdcioModel_Unionrangetest_Union_Uint32{Uint32: 5}, + }, + wantErrors: 1, + }, + { + name: "inferable_range_string_branch_no_range", + category: "range", + req: &sdcio_schema.Device{ + Unionrangetest: &sdcio_schema.SdcioModel_Unionrangetest_Union_String{String: "hello"}, + }, + wantErrors: 0, + }, + // Length + { + name: "inferable_length_string_branch_ok", + category: "length", + req: &sdcio_schema.Device{ + Unionlengthtest: &sdcio_schema.SdcioModel_Unionlengthtest_Union_String{String: "12345678"}, + }, + wantErrors: 0, + }, + { + name: "inferable_length_string_branch_too_short", + category: "length", + req: &sdcio_schema.Device{ + Unionlengthtest: &sdcio_schema.SdcioModel_Unionlengthtest_Union_String{String: "12345"}, + }, + wantErrors: 1, + }, + { + name: "inferable_length_uint32_branch_no_length", + category: "length", + req: &sdcio_schema.Device{ + Unionlengthtest: &sdcio_schema.SdcioModel_Unionlengthtest_Union_Uint32{Uint32: 42}, + }, + wantErrors: 0, + }, + // Leafref (union member must be the leafref branch for required-instance resolution) + { + name: "inferable_leafref_resolves_when_target_exists", + category: "leafref", + req: &sdcio_schema.Device{ + Interface: map[string]*sdcio_schema.SdcioModel_Interface{ + "ethernet-1/1": {Name: ygot.String("ethernet-1/1")}, + }, + Unionleafreftest: ygot.String("ethernet-1/1"), + }, + wantErrors: 0, + }, + { + name: "inferable_leafref_errors_when_target_missing", + category: "leafref", + req: &sdcio_schema.Device{ + Interface: map[string]*sdcio_schema.SdcioModel_Interface{ + "ethernet-1/2": {Name: ygot.String("ethernet-1/2")}, + }, + Unionleafreftest: ygot.String("ethernet-1/1"), + }, + wantErrors: 1, + }, + } +} + +// TestValidate_Union_IngressMatrix_IntentExpandVsStructuredJSON runs issue 008 matrix under +// northbound-style Intent expansion (PRD 001 / ExpandAndConvertIntent vs structured JSON). +func TestValidate_Union_IngressMatrix_IntentExpandVsStructuredJSON(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + valConf := validationConfig + valConfLeafrefOnly := valConf.DeepCopy() + valConfLeafrefOnly.DisabledValidators.DisableAll() + valConfLeafrefOnly.DisabledValidators.Leafref = false + + for _, row := range unionIngressMatrixCases() { + t.Run(row.category+"/"+row.name, func(t *testing.T) { + vc := valConf + if row.category == "leafref" { + vc = valConfLeafrefOnly + } + + rootJSON, poolJSON := importDeviceJSON(t, ctx, scb, row.req) + resJSON, _ := validation.Validate(ctx, rootJSON.Entry, vc, poolJSON) + if got := len(resJSON.ErrorsStr()); got != row.wantErrors { + t.Fatalf("structured JSON: want %d errors, got %d: %v", + row.wantErrors, got, resJSON.ErrorsStr()) + } + + rootExpand, poolExpand := importDeviceIntentJSON(t, ctx, scb, row.req) + resExpand, _ := validation.Validate(ctx, rootExpand.Entry, vc, poolExpand) + t.Logf("intent expand errors:\n%s", strings.Join(resExpand.ErrorsStr(), "\n")) + if got := len(resExpand.ErrorsStr()); got != row.wantErrors { + t.Errorf("intent expansion: want %d errors (match structured), got %d: %v", + row.wantErrors, got, resExpand.ErrorsStr()) + } + }) + } +} + +// TestValidate_Union_IngressMatrix_RunningExpandVsStructuredJSON runs the same matrix under +// Running owner/priority (PRD 002 / gNMI Sync expansion parity). +func TestValidate_Union_IngressMatrix_RunningExpandVsStructuredJSON(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + valConf := validationConfig + valConfLeafrefOnly := valConf.DeepCopy() + valConfLeafrefOnly.DisabledValidators.DisableAll() + valConfLeafrefOnly.DisabledValidators.Leafref = false + + for _, row := range unionIngressMatrixCases() { + t.Run(row.category+"/"+row.name, func(t *testing.T) { + vc := valConf + if row.category == "leafref" { + vc = valConfLeafrefOnly + } + + rootJSON, poolJSON := importDeviceJSONWithOwnerPrio(t, ctx, scb, consts.RunningIntentName, consts.RunningValuesPrio, row.req) + resJSON, _ := validation.Validate(ctx, rootJSON.Entry, vc, poolJSON) + if got := len(resJSON.ErrorsStr()); got != row.wantErrors { + t.Fatalf("structured JSON (running): want %d errors, got %d: %v", + row.wantErrors, got, resJSON.ErrorsStr()) + } + + rootExpand, poolExpand := importDeviceExpandJSONWithOwnerPrio(t, ctx, scb, consts.RunningIntentName, consts.RunningValuesPrio, row.req) + resExpand, _ := validation.Validate(ctx, rootExpand.Entry, vc, poolExpand) + t.Logf("running expand errors:\n%s", strings.Join(resExpand.ErrorsStr(), "\n")) + if got := len(resExpand.ErrorsStr()); got != row.wantErrors { + t.Errorf("running expansion: want %d errors (match structured), got %d: %v", + row.wantErrors, got, resExpand.ErrorsStr()) + } + }) + } +} + +// TestValidate_Union_IngressMatrix_OptionalLeafref_ExpandVsStructuredJSON asserts best-effort +// behaviour (warning, not error) for optional-instance union leafref matches between structured +// import and expansion paths — PRD 12 / B semantics for inferable vs fallback. +func TestValidate_Union_IngressMatrix_OptionalLeafref_ExpandVsStructuredJSON(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + valConf := config.NewValidationConfig() + valConf.DisabledValidators.DisableAll() + valConf.DisabledValidators.Leafref = false + valConf.SetDisableConcurrency(true) + + cases := []struct { + name string + req *sdcio_schema.Device + wantErrors int + wantWarnings int + useRunningPrio bool + }{ + { + name: "ambiguous_optional_leafref_missing_target_warning_intent", + req: &sdcio_schema.Device{ + Unionoptionalleafreftest: ygot.String("ethernet-1/1"), + }, + wantErrors: 0, + wantWarnings: 1, + useRunningPrio: false, + }, + { + name: "ambiguous_optional_leafref_resolves_no_warning_intent", + req: &sdcio_schema.Device{ + Interface: map[string]*sdcio_schema.SdcioModel_Interface{ + "ethernet-1/1": {Name: ygot.String("ethernet-1/1")}, + }, + Unionoptionalleafreftest: ygot.String("ethernet-1/1"), + }, + wantErrors: 0, + wantWarnings: 0, + useRunningPrio: false, + }, + { + name: "ambiguous_optional_leafref_missing_target_warning_running", + req: &sdcio_schema.Device{ + Unionoptionalleafreftest: ygot.String("ethernet-1/1"), + }, + wantErrors: 0, + wantWarnings: 1, + useRunningPrio: true, + }, + { + name: "ambiguous_optional_leafref_resolves_no_warning_running", + req: &sdcio_schema.Device{ + Interface: map[string]*sdcio_schema.SdcioModel_Interface{ + "ethernet-1/1": {Name: ygot.String("ethernet-1/1")}, + }, + Unionoptionalleafreftest: ygot.String("ethernet-1/1"), + }, + wantErrors: 0, + wantWarnings: 0, + useRunningPrio: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + var rootJSON, rootExpand *tree.RootEntry + var poolJSON, poolExpand *pool.SharedTaskPool + + if tc.useRunningPrio { + rootJSON, poolJSON = importDeviceJSONWithOwnerPrio(t, ctx, scb, consts.RunningIntentName, consts.RunningValuesPrio, tc.req) + rootExpand, poolExpand = importDeviceExpandJSONWithOwnerPrio(t, ctx, scb, consts.RunningIntentName, consts.RunningValuesPrio, tc.req) + } else { + rootJSON, poolJSON = importDeviceJSON(t, ctx, scb, tc.req) + rootExpand, poolExpand = importDeviceIntentJSON(t, ctx, scb, tc.req) + } + + resJSON, _ := validation.Validate(ctx, rootJSON.Entry, valConf, poolJSON) + if len(resJSON.ErrorsStr()) != tc.wantErrors || len(resJSON.WarningsStr()) != tc.wantWarnings { + t.Fatalf("structured JSON: want %d errors %d warnings, got %d errors %d warnings\nerrors=%v\nwarnings=%v", + tc.wantErrors, tc.wantWarnings, len(resJSON.ErrorsStr()), len(resJSON.WarningsStr()), + resJSON.ErrorsStr(), resJSON.WarningsStr()) + } + + resExpand, _ := validation.Validate(ctx, rootExpand.Entry, valConf, poolExpand) + t.Logf("expand errors=%v warnings=%v", resExpand.ErrorsStr(), resExpand.WarningsStr()) + if len(resExpand.ErrorsStr()) != tc.wantErrors || len(resExpand.WarningsStr()) != tc.wantWarnings { + t.Errorf("expansion path: want %d errors %d warnings (match structured), got %d errors %d warnings\nerrors=%v\nwarnings=%v", + tc.wantErrors, tc.wantWarnings, len(resExpand.ErrorsStr()), len(resExpand.WarningsStr()), + resExpand.ErrorsStr(), resExpand.WarningsStr()) + } + }) + } +} diff --git a/pkg/tree/ops/validation/validation_intent_expand_union_test.go b/pkg/tree/ops/validation/validation_intent_expand_union_test.go new file mode 100644 index 00000000..646c0eac --- /dev/null +++ b/pkg/tree/ops/validation/validation_intent_expand_union_test.go @@ -0,0 +1,135 @@ +package validation_test + +import ( + "context" + "strings" + "testing" + + "github.com/sdcio/data-server/pkg/tree/consts" + "github.com/sdcio/data-server/pkg/tree/ops/validation" + "github.com/sdcio/data-server/pkg/utils/testhelper" + sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" + "go.uber.org/mock/gomock" +) + +// TestValidate_UnionPattern_IntentExpansionMatchesStructuredJSON verifies PRD 001: +// for an inferable union string branch, expanded intent updates carry matched type so +// branch pattern validation matches structured JSON import (same error count). +func TestValidate_UnionPattern_IntentExpansionMatchesStructuredJSON(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + req *sdcio_schema.Device + wantErrors int + }{ + { + name: "union string branch pattern matches", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hallo AB"}, + }, + wantErrors: 0, + }, + { + name: "union string branch pattern violation", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hello AB"}, + }, + wantErrors: 1, + }, + { + name: "union uint32 branch has no pattern", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_Uint32{Uint32: 42}, + }, + wantErrors: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rootJSON, poolJSON := importDeviceJSON(t, ctx, scb, tt.req) + resultJSON, _ := validation.Validate(ctx, rootJSON.Entry, validationConfig, poolJSON) + if len(resultJSON.ErrorsStr()) != tt.wantErrors { + t.Fatalf("structured JSON import: want %d errors, got %d: %v", + tt.wantErrors, len(resultJSON.ErrorsStr()), resultJSON.ErrorsStr()) + } + + rootExpand, poolExpand := importDeviceIntentJSON(t, ctx, scb, tt.req) + resultExpand, _ := validation.Validate(ctx, rootExpand.Entry, validationConfig, poolExpand) + t.Logf("intent expand validation errors:\n%s", strings.Join(resultExpand.ErrorsStr(), "\n")) + + if len(resultExpand.ErrorsStr()) != tt.wantErrors { + t.Errorf("intent expansion path: want %d errors (same as JSON import), got %d: %v", + tt.wantErrors, len(resultExpand.ErrorsStr()), resultExpand.ErrorsStr()) + } + }) + } +} + +// TestValidate_UnionPattern_RunningExpandMatchesStructuredJSON verifies PRD 002: gNMI Sync uses +// ExpandAndConvertIntent with Owner "running"; union member attribution on that path must match +// structured JSON import for the same owner/priority (branch pattern parity). +func TestValidate_UnionPattern_RunningExpandMatchesStructuredJSON(t *testing.T) { + ctx := context.Background() + mockCtrl := gomock.NewController(t) + + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + tests := []struct { + name string + req *sdcio_schema.Device + wantErrors int + }{ + { + name: "union string branch pattern matches", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hallo AB"}, + }, + wantErrors: 0, + }, + { + name: "union string branch pattern violation", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hello AB"}, + }, + wantErrors: 1, + }, + { + name: "union uint32 branch has no pattern", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_Uint32{Uint32: 42}, + }, + wantErrors: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rootJSON, poolJSON := importDeviceJSONWithOwnerPrio(t, ctx, scb, consts.RunningIntentName, consts.RunningValuesPrio, tt.req) + resultJSON, _ := validation.Validate(ctx, rootJSON.Entry, validationConfig, poolJSON) + if len(resultJSON.ErrorsStr()) != tt.wantErrors { + t.Fatalf("structured JSON import (running): want %d errors, got %d: %v", + tt.wantErrors, len(resultJSON.ErrorsStr()), resultJSON.ErrorsStr()) + } + + rootExpand, poolExpand := importDeviceExpandJSONWithOwnerPrio(t, ctx, scb, consts.RunningIntentName, consts.RunningValuesPrio, tt.req) + resultExpand, _ := validation.Validate(ctx, rootExpand.Entry, validationConfig, poolExpand) + t.Logf("running expand validation errors:\n%s", strings.Join(resultExpand.ErrorsStr(), "\n")) + + if len(resultExpand.ErrorsStr()) != tt.wantErrors { + t.Errorf("running expansion path: want %d errors (same as JSON import), got %d: %v", + tt.wantErrors, len(resultExpand.ErrorsStr()), resultExpand.ErrorsStr()) + } + }) + } +} diff --git a/pkg/tree/ops/validation/validation_test_helpers_test.go b/pkg/tree/ops/validation/validation_test_helpers_test.go index 49cca743..515d402b 100644 --- a/pkg/tree/ops/validation/validation_test_helpers_test.go +++ b/pkg/tree/ops/validation/validation_test_helpers_test.go @@ -4,35 +4,121 @@ import ( "context" "runtime" "testing" + "time" + "github.com/openconfig/ygot/ygot" schemaClient "github.com/sdcio/data-server/pkg/datastore/clients/schema" "github.com/sdcio/data-server/pkg/pool" "github.com/sdcio/data-server/pkg/tree" - "github.com/sdcio/data-server/pkg/tree/types" + xmlimporter "github.com/sdcio/data-server/pkg/tree/importer/xml" + "github.com/sdcio/data-server/pkg/tree/ops" + treetypes "github.com/sdcio/data-server/pkg/tree/types" "github.com/sdcio/data-server/pkg/utils/testhelper" sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" + sdcpb "github.com/sdcio/sdc-protos/sdcpb" +) + +const ( + importDeviceOwner = "owner1" + importDevicePriority = int32(5) ) // importDeviceJSON creates a fresh tree, imports req via JSON (owner "owner1", priority 5), // finishes the insertion phase, and returns the root entry and a shared pool ready for validation. func importDeviceJSON(t *testing.T, ctx context.Context, scb schemaClient.SchemaClientBound, req *sdcio_schema.Device) (*tree.RootEntry, *pool.SharedTaskPool) { t.Helper() + return importDeviceJSONWithOwnerPrio(t, ctx, scb, importDeviceOwner, importDevicePriority, req) +} - tc := tree.NewTreeContext(scb, pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0))) +// importDeviceJSONWithOwnerPrio is like importDeviceJSON but uses the given intent owner and priority. +func importDeviceJSONWithOwnerPrio(t *testing.T, ctx context.Context, scb schemaClient.SchemaClientBound, owner string, prio int32, req *sdcio_schema.Device) (*tree.RootEntry, *pool.SharedTaskPool) { + t.Helper() + return importDevice(t, ctx, scb, func(t *testing.T, root *tree.RootEntry) { + t.Helper() + if _, err := testhelper.LoadYgotStructIntoTreeRoot(ctx, req, root.Entry, owner, prio, false, treetypes.NewUpdateInsertFlags()); err != nil { + t.Fatal(err) + } + }) +} + +// importDeviceIntentJSON is like importDeviceJSON but applies the same device as RFC7951 JSON +// through ExpandAndConvertIntent (northbound-style expansion), for parity tests against structured import. +func importDeviceIntentJSON(t *testing.T, ctx context.Context, scb schemaClient.SchemaClientBound, req *sdcio_schema.Device) (*tree.RootEntry, *pool.SharedTaskPool) { + t.Helper() + return importDeviceExpandJSONWithOwnerPrio(t, ctx, scb, importDeviceOwner, importDevicePriority, req) +} + +// importDeviceExpandJSONWithOwnerPrio applies RFC7951 JSON for req through ExpandAndConvertIntent +// with the given owner and priority (same entry point as gNMI Sync uses for "running"). +func importDeviceExpandJSONWithOwnerPrio(t *testing.T, ctx context.Context, scb schemaClient.SchemaClientBound, owner string, prio int32, req *sdcio_schema.Device) (*tree.RootEntry, *pool.SharedTaskPool) { + t.Helper() + return importDevice(t, ctx, scb, func(t *testing.T, root *tree.RootEntry) { + t.Helper() + jstr, err := ygot.EmitJSON(req, &ygot.EmitJSONConfig{ + Format: ygot.RFC7951, + SkipValidation: true, + }) + if err != nil { + t.Fatalf("EmitJSON: %v", err) + } + expanded, err := treetypes.ExpandAndConvertIntent(ctx, scb, owner, prio, []*sdcpb.Update{{ + Path: &sdcpb.Path{}, + Value: &sdcpb.TypedValue{ + Value: &sdcpb.TypedValue_JsonVal{JsonVal: []byte(jstr)}, + }, + }}, time.Now().Unix()) + if err != nil { + t.Fatalf("ExpandAndConvertIntent: %v", err) + } + flags := treetypes.NewUpdateInsertFlags() + for _, pu := range expanded { + if _, err := ops.AddUpdateRecursive(ctx, root.Entry, pu.GetPath(), pu.GetUpdate(), flags); err != nil { + t.Fatalf("AddUpdateRecursive: %v", err) + } + } + }) +} + +func importDevice(t *testing.T, ctx context.Context, scb schemaClient.SchemaClientBound, apply func(t *testing.T, root *tree.RootEntry)) (*tree.RootEntry, *pool.SharedTaskPool) { + t.Helper() + tc := tree.NewTreeContext(scb, pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0))) root, err := tree.NewTreeRoot(ctx, tc) if err != nil { t.Fatal(err) } - if _, err = testhelper.LoadYgotStructIntoTreeRoot(ctx, req, root.Entry, "owner1", 5, false, types.NewUpdateInsertFlags()); err != nil { + apply(t, root) + + if err := root.FinishInsertionPhase(ctx); err != nil { t.Fatal(err) } - if err = root.FinishInsertionPhase(ctx); err != nil { + return root, pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0)) +} + +// importDeviceJSONThenCanonicalXMLReimport loads req via JSON (same owner/priority as importDeviceJSON), +// exports the tree with ops.ToXML, then imports that document with the XML tree importer into a fresh +// tree. Used to assert NETCONF/XML-style ingress sees the same validation outcomes as the JSON path +// when the logical configuration is equivalent (PRD 006). +func importDeviceJSONThenCanonicalXMLReimport(t *testing.T, ctx context.Context, scb schemaClient.SchemaClientBound, req *sdcio_schema.Device) (*tree.RootEntry, *pool.SharedTaskPool) { + t.Helper() + root, _ := importDeviceJSON(t, ctx, scb, req) + xmlDoc, err := ops.ToXML(ctx, root.Entry, false, false, false, false) + if err != nil { + t.Fatalf("ToXML: %v", err) + } + tc := tree.NewTreeContext(scb, pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0))) + rootXML, err := tree.NewTreeRoot(ctx, tc) + if err != nil { t.Fatal(err) } - sharedPool := pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0)) - return root, sharedPool + if _, err := rootXML.ImportConfig(ctx, nil, xmlimporter.NewXmlTreeImporter(&xmlDoc.Element, importDeviceOwner, importDevicePriority, false), treetypes.NewUpdateInsertFlags(), sharedPool); err != nil { + t.Fatalf("ImportConfig XML: %v", err) + } + if err := rootXML.FinishInsertionPhase(ctx); err != nil { + t.Fatal(err) + } + return rootXML, pool.NewSharedTaskPool(ctx, runtime.GOMAXPROCS(0)) } diff --git a/pkg/tree/ops/validation/validation_union_proto_fallback_test.go b/pkg/tree/ops/validation/validation_union_proto_roundtrip_test.go similarity index 67% rename from pkg/tree/ops/validation/validation_union_proto_fallback_test.go rename to pkg/tree/ops/validation/validation_union_proto_roundtrip_test.go index 0238169a..ec011847 100644 --- a/pkg/tree/ops/validation/validation_union_proto_fallback_test.go +++ b/pkg/tree/ops/validation/validation_union_proto_roundtrip_test.go @@ -18,15 +18,11 @@ import ( "go.uber.org/mock/gomock" ) -// TestValidate_Union_ProtoFallback verifies that proto-imported union leaves — -// where matchedUnionType is nil because no original lexical context is available — -// fall back gracefully to the outer schema type in all validators. -// Expected behaviour: -// - No panics in any validator. -// - Pattern/length/range/leafref validators silently skip because the outer -// union type carries none of those constraints. -// - This matches pre-change proto import behaviour (regression guard). -func TestValidate_Union_ProtoFallback(t *testing.T) { +// TestValidate_Union_ProtoRoundTripParity checks that after export/re-import via +// proto, union member inference (RFC 7950 §9.12–aligned via InferUnionMemberFromTypedValue) +// attaches the same effective branch type as JSON import, so validation matches the JSON path — not the +// older behaviour where matched type was always nil on proto. +func TestValidate_Union_ProtoRoundTripParity(t *testing.T) { tests := []struct { name string req *sdcio_schema.Device @@ -34,9 +30,6 @@ func TestValidate_Union_ProtoFallback(t *testing.T) { wantAfterProto int }{ { - // Valid value: matched string branch pattern ("hallo.*") satisfied. - // After proto round-trip the outer union type has no pattern constraint, - // so the result is still 0 errors — consistent behaviour. name: "valid union pattern value round-trips without error", req: &sdcio_schema.Device{ Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hallo AB"}, @@ -45,38 +38,28 @@ func TestValidate_Union_ProtoFallback(t *testing.T) { wantAfterProto: 0, }, { - // Pattern-violating value: matched branch has pattern "hallo.*" which - // "hello AB" does not satisfy → 1 error after JSON import. - // After proto round-trip matchedType is nil → fallback to outer union - // type (no pattern) → 0 errors. Validators must not panic. - name: "pattern-violating union value has no error after proto round-trip (fallback)", + name: "pattern-violating union value still errors after proto round-trip", req: &sdcio_schema.Device{ Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hello AB"}, }, wantAfterJSON: 1, - wantAfterProto: 0, + wantAfterProto: 1, }, { - // Length-violating value: matched string branch has length constraint - // min 8 chars; "123" is too short → 1 error after JSON import. - // After proto round-trip matchedType is nil → fallback → 0 errors. - name: "length-violating union value has no error after proto round-trip (fallback)", + name: "length-violating union value still errors after proto round-trip", req: &sdcio_schema.Device{ Unionlengthtest: &sdcio_schema.SdcioModel_Unionlengthtest_Union_String{String: "123"}, }, wantAfterJSON: 1, - wantAfterProto: 0, + wantAfterProto: 1, }, { - // Range-violating value: matched uint32 branch has range 10..300; - // 500 is out of range → 1 error after JSON import. - // After proto round-trip matchedType is nil → fallback → 0 errors. - name: "range-violating union value has no error after proto round-trip (fallback)", + name: "range-violating union value still errors after proto round-trip", req: &sdcio_schema.Device{ Unionrangetest: &sdcio_schema.SdcioModel_Unionrangetest_Union_Uint32{Uint32: 500}, }, wantAfterJSON: 1, - wantAfterProto: 0, + wantAfterProto: 1, }, } diff --git a/pkg/tree/ops/validation/validation_union_xml_json_parity_test.go b/pkg/tree/ops/validation/validation_union_xml_json_parity_test.go new file mode 100644 index 00000000..2798369f --- /dev/null +++ b/pkg/tree/ops/validation/validation_union_xml_json_parity_test.go @@ -0,0 +1,106 @@ +package validation_test + +import ( + "context" + "strings" + "testing" + + "github.com/openconfig/ygot/ygot" + "github.com/sdcio/data-server/pkg/tree/ops/validation" + "github.com/sdcio/data-server/pkg/utils/testhelper" + sdcio_schema "github.com/sdcio/data-server/tests/sdcioygot" + "go.uber.org/mock/gomock" +) + +// TestValidate_Union_XMLCanonicalReimportParityWithJSON asserts that configuration +// loaded through the XML tree importer after ops.ToXML (canonical device XML from a +// JSON-imported tree) yields the same validation error counts as direct JSON import +// for logically equivalent values where XML text parsing accepts the update (PRD +// 006 / issue 006). +// +// Cases where no union branch accepts the lexical form fail at XML import +// (TVFromStringWithType) rather than load-then-validate; those are covered separately +// and documented under issue 006 — they are not comparable to JSON+ygot paths that +// may still materialize a TypedValue and fail in validation. +func TestValidate_Union_XMLCanonicalReimportParityWithJSON(t *testing.T) { + tests := []struct { + name string + req *sdcio_schema.Device + wantAfterJSON int + wantAfterXMLRI int + }{ + { + name: "valid union pattern value survives XML re-import", + req: &sdcio_schema.Device{ + Unionpatterntest: &sdcio_schema.SdcioModel_Unionpatterntest_Union_String{String: "hallo AB"}, + }, + wantAfterJSON: 0, + wantAfterXMLRI: 0, + }, + { + name: "union range string branch same outcome after XML re-import", + req: &sdcio_schema.Device{ + Unionrangetest: &sdcio_schema.SdcioModel_Unionrangetest_Union_String{String: "hello"}, + }, + wantAfterJSON: 0, + wantAfterXMLRI: 0, + }, + { + name: "union range uint32 in-range same outcome after XML re-import", + req: &sdcio_schema.Device{ + Unionrangetest: &sdcio_schema.SdcioModel_Unionrangetest_Union_Uint32{Uint32: 100}, + }, + wantAfterJSON: 0, + wantAfterXMLRI: 0, + }, + { + name: "range-violating union uint32 still errors after XML re-import", + req: &sdcio_schema.Device{ + Unionrangetest: &sdcio_schema.SdcioModel_Unionrangetest_Union_Uint32{Uint32: 500}, + }, + wantAfterJSON: 1, + wantAfterXMLRI: 1, + }, + { + name: "union leafref branch resolves same after XML re-import", + req: &sdcio_schema.Device{ + Interface: map[string]*sdcio_schema.SdcioModel_Interface{ + "ethernet-1/1": { + Name: ygot.String("ethernet-1/1"), + }, + }, + Unionleafreftest: ygot.String("ethernet-1/1"), + }, + wantAfterJSON: 0, + wantAfterXMLRI: 0, + }, + } + + ctx := context.TODO() + mockCtrl := gomock.NewController(t) + + scb, err := testhelper.GetSchemaClientBound(t, mockCtrl) + if err != nil { + t.Fatal(err) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rootJSON, poolJSON := importDeviceJSON(t, ctx, scb, tt.req) + resJSON, _ := validation.Validate(ctx, rootJSON.Entry, validationConfig, poolJSON) + t.Logf("Validation errors after JSON import:\n%s", strings.Join(resJSON.ErrorsStr(), "\n")) + if got := len(resJSON.ErrorsStr()); got != tt.wantAfterJSON { + t.Fatalf("after JSON import: want %d error(s), got %d: %v", + tt.wantAfterJSON, got, resJSON.ErrorsStr()) + } + + rootXML, poolXML := importDeviceJSONThenCanonicalXMLReimport(t, ctx, scb, tt.req) + resXML, _ := validation.Validate(ctx, rootXML.Entry, validationConfig, poolXML) + t.Logf("Validation errors after XML re-import:\n%s", strings.Join(resXML.ErrorsStr(), "\n")) + if got := len(resXML.ErrorsStr()); got != tt.wantAfterXMLRI { + t.Errorf("after XML re-import: want %d error(s), got %d: %v", + tt.wantAfterXMLRI, got, resXML.ErrorsStr()) + } + }) + } +} diff --git a/pkg/tree/types/update.go b/pkg/tree/types/update.go index 03360398..3ee6da1f 100644 --- a/pkg/tree/types/update.go +++ b/pkg/tree/types/update.go @@ -149,10 +149,12 @@ func ExpandAndConvertIntent(ctx context.Context, scb utils.SchemaClientBound, in // temp storage for types.Update of the req. They are to be added later. newCacheUpdates := make([]*PathAndUpdate, 0, len(expandedReqUpdates)) - for _, u := range expandedReqUpdates { - upd := NewUpdate(nil, u.GetValue(), priority, intentName, ts) - // construct the types.Update - newCacheUpdates = append(newCacheUpdates, NewPathAndUpdate(u.GetPath(), upd)) + for _, e := range expandedReqUpdates { + upd := NewUpdate(nil, e.Update.GetValue(), priority, intentName, ts) + if e.MatchedUnionType != nil { + upd.WithMatchedType(e.MatchedUnionType) + } + newCacheUpdates = append(newCacheUpdates, NewPathAndUpdate(e.Update.GetPath(), upd)) } return newCacheUpdates, nil } diff --git a/pkg/tree/types/update_slice.go b/pkg/tree/types/update_slice.go index 72a67423..e755239f 100644 --- a/pkg/tree/types/update_slice.go +++ b/pkg/tree/types/update_slice.go @@ -14,7 +14,11 @@ type UpdateSlice []*Update func (u UpdateSlice) CopyWithNewOwnerAndPrio(owner string, prio int32) []*PathAndUpdate { result := make([]*PathAndUpdate, 0, len(u)) for _, x := range u { - result = append(result, NewPathAndUpdate(x.SdcpbPath(), NewUpdate(nil, x.Value(), prio, owner, x.Timestamp()))) + nu := NewUpdate(nil, x.Value(), prio, owner, x.Timestamp()) + if x.matchedUnionType != nil { + nu.WithMatchedType(x.matchedUnionType) + } + result = append(result, NewPathAndUpdate(x.SdcpbPath(), nu)) } return result } diff --git a/pkg/tree/types/update_slice_test.go b/pkg/tree/types/update_slice_test.go new file mode 100644 index 00000000..44223994 --- /dev/null +++ b/pkg/tree/types/update_slice_test.go @@ -0,0 +1,29 @@ +package types + +import ( + "testing" + + sdcpb "github.com/sdcio/sdc-protos/sdcpb" +) + +// TestUpdateSlice_CopyWithNewOwnerAndPrio_PreservesMatchedUnionType guards PRD 003 / sync writeback: +// intent updates copied onto the "running" owner for the sync tree must keep resolved union branch +// metadata so downstream validation is not weakened. +func TestUpdateSlice_CopyWithNewOwnerAndPrio_PreservesMatchedUnionType(t *testing.T) { + matched := &sdcpb.SchemaLeafType{TypeName: "uint32"} + wrongFallback := &sdcpb.SchemaLeafType{TypeName: "string"} + u := NewUpdate(nil, &sdcpb.TypedValue{Value: &sdcpb.TypedValue_UintVal{UintVal: 42}}, 10, "intent1", 99). + WithMatchedType(matched) + + out := UpdateSlice{u}.CopyWithNewOwnerAndPrio("running", 5) + if len(out) != 1 { + t.Fatalf("len(out) = %d, want 1", len(out)) + } + got := out[0].GetUpdate() + if got.EffectiveLeafType(wrongFallback) != matched { + t.Errorf("EffectiveLeafType: want matched branch %v, got %v", matched, got.EffectiveLeafType(wrongFallback)) + } + if got.Owner() != "running" || got.Priority() != 5 || got.Timestamp() != 99 { + t.Errorf("owner/prio/ts: got owner=%q prio=%d ts=%d", got.Owner(), got.Priority(), got.Timestamp()) + } +} diff --git a/pkg/tree/types/update_test.go b/pkg/tree/types/update_test.go index abfe6669..a3b926b3 100644 --- a/pkg/tree/types/update_test.go +++ b/pkg/tree/types/update_test.go @@ -18,10 +18,12 @@ func TestEffectiveLeafType_ReturnsFallback_WhenMatchedTypeNil(t *testing.T) { } } -func TestEffectiveLeafType_ReturnsMatchedType_WhenSet(t *testing.T) { - u := NewUpdate(nil, &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foo"}}, 0, "owner", 0) +// EffectiveLeafType does not derive schema type from TypedValue; it returns the +// union branch type stored by WithMatchedType when present (see importer GetTVValue). +func TestEffectiveLeafType_ReturnsMatchedUnionBranchWhenSet(t *testing.T) { + u := NewUpdate(nil, &sdcpb.TypedValue{Value: &sdcpb.TypedValue_UintVal{UintVal: 42}}, 0, "owner", 0) matched := &sdcpb.SchemaLeafType{TypeName: "uint32"} - fallback := &sdcpb.SchemaLeafType{TypeName: "string"} + fallback := &sdcpb.SchemaLeafType{TypeName: "uint64"} u.WithMatchedType(matched) result := u.EffectiveLeafType(fallback) @@ -32,7 +34,7 @@ func TestEffectiveLeafType_ReturnsMatchedType_WhenSet(t *testing.T) { } func TestWithMatchedType_IsFluent(t *testing.T) { - u := NewUpdate(nil, &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foo"}}, 0, "owner", 0) + u := NewUpdate(nil, &sdcpb.TypedValue{Value: &sdcpb.TypedValue_UintVal{UintVal: 42}}, 0, "owner", 0) matched := &sdcpb.SchemaLeafType{TypeName: "uint32"} returned := u.WithMatchedType(matched) @@ -44,7 +46,7 @@ func TestWithMatchedType_IsFluent(t *testing.T) { func TestDeepCopy_PreservesMatchedType(t *testing.T) { matched := &sdcpb.SchemaLeafType{TypeName: "uint32"} - u := NewUpdate(nil, &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foo"}}, 0, "owner", 0). + u := NewUpdate(nil, &sdcpb.TypedValue{Value: &sdcpb.TypedValue_UintVal{UintVal: 42}}, 0, "owner", 0). WithMatchedType(matched) copied := u.DeepCopy() @@ -55,7 +57,9 @@ func TestDeepCopy_PreservesMatchedType(t *testing.T) { } func TestEqual_IgnoresMatchedType(t *testing.T) { - tv := &sdcpb.TypedValue{Value: &sdcpb.TypedValue_StringVal{StringVal: "foo"}} + // Equal compares marshaled TypedValue (and owner/priority), not matchedUnionType. + // Same wire value: one update records the resolved union branch, one does not. + tv := &sdcpb.TypedValue{Value: &sdcpb.TypedValue_UintVal{UintVal: 42}} withMatched := NewUpdate(nil, tv, 0, "owner", 0). WithMatchedType(&sdcpb.SchemaLeafType{TypeName: "uint32"}) withoutMatched := NewUpdate(nil, tv, 0, "owner", 0) diff --git a/pkg/utils/converter.go b/pkg/utils/converter.go index 0af7dddc..f484b2da 100644 --- a/pkg/utils/converter.go +++ b/pkg/utils/converter.go @@ -21,6 +21,16 @@ type SchemaClientBound interface { GetSchemaElements(ctx context.Context, p *sdcpb.Path, done chan struct{}) (chan *sdcpb.GetSchemaResponse, error) } +// ExpandedUpdate is one leaf-level update after schema-driven expansion of a wire +// value. MatchedUnionType is set when conversion resolved a union member (see +// sdcpb.ConvertJsonValueToTvWithType); nil if not applicable. Union-typed leaf-lists +// intentionally leave MatchedUnionType nil in Phase 1 (see docs/prd/union-member- +// resolution-validation/issues/007-DECISION.md). +type ExpandedUpdate struct { + Update *sdcpb.Update + MatchedUnionType *sdcpb.SchemaLeafType +} + type Converter struct { schemaClientBound SchemaClientBound } @@ -31,8 +41,8 @@ func NewConverter(scb SchemaClientBound) *Converter { } } -func (c *Converter) ExpandUpdates(ctx context.Context, updates []*sdcpb.Update) ([]*sdcpb.Update, error) { - outUpdates := make([]*sdcpb.Update, 0, len(updates)) +func (c *Converter) ExpandUpdates(ctx context.Context, updates []*sdcpb.Update) ([]*ExpandedUpdate, error) { + outUpdates := make([]*ExpandedUpdate, 0, len(updates)) for idx, upd := range updates { _ = idx expUpds, err := c.ExpandUpdate(ctx, upd) @@ -45,9 +55,9 @@ func (c *Converter) ExpandUpdates(ctx context.Context, updates []*sdcpb.Update) } // expandUpdate Expands the value, in case of json to single typed value updates -func (c *Converter) ExpandUpdate(ctx context.Context, upd *sdcpb.Update) ([]*sdcpb.Update, error) { +func (c *Converter) ExpandUpdate(ctx context.Context, upd *sdcpb.Update) ([]*ExpandedUpdate, error) { log := logger.FromContext(ctx) - upds := make([]*sdcpb.Update, 0) + upds := make([]*ExpandedUpdate, 0) rsp, err := c.schemaClientBound.GetSchemaSdcpbPath(ctx, upd.GetPath()) if err != nil { return nil, err @@ -69,7 +79,7 @@ func (c *Converter) ExpandUpdate(ctx context.Context, upd *sdcpb.Update) ([]*sdc upd.Value = &sdcpb.TypedValue{ Value: &sdcpb.TypedValue_EmptyVal{}, } - return append(upds, upd), nil + return []*ExpandedUpdate{{Update: upd, MatchedUnionType: nil}}, nil } if len(upd.Path.Elem) > 0 && len(upd.Path.Elem[len(upd.Path.Elem)-1].Key) > 0 { newUpd := &sdcpb.Update{} @@ -91,7 +101,7 @@ func (c *Converter) ExpandUpdate(ctx context.Context, upd *sdcpb.Update) ([]*sdc return nil, err } } - upds = append(upds, newUpd) + upds = append(upds, &ExpandedUpdate{Update: newUpd, MatchedUnionType: nil}) return upds, nil } return nil, nil @@ -105,7 +115,7 @@ func (c *Converter) ExpandUpdate(ctx context.Context, upd *sdcpb.Update) ([]*sdc case *sdcpb.TypedValue_JsonVal: jsonDecoder = json.NewDecoder(bytes.NewReader(upd.GetValue().GetJsonVal())) default: - return []*sdcpb.Update{upd}, nil + return []*ExpandedUpdate{{Update: upd, MatchedUnionType: nil}}, nil } // don't decode into float64 but keep as a string // this solves issues created by reading long integers @@ -119,12 +129,13 @@ func (c *Converter) ExpandUpdate(ctx context.Context, upd *sdcpb.Update) ([]*sdc if err != nil { return nil, err } - upds := append(upds, rs...) + upds = append(upds, rs...) return upds, nil case *sdcpb.SchemaElem_Field: var v interface{} var err error + var matched *sdcpb.SchemaLeafType var jsonValue []byte if upd.GetValue() == nil { @@ -147,7 +158,7 @@ func (c *Converter) ExpandUpdate(ctx context.Context, upd *sdcpb.Update) ([]*sdc if err != nil { return nil, err } - upd.Value, err = sdcpb.ConvertJsonValueToTv(v, rsp.Field.GetType()) + upd.Value, matched, err = sdcpb.ConvertJsonValueToTvWithType(v, rsp.Field.GetType()) if err != nil { return nil, err } @@ -159,6 +170,7 @@ func (c *Converter) ExpandUpdate(ctx context.Context, upd *sdcpb.Update) ([]*sdc case rsp.Field.GetType().GetTypeName() != "string", rsp.Field.GetType().Type == "identityref": + matched = nil upd.Value, err = sdcpb.TVFromString(rsp.Field.GetType(), upd.GetValue().GetStringVal(), 0) if err != nil { return nil, err @@ -166,10 +178,10 @@ func (c *Converter) ExpandUpdate(ctx context.Context, upd *sdcpb.Update) ([]*sdc } } - upds = append(upds, upd) + upds = append(upds, &ExpandedUpdate{Update: upd, MatchedUnionType: matched}) return upds, nil case *sdcpb.SchemaElem_Leaflist: - upds = append(upds, upd) + upds = append(upds, &ExpandedUpdate{Update: upd, MatchedUnionType: nil}) return upds, nil } return nil, nil @@ -218,22 +230,25 @@ func (c *Converter) ExpandUpdateKeysAsLeaf(ctx context.Context, upd *sdcpb.Updat return upds, nil } -func (c *Converter) ExpandContainerValue(ctx context.Context, p *sdcpb.Path, jv any, cs *sdcpb.SchemaElem_Container) ([]*sdcpb.Update, error) { +func (c *Converter) ExpandContainerValue(ctx context.Context, p *sdcpb.Path, jv any, cs *sdcpb.SchemaElem_Container) ([]*ExpandedUpdate, error) { log := logger.FromContext(ctx) // log.Debugf("expanding jsonVal %T | %v | %v", jv, jv, p) switch jv := jv.(type) { case string: v := strings.Trim(jv, "\"") - return []*sdcpb.Update{ + return []*ExpandedUpdate{ { - Path: p, - Value: &sdcpb.TypedValue{ - Value: &sdcpb.TypedValue_StringVal{StringVal: v}, + Update: &sdcpb.Update{ + Path: p, + Value: &sdcpb.TypedValue{ + Value: &sdcpb.TypedValue_StringVal{StringVal: v}, + }, }, + MatchedUnionType: nil, }, }, nil case map[string]any: - upds := make([]*sdcpb.Update, 0) + upds := make([]*ExpandedUpdate, 0) // make sure all keys are present // and append them to path var keysInPath map[string]string @@ -299,23 +314,21 @@ func (c *Converter) ExpandContainerValue(ctx context.Context, p *sdcpb.Path, jv np := proto.Clone(p).(*sdcpb.Path) np.Elem = append(np.Elem, &sdcpb.PathElem{Name: item.Name}) upd := &sdcpb.Update{Path: np} + var err error switch item.GetType().GetType() { case "empty": upd.Value = &sdcpb.TypedValue{ Value: &sdcpb.TypedValue_EmptyVal{}, } + upds = append(upds, &ExpandedUpdate{Update: upd, MatchedUnionType: nil}) default: - schemaRsp, err := c.schemaClientBound.GetSchemaSdcpbPath(ctx, np) + var matched *sdcpb.SchemaLeafType + upd.Value, matched, err = sdcpb.ConvertJsonValueToTvWithType(v, item.GetType()) if err != nil { return nil, err } - upd.Value, err = sdcpb.SchemaElemToTV(schemaRsp.GetSchema(), fmt.Sprintf("%v", v), 0) - if err != nil { - return nil, err - } - + upds = append(upds, &ExpandedUpdate{Update: upd, MatchedUnionType: matched}) } - upds = append(upds, upd) case *sdcpb.LeafListSchema: // leaflist // log.Debugf("TODO: handling leafList %s", item.Name) np := proto.Clone(p).(*sdcpb.Path) @@ -351,7 +364,8 @@ func (c *Converter) ExpandContainerValue(ctx context.Context, p *sdcpb.Path, jv Value: &sdcpb.TypedValue_LeaflistVal{LeaflistVal: &sdcpb.ScalarArray{Element: list}}, }, } - upds = append(upds, upd) + // Phase 1: no list-level union branch on ExpandedUpdate (007-DECISION). + upds = append(upds, &ExpandedUpdate{Update: upd, MatchedUnionType: nil}) case string: // child container // log.Debugf("handling child container %s", item) @@ -363,17 +377,21 @@ func (c *Converter) ExpandContainerValue(ctx context.Context, p *sdcpb.Path, jv } switch rsp := rsp.GetSchema().Schema.(type) { case *sdcpb.SchemaElem_Container: - var rs []*sdcpb.Update + var rs []*ExpandedUpdate // code for presence containers m, ok := v.(map[string]any) if ok && len(m) == 0 && rsp.Container.IsPresence { - rs = []*sdcpb.Update{ + rs = []*ExpandedUpdate{ { - Path: np, - Value: &sdcpb.TypedValue{ - Value: &sdcpb.TypedValue_EmptyVal{}, + Update: &sdcpb.Update{ + Path: np, + Value: &sdcpb.TypedValue{ + Value: &sdcpb.TypedValue_EmptyVal{}, + }, }, - }} + MatchedUnionType: nil, + }, + } } else { rs, err = c.ExpandContainerValue(ctx, np, v, rsp) if err != nil { @@ -392,7 +410,7 @@ func (c *Converter) ExpandContainerValue(ctx context.Context, p *sdcpb.Path, jv } return upds, nil case []any: - upds := make([]*sdcpb.Update, 0) + upds := make([]*ExpandedUpdate, 0) for _, v := range jv { np := proto.Clone(p).(*sdcpb.Path) r, err := c.ExpandContainerValue(ctx, np, v, cs) diff --git a/pkg/utils/converter_test.go b/pkg/utils/converter_test.go index f4aeecb4..c1a4c6db 100644 --- a/pkg/utils/converter_test.go +++ b/pkg/utils/converter_test.go @@ -47,7 +47,7 @@ func TestExpandUpdateFieldJSONStringNormalizesQuotes(t *testing.T) { t.Fatalf("expected 1 update, got %d", len(updates)) } - got := updates[0].GetValue().GetStringVal() + got := updates[0].Update.GetValue().GetStringVal() if got != "aes128-cbc" { t.Fatalf("expected unquoted value %q, got %q", "aes128-cbc", got) } @@ -81,8 +81,95 @@ func TestExpandUpdateFieldJSONUint64PreservesPrecision(t *testing.T) { t.Fatalf("expected 1 update, got %d", len(updates)) } - got := updates[0].GetValue().GetUintVal() + got := updates[0].Update.GetValue().GetUintVal() if got != ^uint64(0) { t.Fatalf("expected max uint64 %d, got %d", ^uint64(0), got) } } + +func TestExpandUpdate_fieldJSONUnion_setsMatchedUnionBranchOnExpandedUpdate(t *testing.T) { + uint32Branch := &sdcpb.SchemaLeafType{Type: "uint32"} + stringBranch := &sdcpb.SchemaLeafType{Type: "string"} + unionDecl := &sdcpb.SchemaLeafType{ + Type: "union", + UnionTypes: []*sdcpb.SchemaLeafType{uint32Branch, stringBranch}, + } + converter := NewConverter(&testSchemaClientBound{ + getSchemaPathFn: func(context.Context, *sdcpb.Path) (*sdcpb.GetSchemaResponse, error) { + return &sdcpb.GetSchemaResponse{ + Schema: &sdcpb.SchemaElem{ + Schema: &sdcpb.SchemaElem_Field{ + Field: &sdcpb.LeafSchema{Type: unionDecl}, + }, + }, + }, nil + }, + }) + + expanded, err := converter.ExpandUpdate(context.Background(), &sdcpb.Update{ + Path: &sdcpb.Path{Elem: []*sdcpb.PathElem{{Name: "system"}, {Name: "u"}}}, + Value: &sdcpb.TypedValue{ + Value: &sdcpb.TypedValue_JsonVal{JsonVal: []byte(`42`)}, + }, + }) + if err != nil { + t.Fatalf("ExpandUpdate: %v", err) + } + if len(expanded) != 1 { + t.Fatalf("expected 1 expanded update, got %d", len(expanded)) + } + if expanded[0].MatchedUnionType == nil || expanded[0].MatchedUnionType.Type != "uint32" { + t.Fatalf("MatchedUnionType want uint32 branch, got %v", expanded[0].MatchedUnionType) + } + if expanded[0].Update.GetValue().GetUintVal() != 42 { + t.Fatalf("TypedValue: want uint 42, got %v", expanded[0].Update.GetValue()) + } +} + +// TestExpandUpdate_containerJSONUnionLeafList_leavesMatchedUnionTypeNil documents +// Phase 1: union-typed leaf-lists expand to a correct LeaflistVal but do not attach +// MatchedUnionType on the enclosing update (per docs/prd/union-member-resolution- +// validation/issues/007-DECISION.md). +func TestExpandUpdate_containerJSONUnionLeafList_leavesMatchedUnionTypeNil(t *testing.T) { + uint32Branch := &sdcpb.SchemaLeafType{Type: "uint32", TypeName: "uint32"} + stringBranch := &sdcpb.SchemaLeafType{Type: "string", TypeName: "string"} + unionDecl := &sdcpb.SchemaLeafType{ + Type: "union", + UnionTypes: []*sdcpb.SchemaLeafType{uint32Branch, stringBranch}, + } + containerSchema := &sdcpb.SchemaElem{ + Schema: &sdcpb.SchemaElem_Container{ + Container: &sdcpb.ContainerSchema{ + Name: "sys", + Leaflists: []*sdcpb.LeafListSchema{ + {Name: "items", Type: unionDecl}, + }, + }, + }, + } + converter := NewConverter(&testSchemaClientBound{ + getSchemaPathFn: func(context.Context, *sdcpb.Path) (*sdcpb.GetSchemaResponse, error) { + return &sdcpb.GetSchemaResponse{Schema: containerSchema}, nil + }, + }) + + expanded, err := converter.ExpandUpdate(context.Background(), &sdcpb.Update{ + Path: &sdcpb.Path{Elem: []*sdcpb.PathElem{{Name: "sys"}}}, + Value: &sdcpb.TypedValue{ + Value: &sdcpb.TypedValue_JsonVal{JsonVal: []byte(`{"items":[42,43]}`)}, + }, + }) + if err != nil { + t.Fatalf("ExpandUpdate: %v", err) + } + if len(expanded) != 1 { + t.Fatalf("expected 1 expanded update, got %d", len(expanded)) + } + if expanded[0].MatchedUnionType != nil { + t.Fatalf("MatchedUnionType must be nil for union leaf-list in Phase 1, got %#v", expanded[0].MatchedUnionType) + } + elems := expanded[0].Update.GetValue().GetLeaflistVal().GetElement() + if len(elems) != 2 || elems[0].GetUintVal() != 42 || elems[1].GetUintVal() != 43 { + t.Fatalf("LeaflistVal elements: want [42,43], got %#v", elems) + } +} diff --git a/pkg/utils/testhelper/testhelpers.go b/pkg/utils/testhelper/testhelpers.go index 66b1f46b..5dfb9c82 100644 --- a/pkg/utils/testhelper/testhelpers.go +++ b/pkg/utils/testhelper/testhelpers.go @@ -74,13 +74,21 @@ func ExpandUpdateFromConfig(ctx context.Context, conf *sdcio_schema.Device, conv return nil, err } - return converter.ExpandUpdate(ctx, + expanded, err := converter.ExpandUpdate(ctx, &sdcpb.Update{ Path: &sdcpb.Path{ Elem: []*sdcpb.PathElem{}, }, Value: &sdcpb.TypedValue{Value: &sdcpb.TypedValue_JsonVal{JsonVal: []byte(strJson)}}, }) + if err != nil { + return nil, err + } + out := make([]*sdcpb.Update, 0, len(expanded)) + for _, e := range expanded { + out = append(out, e.Update) + } + return out, nil } func AddToRoot(ctx context.Context, e api.Entry, updates []*sdcpb.Update, flags *types.UpdateInsertFlags, owner string, prio int32) error { diff --git a/tests/schema/sdcio_model.yang b/tests/schema/sdcio_model.yang index a6ad047d..2d8aac09 100644 --- a/tests/schema/sdcio_model.yang +++ b/tests/schema/sdcio_model.yang @@ -98,6 +98,7 @@ module sdcio_model { } type uint32; } + default "99"; } leaf unionlengthtest { type union {