lockbox

password manager
Log | Files | Refs | README | LICENSE

commit ae288011354112aa746043c79e5c6c70154ef1e0
parent dd4ed89d2b2b4ed65a92b55d87e07d6c44dca1a1
Author: Sean Enck <sean@ttypty.com>
Date:   Sun, 28 Sep 2025 22:23:24 -0400

move config handling to proper component areas

Diffstat:
Mcmd/lb/main.go | 10+++++++---
Minternal/app/core.go | 18++++++++++++++++++
Minternal/config/toml.go | 16++++++++++------
Minternal/config/toml_test.go | 126+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Minternal/platform/os.go | 24------------------------
Minternal/platform/os_test.go | 69---------------------------------------------------------------------
6 files changed, 104 insertions(+), 159 deletions(-)

diff --git a/cmd/lb/main.go b/cmd/lb/main.go @@ -10,7 +10,6 @@ import ( "github.com/enckse/lockbox/internal/app" "github.com/enckse/lockbox/internal/app/commands" "github.com/enckse/lockbox/internal/config" - "github.com/enckse/lockbox/internal/platform" ) var version string @@ -48,9 +47,14 @@ func handleEarly(command string, args []string) (bool, error) { } func run() error { + cfg := app.ConfigLoader{} for _, p := range config.NewConfigFiles() { - if platform.PathExists(p) { - if err := platform.LoadConfigFile(p); err != nil { + if cfg.Check(p) { + r, err := cfg.Read(p) + if err != nil { + return err + } + if err := config.Load(r, cfg); err != nil { return err } break diff --git a/internal/app/core.go b/internal/app/core.go @@ -2,6 +2,7 @@ package app import ( + "bytes" "fmt" "io" "os" @@ -31,6 +32,9 @@ type ( tx *kdbx.Transaction args []string } + + // ConfigLoader is the default application loader to assist with config handling + ConfigLoader struct{} ) // NewDefaultCommand creates a new app command @@ -81,3 +85,17 @@ func (a *DefaultCommand) IsPipe() bool { func (a *DefaultCommand) Input(interactive, isPassword bool, prompt string) ([]byte, error) { return platform.GetUserInput(interactive, isPassword, prompt) } + +// Read will read a configuration file +func (c ConfigLoader) Read(file string) (io.Reader, error) { + data, err := os.ReadFile(file) + if err != nil { + return nil, err + } + return bytes.NewReader(data), nil +} + +// Check will check if a configuration file is valid for reading +func (c ConfigLoader) Check(file string) bool { + return platform.PathExists(file) +} diff --git a/internal/config/toml.go b/internal/config/toml.go @@ -28,7 +28,10 @@ const ( type ( tomlType string // Loader indicates how included files should be sourced - Loader func(string) (io.Reader, error) + Loader interface { + Read(string) (io.Reader, error) + Check(string) bool + } included struct { value string required bool @@ -246,16 +249,17 @@ func readConfigs(r io.Reader, depth int, loader Loader) ([]map[string]any, error files = matched } for _, file := range files { - reader, err := loader(file) - if err != nil { - return nil, err - } - if reader == nil { + ok := loader.Check(file) + if !ok { if item.required { return nil, fmt.Errorf("failed to load the included file: %s", file) } continue } + reader, err := loader.Read(file) + if err != nil { + return nil, err + } results, err := readConfigs(reader, depth+1, loader) if err != nil { return nil, err diff --git a/internal/config/toml_test.go b/internal/config/toml_test.go @@ -12,8 +12,23 @@ import ( "github.com/enckse/lockbox/internal/config/store" ) -var emptyRead = func(_ string) (io.Reader, error) { - return nil, nil +type mockReader struct { + read func(string) (io.Reader, error) + check func(string) bool +} + +func (m mockReader) Read(path string) (io.Reader, error) { + if m.read == nil { + return nil, nil + } + return m.read(path) +} + +func (m mockReader) Check(path string) bool { + if m.check == nil { + return true + } + return m.check(path) } func TestLoadIncludes(t *testing.T) { @@ -22,59 +37,60 @@ func TestLoadIncludes(t *testing.T) { t.Setenv("TEST", "xyz") data := `include = ["$TEST/abc"]` r := strings.NewReader(data) - if err := config.Load(r, func(p string) (io.Reader, error) { + mock := mockReader{} + mock.read = func(p string) (io.Reader, error) { if p == "xyz/abc" { return strings.NewReader("include = [\"$TEST/abc\"]"), nil - } else { - return nil, errors.New("invalid path") } - }); err == nil || err.Error() != "too many nested includes (11 > 10)" { + return nil, errors.New("invalid path") + } + if err := config.Load(r, mock); err == nil || err.Error() != "too many nested includes (11 > 10)" { t.Errorf("invalid error: %v", err) } data = `include = ["abc"]` r = strings.NewReader(data) - if err := config.Load(r, func(p string) (io.Reader, error) { + mock.read = func(p string) (io.Reader, error) { if p == "xyz/abc" { return strings.NewReader("include = [\"aaa\"]"), nil - } else { - return nil, errors.New("invalid path") } - }); err == nil || err.Error() != "invalid path" { + return nil, errors.New("invalid path") + } + if err := config.Load(r, mock); err == nil || err.Error() != "invalid path" { t.Errorf("invalid error: %v", err) } data = `include = 1` r = strings.NewReader(data) - if err := config.Load(r, func(p string) (io.Reader, error) { + mock.read = func(p string) (io.Reader, error) { if p == "xyz/abc" { return strings.NewReader("include = [\"aaa\"]"), nil - } else { - return nil, errors.New("invalid path") } - }); err == nil || err.Error() != "value is not of array type: 1" { + return nil, errors.New("invalid path") + } + if err := config.Load(r, mock); err == nil || err.Error() != "value is not of array type: 1" { t.Errorf("invalid error: %v", err) } data = `include = [1]` r = strings.NewReader(data) - if err := config.Load(r, func(p string) (io.Reader, error) { + mock.read = func(p string) (io.Reader, error) { if p == "xyz/abc" { return strings.NewReader("include = [\"aaa\"]"), nil - } else { - return nil, errors.New("invalid path") } - }); err == nil || err.Error() != "value is not valid array value: 1" { + return nil, errors.New("invalid path") + } + if err := config.Load(r, mock); err == nil || err.Error() != "value is not valid array value: 1" { t.Errorf("invalid error: %v", err) } data = `include = ["$TEST/abc"] store="xyz" ` r = strings.NewReader(data) - if err := config.Load(r, func(p string) (io.Reader, error) { + mock.read = func(p string) (io.Reader, error) { if p == "xyz/abc" { return strings.NewReader("store = 'abc'"), nil - } else { - return nil, errors.New("invalid path") } - }); err != nil { + return nil, errors.New("invalid path") + } + if err := config.Load(r, mock); err != nil { t.Errorf("invalid error: %v", err) } if len(store.List()) != 1 { @@ -95,7 +111,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 valid array value: 1" { + if err := config.Load(r, mockReader{}); err == nil || err.Error() != "value is not valid array value: 1" { t.Errorf("invalid error: %v", err) } data = `include = [] @@ -104,7 +120,7 @@ store="xyz" copy = ["'xyz/$TEST'", "s"] ` r = strings.NewReader(data) - if err := config.Load(r, emptyRead); err != nil { + if err := config.Load(r, mockReader{}); err != nil { t.Errorf("invalid error: %v", err) } if len(store.List()) != 2 { @@ -124,7 +140,11 @@ store="xyz" copy = [{file = "'cliptest/$TEST'"}, "s"] ` r = strings.NewReader(data) - if err := config.Load(r, emptyRead); err == nil || !strings.Contains(err.Error(), "value is not valid array value:") || !strings.Contains(err.Error(), "cliptest/") { + mock := mockReader{} + mock.check = func(string) bool { + return false + } + if err := config.Load(r, mock); err == nil || !strings.Contains(err.Error(), "value is not valid array value:") || !strings.Contains(err.Error(), "cliptest/") { t.Errorf("invalid error: %v", err) } if len(store.List()) != 2 { @@ -136,7 +156,7 @@ store="xyz" copy = ["'xyz/$TEST'", "s"] ` r = strings.NewReader(data) - if err := config.Load(r, emptyRead); err != nil { + if err := config.Load(r, mockReader{}); err != nil { t.Errorf("invalid error: %v", err) } if len(store.List()) != 2 { @@ -159,7 +179,7 @@ func TestReadInt(t *testing.T) { hash_length = true ` r := strings.NewReader(data) - if err := config.Load(r, emptyRead); err == nil || err.Error() != "non-int64 found where int64 expected: true" { + if err := config.Load(r, mockReader{}); err == nil || err.Error() != "non-int64 found where int64 expected: true" { t.Errorf("invalid error: %v", err) } data = `include = [] @@ -167,7 +187,7 @@ hash_length = true hash_length = 1 ` r = strings.NewReader(data) - if err := config.Load(r, emptyRead); err != nil { + if err := config.Load(r, mockReader{}); err != nil { t.Errorf("invalid error: %v", err) } if len(store.List()) != 1 { @@ -182,7 +202,7 @@ hash_length = 1 hash_length = -1 ` r = strings.NewReader(data) - if err := config.Load(r, emptyRead); err == nil || err.Error() != "-1 is negative (not allowed here)" { + if err := config.Load(r, mockReader{}); err == nil || err.Error() != "-1 is negative (not allowed here)" { t.Errorf("invalid error: %v", err) } } @@ -194,7 +214,7 @@ func TestReadBool(t *testing.T) { clip = 1 ` r := strings.NewReader(data) - if err := config.Load(r, emptyRead); err == nil || err.Error() != "non-bool found where bool expected: 1" { + if err := config.Load(r, mockReader{}); err == nil || err.Error() != "non-bool found where bool expected: 1" { t.Errorf("invalid error: %v", err) } data = `include = [] @@ -202,7 +222,7 @@ clip = 1 clip = true ` r = strings.NewReader(data) - if err := config.Load(r, emptyRead); err != nil { + if err := config.Load(r, mockReader{}); err != nil { t.Errorf("invalid error: %v", err) } if len(store.List()) != 1 { @@ -217,7 +237,7 @@ clip = true clip = false ` r = strings.NewReader(data) - if err := config.Load(r, emptyRead); err != nil { + if err := config.Load(r, mockReader{}); err != nil { t.Errorf("invalid error: %v", err) } if len(store.List()) != 1 { @@ -236,7 +256,7 @@ func TestBadValues(t *testing.T) { enabled = "false" ` r := strings.NewReader(data) - if err := config.Load(r, emptyRead); err == nil || err.Error() != "unknown key: totsp_enabled (LOCKBOX_TOTSP_ENABLED)" { + if err := config.Load(r, mockReader{}); err == nil || err.Error() != "unknown key: totsp_enabled (LOCKBOX_TOTSP_ENABLED)" { t.Errorf("invalid error: %v", err) } data = `include = [] @@ -244,7 +264,7 @@ enabled = "false" otp_format = -1 ` r = strings.NewReader(data) - if err := config.Load(r, emptyRead); err == nil || err.Error() != "non-string found where string expected: -1" { + if err := config.Load(r, mockReader{}); err == nil || err.Error() != "non-string found where string expected: -1" { t.Errorf("invalid error: %v", err) } } @@ -259,7 +279,7 @@ clip.copy = ["$TEST", "$TEST"] otp_format = "$TEST" ` r := strings.NewReader(data) - if err := config.Load(r, emptyRead); err != nil { + if err := config.Load(r, mockReader{}); err != nil { t.Errorf("invalid error: %v", err) } if len(store.List()) != 3 { @@ -287,67 +307,59 @@ func TestLoadIncludesControls(t *testing.T) { store="xyz" ` r := strings.NewReader(data) - if err := config.Load(r, func(p string) (io.Reader, error) { + mock := mockReader{} + mock.read = func(p string) (io.Reader, error) { if p == "xyz/abc" { 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" { + return nil, errors.New("invalid path") + } + if err := config.Load(r, mock); err == nil || err.Error() != "non-bool found where bool expected: 1" { t.Errorf("invalid error: %v", err) } data = `include = [{file = "$TEST/abc", required = true}] store="xyz" ` r = strings.NewReader(data) - if err := config.Load(r, func(_ string) (io.Reader, error) { - return nil, nil - }); err == nil || err.Error() != "failed to load the included file: xyz/abc" { + mock.check = func(string) bool { + return false + } + if err := config.Load(r, mock); err == nil || err.Error() != "failed to load the included file: xyz/abc" { t.Errorf("invalid error: %v", err) } data = `include = [{file = "$TEST/abc", required = false}] store="xyz" ` r = strings.NewReader(data) - if err := config.Load(r, func(_ string) (io.Reader, error) { - return nil, nil - }); err != nil { + if err := config.Load(r, mock); 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:") { + if err := config.Load(r, mockReader{}); 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:") { + if err := config.Load(r, mockReader{}); 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:") { + if err := config.Load(r, mockReader{}); 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" { + if err := config.Load(r, mockReader{}); err == nil || err.Error() != "non-bool found where bool expected: 1" { t.Errorf("invalid error: %v", err) } } diff --git a/internal/platform/os.go b/internal/platform/os.go @@ -6,12 +6,9 @@ import ( "bytes" "errors" "fmt" - "io" "os" "strings" "syscall" - - "github.com/enckse/lockbox/internal/config" ) func termEcho(on bool) { @@ -136,24 +133,3 @@ func PathExists(file string) bool { } return true } - -func configLoader(path string) (io.Reader, error) { - if !PathExists(path) { - return nil, nil - } - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - return bytes.NewReader(data), nil -} - -// LoadConfigFile will load a path as the configuration -// it will also set the environment -func LoadConfigFile(path string) error { - reader, err := configLoader(path) - if err != nil { - return err - } - return config.Load(reader, configLoader) -} diff --git a/internal/platform/os_test.go b/internal/platform/os_test.go @@ -3,11 +3,8 @@ package platform_test import ( "os" "path/filepath" - "strings" "testing" - "github.com/enckse/lockbox/internal/config" - "github.com/enckse/lockbox/internal/config/store" "github.com/enckse/lockbox/internal/platform" ) @@ -22,69 +19,3 @@ func TestPathExist(t *testing.T) { t.Error("test dir SHOULD exist") } } - -func TestLoadConfigFile(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) - } - os.WriteFile(file, []byte(loaded), 0o644) - if err := platform.LoadConfigFile(file); err != nil { - t.Errorf("invalid error: %v", err) - } - if len(store.List()) != 16 { - t.Errorf("invalid environment after load: %d", len(store.List())) - } -} - -func TestLoadConfigFileNoFile(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 = ['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) - } -} - -func TestLoadConfigFileNoFileNotRequired(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', 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) - } -}