diff --git a/v3/go.mod b/v3/go.mod index c5b10feae..ea885c09e 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -3,7 +3,7 @@ module github.com/zmap/zlint/v3 go 1.24.0 require ( - github.com/pelletier/go-toml v1.9.5 + github.com/pelletier/go-toml/v2 v2.2.4 github.com/sirupsen/logrus v1.9.3 github.com/zmap/zcrypto v0.0.0-20251227215108-5ca1211d486b golang.org/x/crypto v0.46.0 diff --git a/v3/go.sum b/v3/go.sum index 436657517..8a1483523 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -5,8 +5,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= diff --git a/v3/lint/configuration.go b/v3/lint/configuration.go index 9c60a97cb..1f292d237 100644 --- a/v3/lint/configuration.go +++ b/v3/lint/configuration.go @@ -22,13 +22,13 @@ import ( "reflect" "strings" - "github.com/pelletier/go-toml" + "github.com/pelletier/go-toml/v2" ) // Configuration is a ZLint configuration which serves as a target // to hold the full TOML tree that is a physical ZLint configuration./ type Configuration struct { - tree *toml.Tree + tree map[string]any } // MaybeConfigure is a thin wrapper over Configure. @@ -93,11 +93,14 @@ func (c Configuration) Configure(lint interface{}, namespace string) error { // The contents of the provided reader MUST be in a valid TOML format. The caller of this function // is responsible for closing the reader, if appropriate. func NewConfig(r io.Reader) (Configuration, error) { - tree, err := toml.LoadReader(r) - if err != nil { + var tree map[string]any + if err := toml.NewDecoder(r).Decode(&tree); err != nil { return Configuration{}, err } - return Configuration{tree}, nil + if tree == nil { + tree = map[string]any{} + } + return Configuration{tree: tree}, nil } // NewConfigFromFile attempts to instantiate a configuration from the provided filesystem path. @@ -167,15 +170,41 @@ func NewEmptyConfig() Configuration { // If there is no such namespace found in this configuration then provided the namespace specific data encoded // within `target` is left unmodified. However, configuration of higher scoped fields will still be attempted. func (c Configuration) deserializeConfigInto(target interface{}, namespace string) error { - if tree := c.tree.Get(namespace); tree != nil { - err := tree.(*toml.Tree).Unmarshal(target) + if tree := getNamespace(c.tree, namespace); tree != nil { + b, err := toml.Marshal(tree) if err != nil { return err } + if err := toml.Unmarshal(b, target); err != nil { + return err + } } return c.resolveHigherScopedReferences(target) } +func getNamespace(doc map[string]any, namespace string) any { + if doc == nil { + return nil + } + if namespace == "" { + return doc + } + + cur := any(doc) + for part := range strings.SplitSeq(namespace, ".") { + m, ok := cur.(map[string]any) + if !ok { + return nil + } + next, ok := m[part] + if !ok { + return nil + } + cur = next + } + return cur +} + // resolveHigherScopeReferences takes in an interface{} value and attempts to // find any field within its inner value that is either a struct or a pointer // to a struct that is one of our global configurable types. If such a field diff --git a/v3/lint/configuration_test.go b/v3/lint/configuration_test.go index 0f7d59062..0c45f1037 100644 --- a/v3/lint/configuration_test.go +++ b/v3/lint/configuration_test.go @@ -21,9 +21,70 @@ import ( "sync" "testing" - "github.com/pelletier/go-toml" + "github.com/pelletier/go-toml/v2" ) +func TestNestedConfigs(t *testing.T) { + c, err := NewConfigFromString(` +[Test] +A = "Test" +[Test.Sub1] +A = "Test.Sub1" +[Test.Sub1.Sub2] +A = "Test.Sub1.Sub2" +[Test2.Sub3.Sub4] +A = "Test2.Sub3.Sub4" +[Test3.ConfigA.ConfigB] +A = "Test3.ConfigA.ConfigB.A" +`) + if err != nil { + t.Fatal(err) + } + type Test struct { + A string + } + type Test2 struct { + ConfigA struct{ ConfigB Test } + } + t.Run("nested configs", func(t *testing.T) { + for _, ns := range []string{"Test", "Test.Sub1", "Test.Sub1.Sub2", "Test2.Sub3.Sub4"} { + test := Test{} + err = c.Configure(&test, ns) + if err != nil { + t.Fatal(err) + } + + if test.A != ns { + t.Fatalf("wanted %q got %q", ns, test.A) + } + } + }) + t.Run("nested config from top", func(t *testing.T) { + got := Test2{} + err = c.Configure(&got, "Test3") + if err != nil { + t.Fatal(err) + } + + want := Test2{} + want.ConfigA.ConfigB.A = "Test3.ConfigA.ConfigB.A" + if !reflect.DeepEqual(got, want) { + t.Fatalf("wanted %+v got %+v", want, got) + } + }) + t.Run("nested config", func(t *testing.T) { + got := Test{} + err = c.Configure(&got, "Test3.ConfigA.ConfigB") + if err != nil { + t.Fatal(err) + } + want := "Test3.ConfigA.ConfigB.A" + if got.A != want { + t.Fatalf("wanted %q got %q", want, got) + } + }) +} + func TestInt(t *testing.T) { type Test struct { A int @@ -383,7 +444,7 @@ func TestSmokeExamplePrinting(t *testing.T) { go func() { defer wg.Done() defer w.Close() - err = toml.NewEncoder(w).Indentation("").CompactComments(true).Encode(mapping) + err = toml.NewEncoder(w).SetIndentSymbol("").SetIndentTables(false).Encode(mapping) }() if err != nil { t.Fatal(err) @@ -930,8 +991,7 @@ func TestPrintConfiguration(t *testing.T) { // I'm not a huge fan of this sort of test since it will have to be updated // on the slightest change, but it's better than not have a test for printing // out the configuration file. - want := ` -[AppleRootStorePolicyConfig] + want := `[AppleRootStorePolicyConfig] [CABFBaselineRequirementsConfig] CrossSignedCa = false @@ -1015,10 +1075,9 @@ func TestNewGlobalWithPrivateMembersDontGetPrinted(t *testing.T) { // I'm not a huge fan of this sort of test since it will have to be updated // on the slightest change, but it's better than not have a test for printing // out the configuration file. - want := ` -[this_is_a_test] + want := `[this_is_a_test] A = 1 -B = "2" +B = '2' ` if got != want { t.Fatalf("wanted '%s' but got '%s'", want, got) @@ -1095,12 +1154,8 @@ func TestStripGlobalsFromStructWithPrivates(t *testing.T) { func TestNewEmptyConfig(t *testing.T) { c := NewEmptyConfig() - got, err := c.tree.Marshal() - if err != nil { - t.Fatal(err) - } - if got != nil { - t.Fatalf("wanted nil byte slice, got %s", string(got)) + if len(c.tree) != 0 { + t.Fatalf("wanted empty config, got %#v", c.tree) } } @@ -1172,12 +1227,8 @@ func TestEmptyConfigFromEmptyPath(t *testing.T) { if err != nil { t.Fatal(err) } - got, err := c.tree.Marshal() - if err != nil { - t.Fatal(err) - } - if got != nil { - t.Fatalf("wanted nil byte slice, got %s", string(got)) + if len(c.tree) != 0 { + t.Fatalf("wanted empty config, got %#v", c.tree) } } diff --git a/v3/lint/registration.go b/v3/lint/registration.go index 4cab32ace..e7fa0cce2 100644 --- a/v3/lint/registration.go +++ b/v3/lint/registration.go @@ -24,7 +24,7 @@ import ( "sort" "strings" - "github.com/pelletier/go-toml" + "github.com/pelletier/go-toml/v2" ) // FilterOptions is a struct used by Registry.Filter to create a sub registry @@ -493,7 +493,7 @@ func (r *registryImpl) defaultConfiguration(globals []GlobalConfiguration) ([]by } w := &bytes.Buffer{} - err := toml.NewEncoder(w).Indentation("").CompactComments(true).Encode(configurables) + err := toml.NewEncoder(w).SetIndentSymbol("").SetIndentTables(false).Encode(configurables) if err != nil { return nil, err }