From d7d6933a0bd6d7fd8cb9df66d954d13164043788 Mon Sep 17 00:00:00 2001 From: Daniel Lavrushin Date: Wed, 1 Apr 2026 13:00:18 +0200 Subject: [PATCH] fix: correct version number in Swagger documentation and enhance config loading with improved JSON parsing --- docs/static/swagger.json | 2 +- src/config/methods.go | 30 +++++++++++++++--- src/config/migration.go | 29 +++++++++++++++--- src/config/migration_test.go | 59 ++++++++++++++++++++++++++++++++++++ src/config/sparse_test.go | 44 +++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 10 deletions(-) diff --git a/docs/static/swagger.json b/docs/static/swagger.json index a1b5b947..33b6010c 100644 --- a/docs/static/swagger.json +++ b/docs/static/swagger.json @@ -4,7 +4,7 @@ "description": "B4 network packet processor REST API", "title": "B4 API", "contact": {}, - "version": "1.47.0" + "version": "1.0.0" }, "basePath": "/api", "paths": { diff --git a/src/config/methods.go b/src/config/methods.go index 105def3c..cd4ec664 100644 --- a/src/config/methods.go +++ b/src/config/methods.go @@ -65,17 +65,37 @@ func (c *Config) LoadFromFile(path string) error { if err != nil { return log.Errorf("failed to read config file: %v", err) } - err = json.Unmarshal(data, c) - if err != nil { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { return log.Errorf("failed to parse config file: %v", err) } - ApplyConfigDefaults(c) - for _, set := range c.Sets { - ApplySetDefaults(set) + rawSets := raw["sets"] + delete(raw, "sets") + + withoutSets, _ := json.Marshal(raw) + if err := json.Unmarshal(withoutSets, c); err != nil { + return log.Errorf("failed to parse config file: %v", err) } + if rawSets != nil { + var setArray []json.RawMessage + if err := json.Unmarshal(rawSets, &setArray); err != nil { + return log.Errorf("failed to parse sets: %v", err) + } + c.Sets = make([]*SetConfig, 0, len(setArray)) + for _, rawSet := range setArray { + set := NewSetConfig() + if err := json.Unmarshal(rawSet, &set); err != nil { + return log.Errorf("failed to parse set: %v", err) + } + c.Sets = append(c.Sets, &set) + } + } + + ApplyConfigDefaults(c) + return nil } diff --git a/src/config/migration.go b/src/config/migration.go index 1973b3b5..23893294 100644 --- a/src/config/migration.go +++ b/src/config/migration.go @@ -443,15 +443,36 @@ func (c *Config) LoadWithMigration(path string) error { return log.Errorf("failed to parse config file: %v", err) } - if err := json.Unmarshal(data, c); err != nil { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { return log.Errorf("failed to parse config file: %v", err) } - ApplyConfigDefaults(c) - for _, set := range c.Sets { - ApplySetDefaults(set) + rawSets := raw["sets"] + delete(raw, "sets") + + withoutSets, _ := json.Marshal(raw) + if err := json.Unmarshal(withoutSets, c); err != nil { + return log.Errorf("failed to parse config file: %v", err) } + if rawSets != nil { + var setArray []json.RawMessage + if err := json.Unmarshal(rawSets, &setArray); err != nil { + return log.Errorf("failed to parse sets: %v", err) + } + c.Sets = make([]*SetConfig, 0, len(setArray)) + for _, rs := range setArray { + set := NewSetConfig() + if err := json.Unmarshal(rs, &set); err != nil { + return log.Errorf("failed to parse set: %v", err) + } + c.Sets = append(c.Sets, &set) + } + } + + ApplyConfigDefaults(c) + if c.Version < CurrentConfigVersion { log.Infof("Config version %d is older than current version %d, migrating", c.Version, CurrentConfigVersion) diff --git a/src/config/migration_test.go b/src/config/migration_test.go index d7de0644..1060c6af 100644 --- a/src/config/migration_test.go +++ b/src/config/migration_test.go @@ -1,12 +1,46 @@ package config import ( + "fmt" "os" "path/filepath" + "reflect" "strings" "testing" ) +func deepCompareFields(prefix string, expected, actual reflect.Value) []string { + var diffs []string + if expected.Kind() == reflect.Ptr { + if expected.IsNil() && actual.IsNil() { + return nil + } + if expected.IsNil() != actual.IsNil() { + return []string{fmt.Sprintf("%s: nil mismatch", prefix)} + } + return deepCompareFields(prefix, expected.Elem(), actual.Elem()) + } + if expected.Kind() == reflect.Struct { + t := expected.Type() + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if !f.IsExported() { + continue + } + name := prefix + "." + f.Name + if prefix == "" { + name = f.Name + } + diffs = append(diffs, deepCompareFields(name, expected.Field(i), actual.Field(i))...) + } + return diffs + } + if !reflect.DeepEqual(expected.Interface(), actual.Interface()) { + diffs = append(diffs, fmt.Sprintf("%s: want %v, got %v", prefix, expected.Interface(), actual.Interface())) + } + return diffs +} + func TestLoadWithMigration(t *testing.T) { tmpDir := t.TempDir() @@ -70,6 +104,31 @@ func TestLoadWithMigration(t *testing.T) { t.Errorf("version should remain %d", CurrentConfigVersion) } }) + + t.Run("sparse roundtrip preserves all set defaults", func(t *testing.T) { + path := filepath.Join(tmpDir, "sparse_all.json") + cfg := NewConfig() + set := NewSetConfig() + set.Id = "all-defaults" + set.Name = "AllDefaults" + cfg.Sets = []*SetConfig{&set} + cfg.SaveToFile(path) + + loaded := NewConfig() + if err := loaded.LoadWithMigration(path); err != nil { + t.Fatalf("LoadWithMigration failed: %v", err) + } + + ls := loaded.Sets[0] + expected := NewSetConfig() + expected.Id = "all-defaults" + expected.Name = "AllDefaults" + + diffs := deepCompareFields("", reflect.ValueOf(expected), reflect.ValueOf(*ls)) + for _, d := range diffs { + t.Errorf("%s", d) + } + }) } func TestDiscoverConfigPath(t *testing.T) { diff --git a/src/config/sparse_test.go b/src/config/sparse_test.go index c9862449..5a78a227 100644 --- a/src/config/sparse_test.go +++ b/src/config/sparse_test.go @@ -202,6 +202,50 @@ func TestSparseSaveLoadRoundtrip_BoolDefaults(t *testing.T) { } } +func TestSparseSaveLoadRoundtrip_TrueBoolDefaultsSurvive(t *testing.T) { + tmpDir := t.TempDir() + path := filepath.Join(tmpDir, "config.json") + + cfg := NewConfig() + set := NewSetConfig() + set.Id = "bool-defaults" + set.Name = "BoolDefaults" + set.Faking.TTL = 8 + cfg.Sets = []*SetConfig{&set} + + if err := cfg.SaveToFile(path); err != nil { + t.Fatalf("SaveToFile failed: %v", err) + } + + loaded := NewConfig() + if err := loaded.LoadFromFile(path); err != nil { + t.Fatalf("LoadFromFile failed: %v", err) + } + + ls := loaded.Sets[0] + if ls.Faking.SNI != DefaultSetConfig.Faking.SNI { + t.Errorf("expected Faking.SNI=%v (default), got %v", DefaultSetConfig.Faking.SNI, ls.Faking.SNI) + } + if ls.Fragmentation.ReverseOrder != DefaultSetConfig.Fragmentation.ReverseOrder { + t.Errorf("expected ReverseOrder=%v (default), got %v", DefaultSetConfig.Fragmentation.ReverseOrder, ls.Fragmentation.ReverseOrder) + } + if ls.Fragmentation.MiddleSNI != DefaultSetConfig.Fragmentation.MiddleSNI { + t.Errorf("expected MiddleSNI=%v (default), got %v", DefaultSetConfig.Fragmentation.MiddleSNI, ls.Fragmentation.MiddleSNI) + } + if ls.Fragmentation.Combo.FirstByteSplit != DefaultSetConfig.Fragmentation.Combo.FirstByteSplit { + t.Errorf("expected FirstByteSplit=%v (default), got %v", DefaultSetConfig.Fragmentation.Combo.FirstByteSplit, ls.Fragmentation.Combo.FirstByteSplit) + } + if ls.Fragmentation.Combo.ExtensionSplit != DefaultSetConfig.Fragmentation.Combo.ExtensionSplit { + t.Errorf("expected ExtensionSplit=%v (default), got %v", DefaultSetConfig.Fragmentation.Combo.ExtensionSplit, ls.Fragmentation.Combo.ExtensionSplit) + } + if ls.Enabled != DefaultSetConfig.Enabled { + t.Errorf("expected Enabled=%v (default), got %v", DefaultSetConfig.Enabled, ls.Enabled) + } + if ls.UDP.FilterSTUN != DefaultSetConfig.UDP.FilterSTUN { + t.Errorf("expected FilterSTUN=%v (default), got %v", DefaultSetConfig.UDP.FilterSTUN, ls.UDP.FilterSTUN) + } +} + func TestMarshalSparse_OmitsDerivedDiscoveryMarks(t *testing.T) { cfg := NewConfig() set := NewSetConfig()