lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 54eaab60f7e718dccc8958aed9f47e2696df4941
parent a599f69c8285cd3ac63d08c80f49054a88d99cdd
Author: Sean Enck <sean@ttypty.com>
Date:   Sun, 28 Sep 2025 19:46:20 -0400

this is better handling of included files

Diffstat:
Minternal/config/toml.go | 116+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Minternal/config/toml_test.go | 51++++++++++++++++++++++++++++++++++++++++++---------
Minternal/platform/os_test.go | 23+++++++++++++++++++----
3 files changed, 136 insertions(+), 54 deletions(-)

diff --git a/internal/config/toml.go b/internal/config/toml.go @@ -15,20 +15,24 @@ import ( ) const ( - isStrict = "strict" - isInclude = "include" - maxDepth = 10 - tomlInt = "integer" - tomlBool = "boolean" - tomlString = "string" - tomlArray = "[]string" - strictDefault = true + isInclude = "include" + maxDepth = 10 + tomlInt = "integer" + tomlBool = "boolean" + tomlString = "string" + tomlArray = "[]string" + fileKey = "file" + requiredKey = "required" ) type ( tomlType string // Loader indicates how included files should be sourced - Loader func(string) (io.Reader, error) + Loader func(string) (io.Reader, error) + stringWrapper struct { + value string + required bool + } ) // DefaultTOML will load the internal, default TOML with additional comment markups @@ -77,15 +81,15 @@ func DefaultTOML() (string, error) { # depth allowed up to %d include levels # # it is ONLY used during TOML configuration loading -%s = [] - -# strict, when enabled, requires the configuration entries -# to adhere to all loading rules. # -# it is currently only used to ignore included files that -# are not found -%s = %t -`, maxDepth, isInclude, isStrict, strictDefault), "\n"} { +# can be an array of strings: ['file.toml'] +# or an array of objects: [{%s = 'file.toml', %s = false}] +# +# this is useful to control optional includes (includes +# are required by default, set to false to allow ignoring +# files) +%s = [] +`, maxDepth, fileKey, requiredKey, isInclude), "\n"} { if _, err := builder.WriteString(header); err != nil { return "", err } @@ -160,11 +164,15 @@ func Load(r io.Reader, loader Loader) error { md := env.display() switch md.tomlType { case tomlArray: - array, err := parseStringArray(v, md.canExpand) + array, err := parseArray(v, md.canExpand, false) if err != nil { return err } - store.SetArray(export, array) + var s []string + for _, item := range array { + s = append(s, item.value) + } + store.SetArray(export, s) case tomlInt: i, ok := v.(int64) if !ok { @@ -220,27 +228,18 @@ func readConfigs(r io.Reader, depth int, loader Loader) ([]map[string]any, error return nil, err } maps := []map[string]any{m} - strict := strictDefault - if v, ok := m[isStrict]; ok { - delete(m, isStrict) - b, err := parseBool(v) - if err != nil { - return nil, err - } - strict = b - } includes, ok := m[isInclude] if ok { delete(m, isInclude) - including, err := parseStringArray(includes, true) + including, err := parseArray(includes, true, true) if err != nil { return nil, err } if len(including) > 0 { - for _, s := range including { - files := []string{s} - if strings.Contains(s, "*") { - matched, err := filepath.Glob(s) + for _, item := range including { + files := []string{item.value} + if strings.Contains(item.value, "*") { + matched, err := filepath.Glob(item.value) if err != nil { return nil, err } @@ -252,7 +251,7 @@ func readConfigs(r io.Reader, depth int, loader Loader) ([]map[string]any, error return nil, err } if reader == nil { - if strict { + if item.required { return nil, fmt.Errorf("failed to load the included file: %s", file) } continue @@ -269,21 +268,56 @@ func readConfigs(r io.Reader, depth int, loader Loader) ([]map[string]any, error return maps, nil } -func parseStringArray(value any, expand bool) ([]string, error) { - var res []string +func parseArray(value any, expand, allowMap bool) ([]stringWrapper, error) { + expander := func(s string) string { + return s + } + if expand { + expander = func(s string) string { + return os.Expand(s, os.Getenv) + } + } + var res []stringWrapper switch t := value.(type) { case []any: for _, item := range t { + err := fmt.Errorf("value is not valid array value: %v", item) + val := "" + required := true switch s := item.(type) { case string: - val := s - if expand { - val = os.Expand(s, os.Getenv) + val = s + case map[string]any: + if !allowMap { + return nil, err + } + length := len(s) + if length > 2 { + return nil, fmt.Errorf("invalid map array, too many keys: %v", item) + } + file, ok := s[fileKey] + if !ok { + return nil, fmt.Errorf("'%s' is required, missing: %v", fileKey, item) + } + val, ok = file.(string) + if !ok { + return nil, newTypeError("string", file) + } + if length == 2 { + req, ok := s[requiredKey] + if !ok { + return nil, fmt.Errorf("only '%s' key is allowed here: %v", requiredKey, item) + } + b, err := parseBool(req) + if err != nil { + return nil, err + } + required = b } - res = append(res, val) default: - return nil, fmt.Errorf("value is not string in array: %v", item) + return nil, err } + res = append(res, stringWrapper{value: expander(val), required: required}) } default: return nil, fmt.Errorf("value is not of array type: %v", value) diff --git a/internal/config/toml_test.go b/internal/config/toml_test.go @@ -61,7 +61,7 @@ func TestLoadIncludes(t *testing.T) { } else { return nil, errors.New("invalid path") } - }); err == nil || err.Error() != "value is not string in array: 1" { + }); err == nil || err.Error() != "value is not valid array value: 1" { t.Errorf("invalid error: %v", err) } data = `include = ["$TEST/abc"] @@ -95,7 +95,7 @@ func TestArrayLoad(t *testing.T) { copy = ["'xyz/$TEST'", "s", 1] ` r := strings.NewReader(data) - if err := config.Load(r, emptyRead); err == nil || err.Error() != "value is not string in array: 1" { + if err := config.Load(r, emptyRead); err == nil || err.Error() != "value is not valid array value: 1" { t.Errorf("invalid error: %v", err) } data = `include = [] @@ -267,27 +267,25 @@ otp_format = "$TEST" } } -func TestLoadIncludesStrictControls(t *testing.T) { +func TestLoadIncludesControls(t *testing.T) { store.Clear() defer os.Clearenv() t.Setenv("TEST", "xyz") data := `include = ["$TEST/abc"] store="xyz" -strict = true ` r := strings.NewReader(data) if err := config.Load(r, func(p string) (io.Reader, error) { if p == "xyz/abc" { - return strings.NewReader("include = ['123']\nstrict = 1\nstore = 'abc'"), nil + return strings.NewReader("include = [{file = '123', required = 1}]\nstore = 'abc'"), nil } else { return nil, errors.New("invalid path") } }); err == nil || err.Error() != "non-bool found where bool expected: 1" { t.Errorf("invalid error: %v", err) } - data = `include = ["$TEST/abc"] + data = `include = [{file = "$TEST/abc", required = true}] store="xyz" -strict = true ` r = strings.NewReader(data) if err := config.Load(r, func(_ string) (io.Reader, error) { @@ -295,9 +293,8 @@ strict = true }); err == nil || err.Error() != "failed to load the included file: xyz/abc" { t.Errorf("invalid error: %v", err) } - data = `include = ["$TEST/abc"] + data = `include = [{file = "$TEST/abc", required = false}] store="xyz" -strict = false ` r = strings.NewReader(data) if err := config.Load(r, func(_ string) (io.Reader, error) { @@ -305,4 +302,40 @@ strict = false }); err != nil { t.Errorf("invalid error: %v", err) } + data = `include = [{file = "$TEST/abc", required = false, other = 1}] +store="xyz" +` + r = strings.NewReader(data) + if err := config.Load(r, func(_ string) (io.Reader, error) { + return nil, nil + }); err == nil || !strings.Contains(err.Error(), "invalid map array, too many keys:") { + t.Errorf("invalid error: %v", err) + } + data = `include = [{fsle = "$TEST/abc"}] +store="xyz" +` + r = strings.NewReader(data) + if err := config.Load(r, func(_ string) (io.Reader, error) { + return nil, nil + }); err == nil || !strings.Contains(err.Error(), "'file' is required, missing:") { + t.Errorf("invalid error: %v", err) + } + data = `include = [{file = "$TEST/abc", require = 1}] +store="xyz" +` + r = strings.NewReader(data) + if err := config.Load(r, func(_ string) (io.Reader, error) { + return nil, nil + }); err == nil || !strings.Contains(err.Error(), "only 'required' key is allowed here:") { + t.Errorf("invalid error: %v", err) + } + data = `include = [{file = "$TEST/abc", required = 1}] +store="xyz" +` + r = strings.NewReader(data) + if err := config.Load(r, func(_ string) (io.Reader, error) { + return nil, nil + }); err == nil || err.Error() != "non-bool found where bool expected: 1" { + t.Errorf("invalid error: %v", err) + } } diff --git a/internal/platform/os_test.go b/internal/platform/os_test.go @@ -41,7 +41,7 @@ func TestLoadConfigFile(t *testing.T) { } } -func TestLoadConfigFileNoFileStrict(t *testing.T) { +func TestLoadConfigFileNoFile(t *testing.T) { store.Clear() os.Mkdir("testdata", 0o755) defer os.RemoveAll("testdata") @@ -57,7 +57,7 @@ func TestLoadConfigFileNoFileStrict(t *testing.T) { } } -func TestLoadConfigFileNoFileNoStrict(t *testing.T) { +func TestLoadConfigFileNoFileNotRequired(t *testing.T) { store.Clear() os.Mkdir("testdata", 0o755) defer os.RemoveAll("testdata") @@ -66,10 +66,25 @@ func TestLoadConfigFileNoFileNoStrict(t *testing.T) { if err != nil { t.Errorf("invalid error: %v", err) } - loaded = strings.Replace(loaded, "include = []", "include = ['invalid.toml']", 1) - loaded = strings.Replace(loaded, "strict = true", "strict = false", 1) + loaded = strings.Replace(loaded, "include = []", "include = [{file = 'invalid.toml', required = false}]", 1) os.WriteFile(file, []byte(loaded), 0o644) if err := platform.LoadConfigFile(file); err != nil { t.Errorf("invalid error: %v", err) } } + +func TestLoadConfigFileNoFileRequired(t *testing.T) { + store.Clear() + os.Mkdir("testdata", 0o755) + defer os.RemoveAll("testdata") + file := filepath.Join("testdata", "config.toml") + loaded, err := config.DefaultTOML() + if err != nil { + t.Errorf("invalid error: %v", err) + } + loaded = strings.Replace(loaded, "include = []", "include = [{file = 'invalid.toml'}]", 1) + os.WriteFile(file, []byte(loaded), 0o644) + if err := platform.LoadConfigFile(file); err == nil || err.Error() != "failed to load the included file: invalid.toml" { + t.Errorf("invalid error: %v", err) + } +}