commit e787eca5c3ca20f001027a426044ce86f538233b
parent 7815c8a5625906a35d9d463bbdd9a05bd781c702
Author: Sean Enck <sean@ttypty.com>
Date: Fri, 6 Dec 2024 18:46:04 -0500
from .env to .toml file
Diffstat:
15 files changed, 620 insertions(+), 356 deletions(-)
diff --git a/cmd/main.go b/cmd/main.go
@@ -41,12 +41,13 @@ func handleEarly(command string, args []string) (bool, error) {
}
func run() error {
- paths, err := config.NewEnvFiles()
- if err != nil {
- return err
- }
- if err := platform.LoadEnvConfigs(paths...); err != nil {
- return err
+ for _, p := range config.NewConfigFiles() {
+ if platform.PathExists(p) {
+ if err := config.LoadConfigFile(p); err != nil {
+ return err
+ }
+ break
+ }
}
args := os.Args
if len(args) < 2 {
diff --git a/go.mod b/go.mod
@@ -3,6 +3,7 @@ module github.com/seanenck/lockbox
go 1.23.0
require (
+ github.com/BurntSushi/toml v1.4.0
github.com/aymanbagabas/go-osc52 v1.2.2
github.com/pquerna/otp v1.4.0
github.com/tobischo/gokeepasslib/v3 v3.6.0
diff --git a/go.sum b/go.sum
@@ -1,3 +1,5 @@
+github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
+github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/aymanbagabas/go-osc52 v1.2.2 h1:NT7wkhEhPTcKnBCdPi9djmyy9L3JOL4+3SsfJyqptCo=
github.com/aymanbagabas/go-osc52 v1.2.2/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
diff --git a/internal/app/core_test.go b/internal/app/core_test.go
@@ -13,7 +13,7 @@ func TestUsage(t *testing.T) {
t.Errorf("invalid usage, out of date? %d", len(u))
}
u, _ = app.Usage(true, "lb")
- if len(u) != 126 {
+ if len(u) != 124 {
t.Errorf("invalid verbose usage, out of date? %d", len(u))
}
for _, usage := range u {
diff --git a/internal/config/config.toml b/internal/config/config.toml
@@ -0,0 +1,46 @@
+language = ""
+platform = ""
+readonly = false
+store = ""
+include = []
+
+[color]
+enabled = true
+
+[clip]
+copy = []
+paste = []
+max = 0
+osc52 = false
+enabled = true
+
+[hook]
+enabled = true
+directory = ""
+
+[json]
+mode = ""
+hash_length = 0
+
+[keys]
+file = ""
+mode = ""
+key = ""
+
+[pwgen]
+enabled = true
+chars = ""
+count = 0
+template = ""
+title = true
+wordlist = []
+
+[totp]
+enabled = true
+attribute = ""
+between = []
+format = ""
+max = 0
+
+[entry]
+modtime = ""
diff --git a/internal/config/core.go b/internal/config/core.go
@@ -24,7 +24,7 @@ const (
no = "no"
detectEnvironment = "detect"
noEnvironment = "none"
- envFile = "lockbox.env"
+ tomlFile = "lockbox.toml"
unknownPlatform = ""
// sub categories
clipCategory keyCategory = "CLIP_"
@@ -33,15 +33,16 @@ const (
// YesValue are yes (on) values
YesValue = yes
// TemplateVariable is used to handle '$' in shell vars (due to expansion)
- TemplateVariable = "[%]"
- configDirName = "lockbox"
- configDir = ".config"
+ TemplateVariable = "[%]"
+ configDirName = "lockbox"
+ configDir = ".config"
+ environmentPrefix = "LOCKBOX_"
)
var (
- configDirOffsetFile = filepath.Join(configDirName, envFile)
- xdgPaths = []string{configDirOffsetFile, envFile}
- homePaths = []string{filepath.Join(configDir, configDirOffsetFile), filepath.Join(configDir, envFile)}
+ configDirOffsetFile = filepath.Join(configDirName, tomlFile)
+ xdgPaths = []string{configDirOffsetFile, tomlFile}
+ homePaths = []string{filepath.Join(configDir, configDirOffsetFile), filepath.Join(configDir, tomlFile)}
exampleColorWindows = []string{strings.Join([]string{exampleColorWindow, exampleColorWindow, exampleColorWindow + "..."}, colorWindowDelimiter)}
registeredEnv = []printer{}
)
@@ -134,7 +135,7 @@ func environOrDefault(envKey, defaultValue string) string {
}
func (e environmentBase) Key() string {
- return fmt.Sprintf("LOCKBOX_%s%s", string(e.cat), e.subKey)
+ return fmt.Sprintf(environmentPrefix+"%s%s", string(e.cat), e.subKey)
}
// Get will get the boolean value for the setting
@@ -319,16 +320,16 @@ func ParseColorWindow(windowString string) ([]ColorWindow, error) {
// NewEnvFiles will get the list of candidate environment files
// it will also set the environment to empty for the caller
-func NewEnvFiles() ([]string, error) {
+func NewConfigFiles() []string {
v := EnvConfig.Get()
if v == "" || v == noEnvironment {
- return []string{}, nil
+ return []string{}
}
if err := EnvConfig.Set(noEnvironment); err != nil {
- return nil, err
+ return nil
}
if v != detectEnvironment {
- return []string{v}, nil
+ return []string{v}
}
var options []string
pathAdder := func(root string, err error, subs []string) {
@@ -341,10 +342,7 @@ func NewEnvFiles() ([]string, error) {
pathAdder(os.Getenv("XDG_CONFIG_HOME"), nil, xdgPaths)
h, err := os.UserHomeDir()
pathAdder(h, err, homePaths)
- if len(options) == 0 {
- return nil, errors.New("unable to initialize default config locations")
- }
- return options, nil
+ return options
}
// IsUnset will indicate if a variable is an unset (and unset it) or return that it isn't
@@ -375,84 +373,6 @@ func Environ() []string {
return results
}
-func parseConfigKeyEarly[T any](env interface {
- Key() string
- Get() (T, error)
-}, inputs map[string]string, conv func(string) (T, error),
-) (T, error) {
- raw, ok := inputs[env.Key()]
- if ok {
- return conv(raw)
- }
- return env.Get()
-}
-
-// ExpandParsed handles cycles of parsing configuration env inputs to resolve ALL variables
-func ExpandParsed(inputs map[string]string) (map[string]string, error) {
- if inputs == nil {
- return nil, errors.New("invalid input variables")
- }
- if len(inputs) == 0 {
- return inputs, nil
- }
- cycles, err := parseConfigKeyEarly(envConfigExpands, inputs, strconv.Atoi)
- if err != nil {
- return nil, err
- }
- quoted, err := parseConfigKeyEarly(envConfigQuoted, inputs, func(v string) (bool, error) {
- return parseStringYesNo(envConfigQuoted, v)
- })
- if err != nil {
- return nil, err
- }
- if cycles == 0 {
- return inputs, nil
- }
- result := inputs
- for cycles > 0 {
- expanded := expandParsed(result, quoted)
- if len(expanded) == len(result) {
- same := true
- for k, v := range expanded {
- val, ok := result[k]
- if !ok {
- same = false
- break
- }
- if val != v {
- same = false
- break
- }
- }
- if same {
- return expanded, nil
- }
- }
- result = expanded
- cycles--
- }
- return nil, errors.New("reached maximum expand cycle count")
-}
-
-func expandParsed(inputs map[string]string, quoted bool) map[string]string {
- result := make(map[string]string)
- for k, v := range inputs {
- val := v
- if quoted {
- if strings.HasPrefix(val, "\"") && strings.HasSuffix(val, "\"") {
- val = strings.TrimPrefix(strings.TrimSuffix(val, "\""), "\"")
- }
- }
- result[k] = os.Expand(val, func(in string) string {
- if i, ok := inputs[in]; ok {
- return i
- }
- return os.Getenv(in)
- })
- }
- return result
-}
-
// Wrap performs simple block text word wrapping
func Wrap(indent uint, in string) string {
var sections []string
diff --git a/internal/config/core_test.go b/internal/config/core_test.go
@@ -107,39 +107,33 @@ func TestParseWindows(t *testing.T) {
func TestNewEnvFiles(t *testing.T) {
os.Clearenv()
- t.Setenv("LOCKBOX_ENV", "none")
- f, err := config.NewEnvFiles()
- if len(f) != 0 || err != nil {
- t.Errorf("invalid files: %v %v", f, err)
+ t.Setenv("LOCKBOX_CONFIG_TOML", "none")
+ f := config.NewConfigFiles()
+ if len(f) != 0 {
+ t.Errorf("invalid files: %v", f)
}
- t.Setenv("LOCKBOX_ENV", "test")
- f, err = config.NewEnvFiles()
- if len(f) != 1 || f[0] != "test" || err != nil {
- t.Errorf("invalid files: %v %v", f, err)
+ t.Setenv("LOCKBOX_CONFIG_TOML", "test")
+ f = config.NewConfigFiles()
+ if len(f) != 1 || f[0] != "test" {
+ t.Errorf("invalid files: %v", f)
}
t.Setenv("HOME", "test")
- t.Setenv("LOCKBOX_ENV", "detect")
- f, err = config.NewEnvFiles()
- if len(f) != 2 || err != nil {
- t.Errorf("invalid files: %v %v", f, err)
+ t.Setenv("LOCKBOX_CONFIG_TOML", "detect")
+ f = config.NewConfigFiles()
+ if len(f) != 2 {
+ t.Errorf("invalid files: %v", f)
}
- t.Setenv("LOCKBOX_ENV", "detect")
+ t.Setenv("LOCKBOX_CONFIG_TOML", "detect")
t.Setenv("XDG_CONFIG_HOME", "test")
- f, err = config.NewEnvFiles()
- if len(f) != 4 || err != nil {
- t.Errorf("invalid files: %v %v", f, err)
+ f = config.NewConfigFiles()
+ if len(f) != 4 {
+ t.Errorf("invalid files: %v", f)
}
- t.Setenv("LOCKBOX_ENV", "detect")
+ t.Setenv("LOCKBOX_CONFIG_TOML", "detect")
os.Unsetenv("HOME")
- f, err = config.NewEnvFiles()
- if len(f) != 2 || err != nil {
- t.Errorf("invalid files: %v %v", f, err)
- }
- t.Setenv("LOCKBOX_ENV", "detect")
- os.Unsetenv("XDG_CONFIG_HOME")
- _, err = config.NewEnvFiles()
- if err == nil || err.Error() != "unable to initialize default config locations" {
- t.Errorf("invalid files: %v", err)
+ f = config.NewConfigFiles()
+ if len(f) != 2 {
+ t.Errorf("invalid files: %v", f)
}
}
@@ -180,103 +174,6 @@ func TestEnviron(t *testing.T) {
}
}
-func TestExpandParsed(t *testing.T) {
- os.Clearenv()
- t.Setenv("TEST_ABC", "1")
- t.Setenv("LOCKBOX_ENV_EXPANDS", "a")
- _, err := config.ExpandParsed(nil)
- if err == nil || err.Error() != "invalid input variables" {
- t.Errorf("invalid error: %v", err)
- }
- r, err := config.ExpandParsed(make(map[string]string))
- if err != nil || len(r) != 0 {
- t.Errorf("invalid expand")
- }
- t.Setenv("LOCKBOX_ENV_EXPANDS", "a")
- ins := make(map[string]string)
- ins["TEST"] = "$TEST_ABC"
- _, err = config.ExpandParsed(ins)
- if err == nil || err.Error() != "strconv.Atoi: parsing \"a\": invalid syntax" {
- t.Errorf("invalid error: %v", err)
- }
- ins["LOCKBOX_ENV_EXPANDS"] = "2"
- r, err = config.ExpandParsed(ins)
- if err != nil || len(r) != 2 || r["TEST"] != "1" {
- t.Errorf("invalid expand: %v", r)
- }
- delete(ins, "LOCKBOX_ENV_EXPANDS")
- t.Setenv("LOCKBOX_ENV_EXPANDS", "2")
- r, err = config.ExpandParsed(ins)
- if err != nil || len(r) != 1 || r["TEST"] != "1" {
- t.Errorf("invalid expand: %v", r)
- }
- t.Setenv("LOCKBOX_ENV_EXPANDS", "2")
- r, err = config.ExpandParsed(ins)
- if err != nil || len(r) != 1 || r["TEST"] != "1" {
- t.Errorf("invalid expand: %v", r)
- }
- t.Setenv("TEST_ABC", "$OTHER_TEST")
- t.Setenv("OTHER_TEST", "$ANOTHER_TEST")
- t.Setenv("ANOTHER_TEST", "2")
- t.Setenv("LOCKBOX_ENV_EXPANDS", "1")
- if _, err = config.ExpandParsed(ins); err == nil || err.Error() != "reached maximum expand cycle count" {
- t.Errorf("invalid error: %v", err)
- }
- t.Setenv("LOCKBOX_ENV_EXPANDS", "2")
- ins["OTHER_FIRST"] = "2"
- ins["OTHER_OTHER"] = "$ANOTHER_TEST|$TEST_ABC|$OTHER_TEST"
- ins["OTHER"] = "$OTHER_OTHER|$OTHER_FIRST"
- t.Setenv("LOCKBOX_ENV_EXPANDS", "20")
- r, err = config.ExpandParsed(ins)
- if err != nil || len(r) != 4 || r["TEST"] != "2" || r["OTHER"] != "2|2|2|2" || r["OTHER_OTHER"] != "2|2|2" {
- t.Errorf("invalid expand: %v", r)
- }
- t.Setenv("LOCKBOX_ENV_EXPANDS", "0")
- delete(ins, "OTHER_FIRST")
- delete(ins, "OTHER")
- delete(ins, "OTHER_OTHER")
- r, err = config.ExpandParsed(ins)
- if err != nil || len(r) != 1 || r["TEST"] != "$TEST_ABC" {
- t.Errorf("invalid expand: %v", r)
- }
- os.Unsetenv("LOCKBOX_ENV_EXPANDS")
- delete(ins, "OTHER_FIRST")
- delete(ins, "OTHER")
- delete(ins, "OTHER_OTHER")
- ins["LOCKBOX_ENV_EXPANDS"] = "0"
- r, err = config.ExpandParsed(ins)
- if err != nil || len(r) != 2 || r["TEST"] != "$TEST_ABC" {
- t.Errorf("invalid expand: %v", r)
- }
- ins["LOCKBOX_ENV_EXPANDS"] = "5"
- ins["TEST"] = "\"abc\""
- t.Setenv("LOCKBOX_ENV_QUOTED", "yes")
- r, err = config.ExpandParsed(ins)
- if err != nil || len(r) != 2 || r["TEST"] != "abc" {
- t.Errorf("invalid expand: %v", r)
- }
- t.Setenv("LOCKBOX_ENV_QUOTED", "no")
- ins["TEST"] = "\"abc\""
- r, err = config.ExpandParsed(ins)
- if err != nil || len(r) != 2 || r["TEST"] != "\"abc\"" {
- t.Errorf("invalid expand: %v", r)
- }
- os.Unsetenv("LOCKBOX_ENV_QUOTED")
- r, err = config.ExpandParsed(ins)
- if err != nil || len(r) != 2 || r["TEST"] != "abc" {
- t.Errorf("invalid expand: %v", r)
- }
- ins["LOCKBOX_ENV_QUOTED"] = "yes"
- r, err = config.ExpandParsed(ins)
- if err != nil || len(r) != 3 || r["TEST"] != "abc" {
- t.Errorf("invalid expand: %v", r)
- }
- ins["LOCKBOX_ENV_QUOTED"] = "1"
- if _, err = config.ExpandParsed(ins); err == nil || err.Error() != "invalid yes/no env value for LOCKBOX_ENV_QUOTED" {
- t.Errorf("invalid error: %v", err)
- }
-}
-
func TestWrap(t *testing.T) {
w := config.Wrap(0, "")
if w != "" {
diff --git a/internal/config/toml.go b/internal/config/toml.go
@@ -0,0 +1,220 @@
+package config
+
+import (
+ "bytes"
+ _ "embed"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "slices"
+ "strings"
+
+ "github.com/BurntSushi/toml"
+)
+
+type (
+ ConfigLoader func(string) (io.Reader, error)
+ ShellEnv struct {
+ Key string
+ Value string
+ }
+)
+
+var (
+ //go:embed "config.toml"
+ ExampleTOML string
+ redirects = map[string]string{
+ "HOOK_DIRECTORY": EnvHookDir.Key(),
+ "HOOK_ENABLED": EnvNoHooks.Key(),
+ "JSON_MODE": EnvJSONDataOutput.Key(),
+ "JSON_HASH_LENGTH": EnvHashLength.Key(),
+ "KEYS_FILE": EnvKeyFile.Key(),
+ "KEYS_MODE": EnvKeyMode.Key(),
+ "KEYS_KEY": envKey.Key(),
+ "CLIP_ENABLED": EnvNoClip.Key(),
+ "COLOR_ENABLED": EnvNoColor.Key(),
+ "PWGEN_ENABLED": EnvNoPasswordGen.Key(),
+ "TOTP_ENABLED": EnvNoTOTP.Key(),
+ "TOTP_ATTRIBUTE": EnvTOTPToken.Key(),
+ "ENTRY_MODTIME": EnvModTime.Key(),
+ }
+ arrayTypes = []string{
+ EnvClipCopy.Key(),
+ EnvClipPaste.Key(),
+ EnvPasswordGenWordList.Key(),
+ envKey.Key(),
+ EnvTOTPColorBetween.Key(),
+ }
+ intTypes = []string{
+ EnvClipMax.Key(),
+ EnvMaxTOTP.Key(),
+ EnvHashLength.Key(),
+ EnvPasswordGenCount.Key(),
+ }
+ boolTypes = []string{
+ EnvClipOSC52.Key(),
+ EnvNoClip.Key(),
+ EnvNoColor.Key(),
+ EnvNoHooks.Key(),
+ EnvNoPasswordGen.Key(),
+ EnvNoTOTP.Key(),
+ EnvPasswordGenTitle.Key(),
+ EnvReadOnly.Key(),
+ }
+)
+
+func LoadConfig(r io.Reader, loader ConfigLoader) ([]ShellEnv, error) {
+ m := make(map[string]interface{})
+ if err := overlayConfig(r, true, &m, loader); err != nil {
+ return nil, err
+ }
+ var allowed []string
+ for _, k := range registeredEnv {
+ allowed = append(allowed, k.self().Key())
+ }
+ m = flatten(m, "")
+ var res []ShellEnv
+ for k, v := range m {
+ export := strings.ToUpper(k)
+ if r, ok := redirects[export]; ok {
+ export = r
+ } else {
+ export = environmentPrefix + export
+ }
+ if !slices.Contains(allowed, export) {
+ return nil, fmt.Errorf("unknown key: %s (%s)", k, export)
+ }
+ value, ok := v.(string)
+ if !ok {
+ if slices.Contains(arrayTypes, export) {
+ array, err := parseStringArray(v)
+ if err != nil {
+ return nil, err
+ }
+ value = strings.Join(array, " ")
+ } else if slices.Contains(intTypes, export) {
+ i, ok := v.(int64)
+ if !ok {
+ return nil, fmt.Errorf("non-int64 found where expected: %v", v)
+ }
+ if i < 0 {
+ return nil, fmt.Errorf("%d is negative (not allowed here)", i)
+ }
+ value = fmt.Sprintf("%d", i)
+ } else if slices.Contains(boolTypes, export) {
+ switch t := v.(type) {
+ case bool:
+ if t {
+ value = yes
+ } else {
+ value = no
+ }
+ default:
+ return nil, fmt.Errorf("non-bool found where expected: %v", v)
+ }
+ } else {
+ return nil, fmt.Errorf("unknown field, can't determine type: %s (%v)", k, v)
+ }
+ }
+ value = os.Expand(value, os.Getenv)
+ res = append(res, ShellEnv{Key: export, Value: value})
+
+ }
+ return res, nil
+}
+
+func overlayConfig(r io.Reader, canInclude bool, m *map[string]interface{}, loader ConfigLoader) error {
+ d := toml.NewDecoder(r)
+ if _, err := d.Decode(m); err != nil {
+ return err
+ }
+ res := *m
+ includes, ok := res["include"]
+ if ok {
+ delete(*m, "include")
+ including, err := parseStringArray(includes)
+ if err != nil {
+ return err
+ }
+ if len(including) > 0 {
+ if !canInclude {
+ return errors.New("nested includes not allowed")
+ }
+ for _, s := range including {
+ read := os.Expand(s, os.Getenv)
+ reader, err := loader(read)
+ if err != nil {
+ return err
+ }
+ if err := overlayConfig(reader, false, m, nil); err != nil {
+ return err
+ }
+ }
+ }
+ }
+ return nil
+}
+
+func parseStringArray(value interface{}) ([]string, error) {
+ var res []string
+ switch t := value.(type) {
+ case []interface{}:
+ for _, item := range t {
+ switch s := item.(type) {
+ case string:
+ res = append(res, s)
+ default:
+ return nil, fmt.Errorf("value is not string in array: %v", item)
+ }
+ }
+ default:
+ return nil, fmt.Errorf("value is not of array type: %v", value)
+ }
+ return res, nil
+}
+
+func flatten(m map[string]interface{}, prefix string) map[string]interface{} {
+ flattened := make(map[string]interface{})
+
+ for k, v := range m {
+ key := k
+ if prefix != "" {
+ key = prefix + "_" + k
+ }
+
+ switch to := v.(type) {
+ case map[string]interface{}:
+ for subKey, subVal := range flatten(to, key) {
+ flattened[subKey] = subVal
+ }
+ default:
+ flattened[key] = v
+ }
+ }
+
+ return flattened
+}
+
+func configLoader(path string) (io.Reader, error) {
+ data, err := os.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ return bytes.NewReader(data), nil
+}
+
+func LoadConfigFile(path string) error {
+ reader, err := configLoader(path)
+ if err != nil {
+ return err
+ }
+ env, err := LoadConfig(reader, configLoader)
+ if err != nil {
+ return err
+ }
+ for _, v := range env {
+ os.Setenv(v.Key, v.Value)
+ }
+ return nil
+}
diff --git a/internal/config/toml_test.go b/internal/config/toml_test.go
@@ -0,0 +1,278 @@
+package config_test
+
+import (
+ "errors"
+ "io"
+ "os"
+ "path/filepath"
+ "slices"
+ "strings"
+ "testing"
+
+ "github.com/seanenck/lockbox/internal/config"
+)
+
+func TestLoadIncludes(t *testing.T) {
+ defer os.Clearenv()
+ t.Setenv("TEST", "xyz")
+ data := `include = ["$TEST/abc"]`
+ r := strings.NewReader(data)
+ if _, err := config.LoadConfig(r, 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() != "nested includes not allowed" {
+ t.Errorf("invalid error: %v", err)
+ }
+ data = `include = ["abc"]`
+ r = strings.NewReader(data)
+ if _, err := config.LoadConfig(r, 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" {
+ t.Errorf("invalid error: %v", err)
+ }
+ data = `include = 1`
+ r = strings.NewReader(data)
+ if _, err := config.LoadConfig(r, 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" {
+ t.Errorf("invalid error: %v", err)
+ }
+ data = `include = [1]`
+ r = strings.NewReader(data)
+ if _, err := config.LoadConfig(r, 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 string in array: 1" {
+ t.Errorf("invalid error: %v", err)
+ }
+ data = `include = ["$TEST/abc"]
+store="xyz"
+`
+ r = strings.NewReader(data)
+ env, err := config.LoadConfig(r, func(p string) (io.Reader, error) {
+ if p == "xyz/abc" {
+ return strings.NewReader("store = 'abc'"), nil
+ } else {
+ return nil, errors.New("invalid path")
+ }
+ })
+ if err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if len(env) != 1 || env[0].Key != "LOCKBOX_STORE" || env[0].Value != "abc" {
+ t.Errorf("invalid object: %v", env)
+ }
+}
+
+func TestArrayLoad(t *testing.T) {
+ defer os.Clearenv()
+ t.Setenv("TEST", "abc")
+ data := `store="xyz"
+[clip]
+copy = ["'xyz/$TEST'", "s", 1]
+`
+ r := strings.NewReader(data)
+ _, err := config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ })
+ if err == nil || err.Error() != "value is not string in array: 1" {
+ t.Errorf("invalid error: %v", err)
+ }
+ data = `include = []
+store="xyz"
+[clip]
+copy = ["'xyz/$TEST'", "s"]
+`
+ r = strings.NewReader(data)
+ env, err := config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ })
+ if err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ slices.SortFunc(env, func(x, y config.ShellEnv) int {
+ return strings.Compare(x.Key, y.Key)
+ })
+ if len(env) != 2 || env[1].Key != "LOCKBOX_STORE" || env[1].Value != "xyz" || env[0].Key != "LOCKBOX_CLIP_COPY" || env[0].Value != "'xyz/abc' s" {
+ t.Errorf("invalid object: %v", env)
+ }
+ data = `include = []
+store="xyz"
+[clip]
+copy = "'xyz/$TEST' s"
+`
+ r = strings.NewReader(data)
+ env, err = config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ })
+ if err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ slices.SortFunc(env, func(x, y config.ShellEnv) int {
+ return strings.Compare(x.Key, y.Key)
+ })
+ if len(env) != 2 || env[1].Key != "LOCKBOX_STORE" || env[1].Value != "xyz" || env[0].Key != "LOCKBOX_CLIP_COPY" || env[0].Value != "'xyz/abc' s" {
+ t.Errorf("invalid object: %v", env)
+ }
+}
+
+func TestRedirect(t *testing.T) {
+ data := `include = []
+[hook]
+directory = "xyz"
+`
+ r := strings.NewReader(data)
+ env, err := config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ })
+ if err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if len(env) != 1 || env[0].Key != "LOCKBOX_HOOKDIR" || env[0].Value != "xyz" {
+ t.Errorf("invalid object: %v", env)
+ }
+}
+
+func TestReadInt(t *testing.T) {
+ data := `
+[clip]
+max = true
+`
+ r := strings.NewReader(data)
+ _, err := config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ })
+ if err == nil || err.Error() != "non-int64 found where expected: true" {
+ t.Errorf("invalid error: %v", err)
+ }
+ data = `include = []
+[clip]
+max = 1
+`
+ r = strings.NewReader(data)
+ env, err := config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ })
+ if err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if len(env) != 1 || env[0].Key != "LOCKBOX_CLIP_MAX" || env[0].Value != "1" {
+ t.Errorf("invalid object: %v", env)
+ }
+ data = `include = []
+[clip]
+max = -1
+`
+ r = strings.NewReader(data)
+ env, err = config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ })
+ if err == nil || err.Error() != "-1 is negative (not allowed here)" {
+ t.Errorf("invalid error: %v", err)
+ }
+}
+
+func TestReadBool(t *testing.T) {
+ defer os.Clearenv()
+ t.Setenv("TEST", "abc")
+ data := `
+[totp]
+enabled = 1
+`
+ r := strings.NewReader(data)
+ _, err := config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ })
+ if err == nil || err.Error() != "non-bool found where expected: 1" {
+ t.Errorf("invalid error: %v", err)
+ }
+ data = `include = []
+[totp]
+enabled = true
+`
+ r = strings.NewReader(data)
+ env, err := config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ })
+ if err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if len(env) != 1 || env[0].Key != "LOCKBOX_NOTOTP" || env[0].Value != "yes" {
+ t.Errorf("invalid object: %v", env)
+ }
+ data = `include = []
+[totp]
+enabled = false
+`
+ r = strings.NewReader(data)
+ env, err = config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ })
+ if err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if len(env) != 1 || env[0].Key != "LOCKBOX_NOTOTP" || env[0].Value != "no" {
+ t.Errorf("invalid object: %v", env)
+ }
+}
+
+func TestBadValues(t *testing.T) {
+ defer os.Clearenv()
+ t.Setenv("TEST", "abc")
+ data := `
+[totsp]
+enabled = "false"
+`
+ r := strings.NewReader(data)
+ _, err := config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ })
+ if err == nil || err.Error() != "unknown key: totsp_enabled (LOCKBOX_TOTSP_ENABLED)" {
+ t.Errorf("invalid error: %v", err)
+ }
+ data = `include = []
+[totp]
+format = -1
+`
+ r = strings.NewReader(data)
+ _, err = config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ })
+ if err == nil || err.Error() != "unknown field, can't determine type: totp_format (-1)" {
+ t.Errorf("invalid error: %v", err)
+ }
+}
+
+func TestLoadFile(t *testing.T) {
+ os.Mkdir("testdata", 0o755)
+ // defer os.RemoveAll("testdata")
+ defer os.Clearenv()
+ file := filepath.Join("testdata", "config.toml")
+ os.WriteFile(file, []byte(config.ExampleTOML), 0o644)
+ if err := config.LoadConfigFile(file); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ count := 0
+ for _, item := range os.Environ() {
+ if strings.HasPrefix(item, "LOCKBOX_") {
+ count++
+ }
+ }
+ if count != 29 {
+ t.Errorf("invalid environment after load: %d", count)
+ }
+}
diff --git a/internal/config/vars.go b/internal/config/vars.go
@@ -271,14 +271,15 @@ and '%s' allows for multiple windows.`, colorWindowSpan, colorWindowDelimiter),
cat: totpCategory,
desc: "Override the otpauth url used to store totp tokens. It must have ONE format string ('%s') to insert the totp base code.",
}, fxn: formatterTOTP, allowed: "otpauth//url/%s/args..."})
- // EnvConfig is the location of the config file to read environment variables from
+ // EnvConfig is the location of the config file to read
EnvConfig = environmentRegister(
EnvironmentString{
environmentDefault: newDefaultedEnvironment(detectEnvironment,
environmentBase{
- subKey: "ENV",
- desc: fmt.Sprintf(`Allows setting a specific file of environment variables for lockbox to read and use as
-configuration values (an '.env' file). The keyword '%s' will disable this functionality and the keyword '%s' will
+ subKey: "CONFIG_TOML",
+ desc: fmt.Sprintf(`Allows setting a specific toml file to read and load into the environment.
+
+The keyword '%s' will disable this functionality and the keyword '%s' will
search for a file in the following paths in XDG_CONFIG_HOME (%s) or from the user's HOME (%s).
Matches the first file found.
@@ -313,26 +314,6 @@ Set to '%s' to ignore the set key value`, noKeyMode, IgnoreKeyMode),
allowed: []string{commandArgsExample, "password"},
canDefault: false,
})
- envConfigQuoted = environmentRegister(
- EnvironmentBool{
- environmentDefault: newDefaultedEnvironment(true,
- environmentBase{
- subKey: EnvConfig.subKey + "_QUOTED",
- desc: "Enables removing prefix/suffix quotes from shell environment config settings\nwhen loaded through configuration file.",
- }),
- })
- envConfigExpands = environmentRegister(
- EnvironmentInt{
- environmentDefault: newDefaultedEnvironment(20,
- environmentBase{
- subKey: EnvConfig.subKey + "_EXPANDS",
- desc: `The maximum number of times to expand the input env to resolve variables (set to 0 to disable expansion).
-
-This value can NOT be an expansion itself.`,
- }),
- shortDesc: "max expands",
- allowZero: true,
- })
// EnvPasswordGenCount is the number of words that will be selected for password generation
EnvPasswordGenCount = environmentRegister(
EnvironmentInt{
diff --git a/internal/config/vars_test.go b/internal/config/vars_test.go
@@ -105,7 +105,7 @@ func TestListVariables(t *testing.T) {
known[trim] = struct{}{}
}
l := len(known)
- if l != 34 {
+ if l != 32 {
t.Errorf("invalid env count, outdated? %d", l)
}
}
@@ -240,7 +240,7 @@ func TestEnvironDefinitions(t *testing.T) {
if !strings.HasPrefix(env, "LOCKBOX_") {
t.Errorf("invalid env var: %s", env)
}
- if env == "LOCKBOX_ENV" {
+ if env == "LOCKBOX_CONFIG_TOML" {
continue
}
t.Setenv(env, "test")
diff --git a/internal/platform/os.go b/internal/platform/os.go
@@ -9,9 +9,6 @@ import (
"os"
"strings"
"syscall"
-
- "github.com/seanenck/lockbox/internal/config"
- "mvdan.cc/sh/v3/expand"
)
func termEcho(on bool) {
@@ -141,39 +138,3 @@ func PathExists(file string) bool {
}
return true
}
-
-// LoadEnvConfigs load environment settings from configs
-func LoadEnvConfigs(paths ...string) error {
- for _, useEnv := range paths {
- if !PathExists(useEnv) {
- continue
- }
- b, err := os.ReadFile(useEnv)
- if err != nil {
- return err
- }
- env := expand.ListEnviron(strings.Split(string(b), "\n")...)
- found := make(map[string]string)
- env.Each(func(name string, vr expand.Variable) bool {
- found[name] = vr.String()
- return true
- })
- result, err := config.ExpandParsed(found)
- if err != nil {
- return err
- }
- for k, v := range result {
- ok, err := config.IsUnset(k, v)
- if err != nil {
- return err
- }
- if !ok {
- if err := os.Setenv(k, v); err != nil {
- return err
- }
- }
- }
- break
- }
- return nil
-}
diff --git a/internal/platform/os_test.go b/internal/platform/os_test.go
@@ -1,10 +1,8 @@
package platform_test
import (
- "fmt"
"os"
"path/filepath"
- "strings"
"testing"
"github.com/seanenck/lockbox/internal/platform"
@@ -21,58 +19,3 @@ func TestPathExist(t *testing.T) {
t.Error("test dir SHOULD exist")
}
}
-
-func TestLoadEnvConfigs(t *testing.T) {
- os.Clearenv()
- files := []string{filepath.Join("testdata", "xyz"), filepath.Join("testdata", "abc")}
- if err := platform.LoadEnvConfigs(files...); err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- cfg := files[1]
- os.WriteFile(cfg, []byte(`
-TEST_X=1
-TEST_Y=2
-TEST_Z="1
-TEST11=1"
-TEST_3="abc $HOME $X $TEST_X"`), 0o644)
- if err := platform.LoadEnvConfigs(files...); err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- env := fmt.Sprintf("%v", os.Environ())
- verify := func(expects []string) {
- for _, e := range expects {
- if !strings.Contains(env, e) {
- t.Errorf("invalid env: %s (missing '%s')", env, e)
- }
- }
- }
- verify([]string{"TEST_X=1", "TEST_Y=2", "TEST_3=abc 1", "TEST_Z=\"1", "TEST11=1\""})
- t.Setenv("HOME", "a123")
- if err := platform.LoadEnvConfigs(files...); err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- env = fmt.Sprintf("%v", os.Environ())
- verify([]string{"TEST_X=1", "TEST_Y=2", "TEST_3=abc a123 1", "TEST_Z=\"1", "TEST11=1\""})
- t.Setenv("XYZ", "xyz")
- t.Setenv("HOME", "$TEST4")
- t.Setenv("TEST4", "$XYZ")
- if err := platform.LoadEnvConfigs(files...); err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- env = fmt.Sprintf("%v", os.Environ())
- verify([]string{"TEST_X=1", "TEST_Y=2", "TEST_3=abc xyz 1", "TEST_Z=\"1", "TEST11=1\""})
- final := filepath.Join("testdata", "zzz")
- files = append(files, final)
- t.Setenv("TEST4", "a")
- os.WriteFile(final, []byte(`
-TEST_X=1
-TEST_Y=2
-TEST_Z="1
-TEST11=2"
-TEST_3="abc $HOME $X $TEST_X"`), 0o644)
- if err := platform.LoadEnvConfigs(files...); err != nil {
- t.Errorf("unexpected error: %v", err)
- }
- env = fmt.Sprintf("%v", os.Environ())
- verify([]string{"TEST_X=1", "TEST_Y=2", "TEST_3=abc a 1", "TEST_Z=\"1", "TEST11=1\""})
-}
diff --git a/justfile b/justfile
@@ -11,10 +11,10 @@ build:
go build {{goflags}} -ldflags "{{ldflags}} -X main.version={{version}}" -o "{{object}}" cmd/main.go
unittest:
- LOCKBOX_ENV=none go test ./...
+ LOCKBOX_CONFIG_TOML=none go test ./...
check: unittest build
- cd tests && LOCKBOX_ENV=none ./run.sh
+ cd tests && LOCKBOX_CONFIG_TOML=none ./run.sh
clean:
rm -f "{{object}}"
diff --git a/tests/run.sh b/tests/run.sh
@@ -1,7 +1,7 @@
#!/bin/sh
LB_BINARY=../target/lb
DATA="testdata/$1"
-ENV="$DATA/env"
+TOML="$DATA/config.toml"
CLIP_WAIT=1
CLIP_TRIES=3
CLIP_COPY="$DATA/clip.copy"
@@ -26,7 +26,7 @@ if [ -z "$1" ]; then
fi
_unset
-export LOCKBOX_ENV="none"
+export LOCKBOX_CONFIG_TOML="none"
if [ ! -x "${LB_BINARY}" ]; then
echo "binary missing?"
exit 1
@@ -219,15 +219,29 @@ printf "%-10s ... " "$1"
export LOCKBOX_KEYMODE="$OLDMODE"
# configuration
{
- echo "PLAINTEXT=text"
- # shellcheck disable=SC2016
- env | grep '^LOCKBOX' | sed 's/plaintext/$LOCKBOX_FAKE_TEST$PLAINTEXT/g'
- } > "$ENV"
+ cat << EOF
+store = "$LOCKBOX_STORE"
+
+[clip]
+copy = [$(echo "$LOCKBOX_CLIP_COPY" | sed 's/ /", "/g;s/^/"/g;s/$/"/g')]
+copy = [$(echo "$LOCKBOX_CLIP_PASTE" | sed 's/ /", "/g;s/^/"/g;s/$/"/g')]
+max = $LOCKBOX_CLIP_MAX
+
+[json]
+mode = "$LOCKBOX_JSON_DATA"
+hash_length = $LOCKBOX_JSON_DATA_HASH_LENGTH
+
+[keys]
+file = "$LOCKBOX_KEYFILE"
+mode = "$LOCKBOX_KEYMODE"
+key = "$LOCKBOX_KEY"
+EOF
+ } > "$TOML"
_unset
export LOCKBOX_FAKE_TEST=plain
- export LOCKBOX_ENV="none"
+ export LOCKBOX_CONFIG_TOML="none"
${LB_BINARY} ls
- export LOCKBOX_ENV="$ENV"
+ export LOCKBOX_CONFIG_TOML="$TOML"
${LB_BINARY} ls
} 2>&1 | \
sed 's/"modtime": "[0-9].*$/"modtime": "XXXX-XX-XX",/g' | \