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:
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)
- }
-}