lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 68f21667cbde4d324b8399360d1d251583fdee3f
parent 613f1eef09c45e09a7172c5a0a3ff1ca1312df2e
Author: Sean Enck <sean@ttypty.com>
Date:   Sat,  7 Dec 2024 19:20:05 -0500

lockbox runs without os.Getenv/env vars

Diffstat:
MREADME.md | 11+----------
Mcmd/main.go | 2+-
Mgo.mod | 1-
Mgo.sum | 10----------
Minternal/app/help/core_test.go | 2+-
Minternal/app/help/doc/clipboard.txt | 2+-
Minternal/app/help/doc/toml.txt | 6+-----
Minternal/app/info.go | 13+++++++++----
Minternal/app/info_test.go | 9+++++++--
Minternal/app/list_test.go | 13+++++--------
Minternal/app/pwgen.go | 17++++-------------
Minternal/app/pwgen_test.go | 54+++++++++++++++++++++++++++---------------------------
Minternal/app/totp.go | 17+++++------------
Minternal/app/totp_test.go | 44+++++++++++++++++++++-----------------------
Minternal/backend/actions_test.go | 59++++++++++++++++++++++++++---------------------------------
Minternal/backend/core.go | 5+----
Minternal/backend/hooks.go | 11+++++------
Minternal/backend/hooks_test.go | 9+++++----
Minternal/backend/query.go | 5+++--
Minternal/backend/query_test.go | 18++++++++++--------
Minternal/config/core.go | 79++++++-------------------------------------------------------------------------
Minternal/config/core_test.go | 91+++++++-------------------------------------------------------------------------
Minternal/config/env.go | 104+++++++++++++++++++++++++++++--------------------------------------------------
Minternal/config/key.go | 39+++++++++++++++++++++++++--------------
Minternal/config/key_test.go | 85++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Ainternal/config/store/core.go | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/config/store/core_test.go | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/config/toml.go | 67+++++++++++++++++++++++++------------------------------------------
Minternal/config/toml_test.go | 132+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Minternal/config/vars.go | 12++++--------
Minternal/config/vars_test.go | 60++++++++++++++++++++----------------------------------------
Minternal/platform/clip/core.go | 32++++++++++----------------------
Minternal/platform/clip/core_test.go | 67+++++++++++++++++++++++++++++++++++++++----------------------------
Minternal/platform/core.go | 4++--
Mjustfile | 8+++++---
35 files changed, 640 insertions(+), 653 deletions(-)

diff --git a/README.md b/README.md @@ -17,25 +17,16 @@ via other tooling if needed. ## configuration -There are two ways to configure `lb`: -- TOML configuration file(s) -- Environment variables - -The TOML configuration files have higher priority over environment variables -(if both are set) where the TOML files are ultimately loaded into the -processes environment itself (once parsed). To run `lb` at least the -following variables must be set: +`lb` uses TOML configuration file(s) ``` config.toml --- # database to read -# this can also be set via LOCKBOX_STORE store = "$HOME/.passwords/secrets.kdbx" [credentials] # the keying object to use to ACTUALLY unlock the passwords (e.g. using a gpg encrypted file with the password inside of it) -# this can also be set via LOCKBOX_KEY # alternative credential settings for key files are also available password = ["gpg", "--decrypt", "$HOME/.secrets/key.gpg"] ``` diff --git a/cmd/main.go b/cmd/main.go @@ -107,7 +107,7 @@ func run() error { } func clearClipboard() error { - idx := 0 + var idx int64 val, err := platform.Stdin(false) if err != nil { return err diff --git a/go.mod b/go.mod @@ -8,7 +8,6 @@ require ( github.com/pquerna/otp v1.4.0 github.com/tobischo/gokeepasslib/v3 v3.6.0 golang.org/x/text v0.21.0 - mvdan.cc/sh/v3 v3.10.0 ) require ( diff --git a/go.sum b/go.sum @@ -8,20 +8,12 @@ github.com/boombuler/barcode v1.0.2/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= -github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg= github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -40,5 +32,3 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -mvdan.cc/sh/v3 v3.10.0 h1:v9z7N1DLZ7owyLM/SXZQkBSXcwr2IGMm2LY2pmhVXj4= -mvdan.cc/sh/v3 v3.10.0/go.mod h1:z/mSSVyLFGZzqb3ZIKojjyqIx/xbmz/UHdCSv9HmqXY= diff --git a/internal/app/help/core_test.go b/internal/app/help/core_test.go @@ -13,7 +13,7 @@ func TestUsage(t *testing.T) { t.Errorf("invalid usage, out of date? %d", len(u)) } u, _ = help.Usage(true, "lb") - if len(u) != 100 { + if len(u) != 97 { t.Errorf("invalid verbose usage, out of date? %d", len(u)) } for _, usage := range u { diff --git a/internal/app/help/doc/clipboard.txt b/internal/app/help/doc/clipboard.txt @@ -1,3 +1,3 @@ By default clipboard commands are detected via determing the platform and utilizing default commands to interact with (copy to/paste to) the clipboard. -These settings can be overriden via environment variables. +These settings can be overriden via configuration. diff --git a/internal/app/help/doc/toml.txt b/internal/app/help/doc/toml.txt @@ -1,11 +1,7 @@ The core components of `{{ $.Executable }}` are controlled via -environment settings, but these settings are secondary (overriden) -by TOML configuration file(s) if given. Run `{{ $.Executable }} {{ $.HelpCommand }} {{ $.HelpConfigCommand +TOML configuration file(s). Run `{{ $.Executable }} {{ $.HelpCommand }} {{ $.HelpConfigCommand }}` for more information. -- TOML values are read, transformed, and set into the process -environment. - - Arrays defined within the TOML configuration are flattened into a string (space delimited), quoting should be done within array parameters when needed. diff --git a/internal/app/info.go b/internal/app/info.go @@ -14,6 +14,7 @@ import ( "github.com/seanenck/lockbox/internal/app/completions" "github.com/seanenck/lockbox/internal/app/help" "github.com/seanenck/lockbox/internal/config" + "github.com/seanenck/lockbox/internal/config/store" ) // Info will report help/bash/env details @@ -81,11 +82,15 @@ func info(command string, args []string) ([]string, error) { default: return nil, errors.New("invalid env command, too many arguments") } - env := config.Environ(set...) - if len(env) == 0 { - env = []string{""} + var results []string + for _, item := range store.List(set...) { + value := fmt.Sprintf("%s=%v", item.Key, item.Value) + results = append(results, value) } - return env, nil + if len(results) == 0 { + results = []string{""} + } + return results, nil case commands.Completions: shell := "" exe, err := exeName() diff --git a/internal/app/info_test.go b/internal/app/info_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/seanenck/lockbox/internal/app" + "github.com/seanenck/lockbox/internal/config/store" ) func TestNoInfo(t *testing.T) { @@ -19,6 +20,7 @@ func TestNoInfo(t *testing.T) { func TestHelpInfo(t *testing.T) { os.Clearenv() + store.Clear() var buf bytes.Buffer ok, err := app.Info(&buf, "help", []string{}) if !ok || err != nil { @@ -53,6 +55,7 @@ func TestHelpInfo(t *testing.T) { func TestEnvInfo(t *testing.T) { os.Clearenv() + store.Clear() var buf bytes.Buffer ok, err := app.Info(&buf, "env", []string{}) if !ok || err != nil { @@ -62,7 +65,7 @@ func TestEnvInfo(t *testing.T) { t.Error("nothing written") } buf = bytes.Buffer{} - t.Setenv("LOCKBOX_STORE", "1") + store.SetString("LOCKBOX_STORE", "1") ok, err = app.Info(&buf, "env", []string{}) if !ok || err != nil { t.Errorf("invalid error: %v", err) @@ -78,7 +81,7 @@ func TestEnvInfo(t *testing.T) { if buf.String() != "\n" { t.Error("nothing written") } - t.Setenv("LOCKBOX_READONLY", "true") + store.SetString("LOCKBOX_READONLY", "true") buf = bytes.Buffer{} ok, err = app.Info(&buf, "env", []string{"completions"}) if !ok || err != nil { @@ -115,6 +118,7 @@ func TestCompletionInfo(t *testing.T) { "bash": "local cur opts", } { for _, b := range []bool{true, false} { + store.Clear() os.Clearenv() sub := []string{k} t.Setenv("SHELL", "invalid") @@ -142,6 +146,7 @@ func TestCompletionInfo(t *testing.T) { t.Errorf("invalid error: %v", err) } os.Clearenv() + store.Clear() t.Setenv("SHELL", "bad") if _, err := app.Info(&buf, "completions", []string{}); err.Error() != "unknown completion type: bad" { t.Errorf("invalid error: %v", err) diff --git a/internal/app/list_test.go b/internal/app/list_test.go @@ -7,6 +7,7 @@ import ( "github.com/seanenck/lockbox/internal/app" "github.com/seanenck/lockbox/internal/backend" + "github.com/seanenck/lockbox/internal/config/store" "github.com/seanenck/lockbox/internal/platform" ) @@ -24,14 +25,10 @@ func fullSetup(t *testing.T, keep bool) *backend.Transaction { if !keep { os.Remove(file) } - t.Setenv("LOCKBOX_READONLY", "false") - t.Setenv("LOCKBOX_STORE", file) - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "test") - t.Setenv("LOCKBOX_CREDENTIALS_KEY_FILE", "") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") - t.Setenv("LOCKBOX_TOTP_ENTRY", "totp") - t.Setenv("LOCKBOX_HOOKS_DIRECTORY", "") - t.Setenv("LOCKBOX_SET_MODTIME", "") + store.SetString("LOCKBOX_STORE", file) + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"test"}) + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") + store.SetString("LOCKBOX_TOTP_ENTRY", "totp") tr, err := backend.NewTransaction() if err != nil { t.Errorf("failed: %v", err) diff --git a/internal/app/pwgen.go b/internal/app/pwgen.go @@ -18,10 +18,7 @@ import ( // GeneratePassword generates a password func GeneratePassword(cmd CommandOptions) error { - enabled, err := config.EnvPasswordGenEnabled.Get() - if err != nil { - return err - } + enabled := config.EnvPasswordGenEnabled.Get() if !enabled { return errors.New("password generation is disabled") } @@ -34,10 +31,7 @@ func GeneratePassword(cmd CommandOptions) error { } tmplString := config.EnvPasswordGenTemplate.Get() tmplString = strings.ReplaceAll(tmplString, config.TemplateVariable, "$") - wordList, err := config.EnvPasswordGenWordList.Get() - if err != nil { - return err - } + wordList := config.EnvPasswordGenWordList.Get() if len(wordList) == 0 { return errors.New("word list command must set") } @@ -46,10 +40,7 @@ func GeneratePassword(cmd CommandOptions) error { if len(wordList) > 1 { args = wordList[1:] } - capitalize, err := config.EnvPasswordGenTitle.Get() - if err != nil { - return err - } + capitalize := config.EnvPasswordGenTitle.Get() wordResults, err := exec.Command(exe, args...).Output() if err != nil { return err @@ -104,7 +95,7 @@ func GeneratePassword(cmd CommandOptions) error { return errors.New("no sources given") } var selected []word - cnt := 0 + var cnt int64 totalLength := 0 for cnt < length { choice := choices[rand.Intn(found)] diff --git a/internal/app/pwgen_test.go b/internal/app/pwgen_test.go @@ -8,9 +8,11 @@ import ( "testing" "github.com/seanenck/lockbox/internal/app" + "github.com/seanenck/lockbox/internal/config/store" ) func setupGenScript() string { + store.Clear() os.Clearenv() const pwgenScript = "pwgen.sh" pwgenPath := filepath.Join("testdata", pwgenScript) @@ -25,31 +27,31 @@ done func TestGenerateError(t *testing.T) { m := newMockCommand(t) pwgenPath := setupGenScript() - t.Setenv("LOCKBOX_PWGEN_WORD_COUNT", "0") + store.SetInt64("LOCKBOX_PWGEN_WORD_COUNT", 0) if err := app.GeneratePassword(m); err == nil || err.Error() != "word count must be > 0" { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_PWGEN_WORD_COUNT", "1") + store.SetInt64("LOCKBOX_PWGEN_WORD_COUNT", 1) if err := app.GeneratePassword(m); err == nil || err.Error() != "word list command must set" { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_PWGEN_WORDS_COMMAND", "1 x") - if err := app.GeneratePassword(m); err == nil || !strings.Contains(err.Error(), "exec: \"1\":") { + store.SetArray("LOCKBOX_PWGEN_WORDS_COMMAND", []string{"1 x"}) + if err := app.GeneratePassword(m); err == nil || !strings.Contains(err.Error(), "exec: \"1 x\":") { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_PWGEN_WORDS_COMMAND", pwgenPath) + store.SetArray("LOCKBOX_PWGEN_WORDS_COMMAND", []string{pwgenPath}) if err := app.GeneratePassword(m); err == nil || err.Error() != "no sources given" { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_PWGEN_WORDS_COMMAND", fmt.Sprintf("%s 1", pwgenPath)) + store.SetArray("LOCKBOX_PWGEN_WORDS_COMMAND", []string{pwgenPath, "1"}) if err := app.GeneratePassword(m); err != nil { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_PWGEN_WORDS_COMMAND", fmt.Sprintf("%s aloj 1", pwgenPath)) + store.SetArray("LOCKBOX_PWGEN_WORDS_COMMAND", []string{pwgenPath, "aloj", "1"}) if err := app.GeneratePassword(m); err != nil { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_PWGEN_ENABLED", "false") + store.SetBool("LOCKBOX_PWGEN_ENABLED", false) if err := app.GeneratePassword(m); err == nil || err.Error() != "password generation is disabled" { t.Errorf("invalid error: %v", err) } @@ -68,33 +70,31 @@ func testPasswordGen(t *testing.T, expect string) { func TestGenerate(t *testing.T) { pwgenPath := setupGenScript() - t.Setenv("LOCKBOX_PWGEN_WORD_COUNT", "1") - t.Setenv("LOCKBOX_PWGEN_WORDS_COMMAND", fmt.Sprintf("%s 1", pwgenPath)) + store.SetInt64("LOCKBOX_PWGEN_WORD_COUNT", 1) + store.SetArray("LOCKBOX_PWGEN_WORDS_COMMAND", []string{pwgenPath, "1"}) testPasswordGen(t, "1") - t.Setenv("LOCKBOX_PWGEN_WORD_COUNT", "10") - t.Setenv("LOCKBOX_PWGEN_WORDS_COMMAND", fmt.Sprintf("%s 1 1 1 1 1 1 1 1 1 1 1 1", pwgenPath)) + store.SetInt64("LOCKBOX_PWGEN_WORD_COUNT", 10) + store.SetArray("LOCKBOX_PWGEN_WORDS_COMMAND", []string{pwgenPath, "1 1 1 1 1 1 1 1 1 1 1 1"}) testPasswordGen(t, "1-1-1-1-1-1-1-1-1-1") - t.Setenv("LOCKBOX_PWGEN_WORD_COUNT", "4") - t.Setenv("LOCKBOX_PWGEN_TITLE", "true") - t.Setenv("LOCKBOX_PWGEN_WORDS_COMMAND", fmt.Sprintf("%s a a a a a a a a a a a a a a a", pwgenPath)) + store.SetInt64("LOCKBOX_PWGEN_WORD_COUNT", 4) + store.SetBool("LOCKBOX_PWGEN_TITLE", true) + store.SetArray("LOCKBOX_PWGEN_WORDS_COMMAND", []string{pwgenPath, "a a a a a a a a a a a a a a a a a a a a a a"}) testPasswordGen(t, "A-A-A-A") - t.Setenv("LOCKBOX_PWGEN_CHARACTERS", "bc") - t.Setenv("LOCKBOX_PWGEN_WORDS_COMMAND", fmt.Sprintf("%s abc abc abc abc abc aaa aaa aa a", pwgenPath)) + store.SetString("LOCKBOX_PWGEN_CHARACTERS", "bc") + store.SetArray("LOCKBOX_PWGEN_WORDS_COMMAND", []string{pwgenPath, "abc abc abc abc abc abc aaa aa aaa a"}) testPasswordGen(t, "Bc-Bc-Bc-Bc") - os.Unsetenv("LOCKBOX_PWGEN_CHARACTERS") - t.Setenv("LOCKBOX_PWGEN_WORDS_COMMAND", fmt.Sprintf("%s a a a a a a a a a a a a a a a", pwgenPath)) - t.Setenv("LOCKBOX_PWGEN_TITLE", "false") - t.Setenv("LOCKBOX_PWGEN_TITLE", "false") + store.SetString("LOCKBOX_PWGEN_CHARACTERS", "") + store.SetArray("LOCKBOX_PWGEN_WORDS_COMMAND", []string{pwgenPath, "a a a a a a a a a a a a a a a a a a a a a a"}) + store.SetBool("LOCKBOX_PWGEN_TITLE", false) testPasswordGen(t, "a-a-a-a") // NOTE: this allows templating below in golang - t.Setenv("DOLLAR", "$") - t.Setenv("LOCKBOX_PWGEN_TEMPLATE", "{{range ${DOLLAR}idx, ${DOLLAR}val := .}}{{if lt ${DOLLAR}idx 5}}-{{end}}{{ ${DOLLAR}val.Text }}{{ ${DOLLAR}val.Position.Start }}{{ ${DOLLAR}val.Position.End }}{{end}}") + store.SetString("LOCKBOX_PWGEN_TEMPLATE", "{{range $idx, $val := .}}{{if lt $idx 5}}-{{end}}{{ $val.Text }}{{ $val.Position.Start }}{{ $val.Position.End }}{{end}}") testPasswordGen(t, "-a01-a12-a23-a34") - t.Setenv("LOCKBOX_PWGEN_TEMPLATE", "{{range [%]idx, [%]val := .}}{{if lt [%]idx 5}}-{{end}}{{ [%]val.Text }}{{end}}") + store.SetString("LOCKBOX_PWGEN_TEMPLATE", "{{range [%]idx, [%]val := .}}{{if lt [%]idx 5}}-{{end}}{{ [%]val.Text }}{{end}}") testPasswordGen(t, "-a-a-a-a") - os.Unsetenv("LOCKBOX_PWGEN_TEMPLATE") - t.Setenv("LOCKBOX_PWGEN_TITLE", "true") - t.Setenv("LOCKBOX_PWGEN_WORDS_COMMAND", fmt.Sprintf("%s abc axy axY aZZZ aoijafea aoiajfoea afaeoa", pwgenPath)) + store.Clear() + store.SetBool("LOCKBOX_PWGEN_TITLE", true) + store.SetArray("LOCKBOX_PWGEN_WORDS_COMMAND", []string{pwgenPath, "abc axy axY aZZZ aoijafea aoiajfoea afeafa"}) m := newMockCommand(t) if err := app.GeneratePassword(m); err != nil { t.Errorf("invalid error: %v", err) diff --git a/internal/app/totp.go b/internal/app/totp.go @@ -41,8 +41,8 @@ type ( TOTPOptions struct { app CommandOptions Clear func() - CanTOTP func() (bool, error) - IsInteractive func() (bool, error) + CanTOTP func() bool + IsInteractive func() bool } ) @@ -90,10 +90,7 @@ func (w totpWrapper) generateCode() (string, error) { } func (args *TOTPArguments) display(opts TOTPOptions) error { - interactive, err := opts.IsInteractive() - if err != nil { - return err - } + interactive := opts.IsInteractive() if args.Mode == MinimalTOTPMode { interactive = false } @@ -130,7 +127,7 @@ func (args *TOTPArguments) display(opts TOTPOptions) error { return nil } first := true - running := 0 + var running int64 lastSecond := -1 if !clipMode { if !once { @@ -221,11 +218,7 @@ func (args *TOTPArguments) Do(opts TOTPOptions) error { if opts.Clear == nil || opts.CanTOTP == nil || opts.IsInteractive == nil { return errors.New("invalid option functions") } - can, err := opts.CanTOTP() - if err != nil { - return err - } - if !can { + if !opts.CanTOTP() { return ErrNoTOTP } if args.Mode == ListTOTPMode { diff --git a/internal/app/totp_test.go b/internal/app/totp_test.go @@ -9,6 +9,7 @@ import ( "github.com/seanenck/lockbox/internal/app" "github.com/seanenck/lockbox/internal/backend" + "github.com/seanenck/lockbox/internal/config/store" ) type ( @@ -29,29 +30,26 @@ func newMock(t *testing.T) (*mockOptions, app.TOTPOptions) { opts := app.NewDefaultTOTPOptions(m) opts.Clear = func() { } - opts.CanTOTP = func() (bool, error) { - return true, nil + opts.CanTOTP = func() bool { + return true } - opts.IsInteractive = func() (bool, error) { - return true, nil + opts.IsInteractive = func() bool { + return true } return m, opts } func fullTOTPSetup(t *testing.T, keep bool) *backend.Transaction { + store.Clear() file := testFile() if !keep { os.Remove(file) } - t.Setenv("LOCKBOX_READONLY", "false") - t.Setenv("LOCKBOX_STORE", file) - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "test") - t.Setenv("LOCKBOX_CREDENTIALS_KEY_FILE", "") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") - t.Setenv("LOCKBOX_TOTP_ENTRY", "totp") - t.Setenv("LOCKBOX_HOOKS_DIRECTORY", "") - t.Setenv("LOCKBOX_DEFAULTS_MODTIME", "") - t.Setenv("LOCKBOX_TOTP_TIMEOUT", "1") + store.SetString("LOCKBOX_STORE", file) + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"test"}) + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") + store.SetString("LOCKBOX_TOTP_ENTRY", "totp") + store.SetInt64("LOCKBOX_TOTP_TIMEOUT", 1) tr, err := backend.NewTransaction() if err != nil { t.Errorf("failed: %v", err) @@ -143,14 +141,14 @@ func TestDoErrors(t *testing.T) { if err := args.Do(opts); err == nil || err.Error() != "invalid option functions" { t.Errorf("invalid error: %v", err) } - opts.CanTOTP = func() (bool, error) { - return false, nil + opts.CanTOTP = func() bool { + return false } if err := args.Do(opts); err == nil || err.Error() != "invalid option functions" { t.Errorf("invalid error: %v", err) } - opts.IsInteractive = func() (bool, error) { - return false, nil + opts.IsInteractive = func() bool { + return false } if err := args.Do(opts); err == nil || err.Error() != "totp is disabled" { t.Errorf("invalid error: %v", err) @@ -173,14 +171,14 @@ func TestNonListError(t *testing.T) { setupTOTP(t) args, _ := app.NewTOTPArguments([]string{"clip", "test"}, "totp") _, opts := newMock(t) - opts.IsInteractive = func() (bool, error) { - return false, nil + opts.IsInteractive = func() bool { + return false } if err := args.Do(opts); err == nil || err.Error() != "clipboard not available in non-interactive mode" { t.Errorf("invalid error: %v", err) } - opts.IsInteractive = func() (bool, error) { - return true, nil + opts.IsInteractive = func() bool { + return true } if err := args.Do(opts); err == nil || err.Error() != "object does not exist" { t.Errorf("invalid error: %v", err) @@ -203,8 +201,8 @@ func TestNonInteractive(t *testing.T) { setupTOTP(t) args, _ := app.NewTOTPArguments([]string{"show", "test/test3"}, "totp") m, opts := newMock(t) - opts.IsInteractive = func() (bool, error) { - return false, nil + opts.IsInteractive = func() bool { + return false } if err := args.Do(opts); err != nil { t.Errorf("invalid error: %v", err) diff --git a/internal/backend/actions_test.go b/internal/backend/actions_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/seanenck/lockbox/internal/backend" + "github.com/seanenck/lockbox/internal/config/store" "github.com/seanenck/lockbox/internal/platform" ) @@ -27,14 +28,11 @@ func fullSetup(t *testing.T, keep bool) *backend.Transaction { if !keep { os.Remove(file) } - t.Setenv("LOCKBOX_READONLY", "false") - t.Setenv("LOCKBOX_STORE", file) - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "test") - t.Setenv("LOCKBOX_CREDENTIALS_KEY_FILE", "") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") - t.Setenv("LOCKBOX_TOTP_ENTRY", "totp") - t.Setenv("LOCKBOX_HOOKS_DIRECTORY", "") - t.Setenv("LOCKBOX_DEFAULTS_MODTIME", "") + store.SetBool("LOCKBOX_READONLY", false) + store.SetString("LOCKBOX_STORE", file) + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"test"}) + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") + store.SetString("LOCKBOX_TOTP_ENTRY", "totp") tr, err := backend.NewTransaction() if err != nil { t.Errorf("failed: %v", err) @@ -43,18 +41,16 @@ func fullSetup(t *testing.T, keep bool) *backend.Transaction { } func TestKeyFile(t *testing.T) { - os.Clearenv() + store.Clear() + defer store.Clear() file := testFile("keyfile_test.kdbx") keyFile := testFile("file.key") os.Remove(file) - t.Setenv("LOCKBOX_READONLY", "false") - t.Setenv("LOCKBOX_STORE", file) - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "test") - t.Setenv("LOCKBOX_CREDENTIALS_KEY_FILE", keyFile) - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") - t.Setenv("LOCKBOX_TOTP_ENTRY", "totp") - t.Setenv("LOCKBOX_HOOKS_DIRECTORY", "") - t.Setenv("LOCKBOX_DEFAULTS_MODTIME", "") + store.SetString("LOCKBOX_STORE", file) + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"test"}) + store.SetString("LOCKBOX_CREDENTIALS_KEY_FILE", keyFile) + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") + store.SetString("LOCKBOX_TOTP_ENTRY", "totp") os.WriteFile(keyFile, []byte("test"), 0o644) tr, err := backend.NewTransaction() if err != nil { @@ -71,7 +67,7 @@ func setup(t *testing.T) *backend.Transaction { func TestNoWriteOnRO(t *testing.T) { setup(t) - t.Setenv("LOCKBOX_READONLY", "true") + store.SetBool("LOCKBOX_READONLY", true) tr, _ := backend.NewTransaction() if err := tr.Insert("a/a/a", "a"); err.Error() != "unable to alter database in readonly mode" { t.Errorf("wrong error: %v", err) @@ -80,7 +76,7 @@ func TestNoWriteOnRO(t *testing.T) { func TestBadTOTP(t *testing.T) { tr := setup(t) - t.Setenv("LOCKBOX_TOTP_ENTRY", "Title") + store.SetString("LOCKBOX_TOTP_ENTRY", "Title") if err := tr.Insert("a/a/a", "a"); err.Error() != "invalid totp field, uses restricted name" { t.Errorf("wrong error: %v", err) } @@ -280,19 +276,19 @@ func TestKeyAndOrKeyFile(t *testing.T) { } func keyAndOrKeyFile(t *testing.T, key, keyFile bool) { - os.Clearenv() + store.Clear() file := testFile("keyorkeyfile.kdbx") os.Remove(file) - t.Setenv("LOCKBOX_STORE", file) + store.SetString("LOCKBOX_STORE", file) if key { - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "test") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"test"}) + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") } else { - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "none") + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "none") } if keyFile { key := testFile("keyfileor.key") - t.Setenv("LOCKBOX_CREDENTIALS_KEY_FILE", key) + store.SetString("LOCKBOX_CREDENTIALS_KEY_FILE", key) os.WriteFile(key, []byte("test"), 0o644) } tr, err := backend.NewTransaction() @@ -313,17 +309,14 @@ func keyAndOrKeyFile(t *testing.T, key, keyFile bool) { } func TestReKey(t *testing.T) { - os.Clearenv() + store.Clear() f := "rekey_test.kdbx" file := testFile(f) defer os.Remove(filepath.Join(testDir, f)) - t.Setenv("LOCKBOX_READONLY", "false") - t.Setenv("LOCKBOX_STORE", file) - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "test") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") - t.Setenv("LOCKBOX_TOTP_ENTRY", "totp") - t.Setenv("LOCKBOX_HOOKS_DIRECTORY", "") - t.Setenv("LOCKBOX_DEFAULTS_MODTIME", "") + store.SetString("LOCKBOX_STORE", file) + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"test"}) + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") + store.SetString("LOCKBOX_TOTP_ENTRY", "totp") tr, err := backend.NewTransaction() if err != nil { t.Errorf("failed: %v", err) diff --git a/internal/backend/core.go b/internal/backend/core.go @@ -65,10 +65,7 @@ func loadFile(file string, must bool) (*Transaction, error) { return nil, errors.New("invalid file, does not exist") } } - ro, err := config.EnvReadOnly.Get() - if err != nil { - return nil, err - } + ro := config.EnvReadOnly.Get() return &Transaction{valid: true, file: file, exists: exists, readonly: ro}, nil } diff --git a/internal/backend/hooks.go b/internal/backend/hooks.go @@ -3,6 +3,7 @@ package backend import ( "errors" + "fmt" "os" "os/exec" "path/filepath" @@ -25,6 +26,7 @@ type ( ) const ( + internalHookEnv = "___HOOK___CALLED___" // HookPre are triggers BEFORE an action is performed on an entity HookPre HookMode = "pre" // HookPost are triggers AFTER an action is performed on an entity @@ -33,11 +35,8 @@ const ( // NewHook will create a new hook type func NewHook(path string, a ActionMode) (Hook, error) { - enabled, err := config.EnvHooksEnabled.Get() - if err != nil { - return Hook{}, err - } - if !enabled { + enabled := config.EnvHooksEnabled.Get() + if !enabled || os.Getenv(internalHookEnv) != "" { return Hook{enabled: false}, nil } if strings.TrimSpace(path) == "" { @@ -70,7 +69,7 @@ func (h Hook) Run(mode HookMode) error { return nil } env := os.Environ() - env = append(env, config.EnvHooksEnabled.KeyValue(config.NoValue)) + env = append(env, fmt.Sprintf("%s=1", internalHookEnv)) for _, s := range h.scripts { c := exec.Command(s, string(mode), string(h.mode), h.path) c.Stdout = os.Stdout diff --git a/internal/backend/hooks_test.go b/internal/backend/hooks_test.go @@ -7,10 +7,11 @@ import ( "testing" "github.com/seanenck/lockbox/internal/backend" + "github.com/seanenck/lockbox/internal/config/store" ) func TestHooks(t *testing.T) { - t.Setenv("LOCKBOX_HOOKS_DIRECTORY", "") + store.Clear() h, err := backend.NewHook("a", backend.InsertAction) if err != nil { t.Errorf("invalid error: %v", err) @@ -21,7 +22,7 @@ func TestHooks(t *testing.T) { if _, err := backend.NewHook("", backend.InsertAction); err.Error() != "empty path is not allowed for hooks" { t.Errorf("wrong error: %v", err) } - t.Setenv("LOCKBOX_HOOKS_DIRECTORY", "is_garbage") + store.SetString("LOCKBOX_HOOKS_DIRECTORY", "is_garbage") if _, err := backend.NewHook("b", backend.InsertAction); err.Error() != "hook directory does NOT exist" { t.Errorf("wrong error: %v", err) } @@ -30,7 +31,7 @@ func TestHooks(t *testing.T) { if err := os.MkdirAll(testPath, 0o755); err != nil { t.Errorf("failed, mkdir: %v", err) } - t.Setenv("LOCKBOX_HOOKS_DIRECTORY", testPath) + store.SetString("LOCKBOX_HOOKS_DIRECTORY", testPath) h, err = backend.NewHook("a", backend.InsertAction) if err != nil { t.Errorf("invalid error: %v", err) @@ -59,7 +60,7 @@ func TestHooks(t *testing.T) { if err := h.Run(backend.HookPre); strings.Contains("fork/exec", err.Error()) { t.Errorf("wrong error: %v", err) } - t.Setenv("LOCKBOX_HOOKS_ENABLED", "false") + store.SetBool("LOCKBOX_HOOKS_ENABLED", false) h, err = backend.NewHook("a", backend.InsertAction) if err != nil { t.Errorf("invalid error: %v", err) diff --git a/internal/backend/query.go b/internal/backend/query.go @@ -183,13 +183,14 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { } jsonMode = m } - var hashLength int + var hashLength int64 if jsonMode == output.JSONModes.Hash { hashLength, err = config.EnvJSONHashLength.Get() if err != nil { return nil, err } } + l := int(hashLength) return func(yield func(Entity, error) bool) { for _, item := range entities { entity := Entity{Path: item.path} @@ -207,7 +208,7 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { data = val case output.JSONModes.Hash: data = fmt.Sprintf("%x", sha512.Sum512([]byte(val))) - if hashLength > 0 && len(data) > hashLength { + if hashLength > 0 && len(data) > l { data = data[0:hashLength] } } diff --git a/internal/backend/query_test.go b/internal/backend/query_test.go @@ -2,11 +2,11 @@ package backend_test import ( "encoding/json" - "os" "strings" "testing" "github.com/seanenck/lockbox/internal/backend" + "github.com/seanenck/lockbox/internal/config/store" ) func setupInserts(t *testing.T) { @@ -18,6 +18,7 @@ func setupInserts(t *testing.T) { } func TestMatchPath(t *testing.T) { + store.Clear() setupInserts(t) q, err := fullSetup(t, true).MatchPath("test/test/abc") if err != nil { @@ -74,7 +75,7 @@ func TestGet(t *testing.T) { } func TestValueModes(t *testing.T) { - os.Clearenv() + store.Clear() setupInserts(t) q, err := fullSetup(t, true).Get("test/test/abc", backend.BlankValue) if err != nil { @@ -97,7 +98,7 @@ func TestValueModes(t *testing.T) { if len(m.ModTime) < 20 { t.Errorf("invalid date/time") } - t.Setenv("LOCKBOX_JSON_HASH_LENGTH", "10") + store.SetInt64("LOCKBOX_JSON_HASH_LENGTH", 10) q, err = fullSetup(t, true).Get("test/test/abc", backend.JSONValue) if err != nil { t.Errorf("no error: %v", err) @@ -127,7 +128,7 @@ func TestValueModes(t *testing.T) { if len(m.ModTime) < 20 || m.Data == "" { t.Errorf("invalid json: %v", m) } - t.Setenv("LOCKBOX_JSON_MODE", "plAINtExt") + store.SetString("LOCKBOX_JSON_MODE", "plAINtExt") q, err = fullSetup(t, true).Get("test/test/abc", backend.JSONValue) if err != nil { t.Errorf("no error: %v", err) @@ -139,7 +140,7 @@ func TestValueModes(t *testing.T) { if len(m.ModTime) < 20 || m.Data != "tedst" { t.Errorf("invalid json: %v", m) } - t.Setenv("LOCKBOX_JSON_MODE", "emPTY") + store.SetString("LOCKBOX_JSON_MODE", "emPTY") q, err = fullSetup(t, true).Get("test/test/abc", backend.JSONValue) if err != nil { t.Errorf("no error: %v", err) @@ -209,9 +210,10 @@ func TestQueryCallback(t *testing.T) { } func TestSetModTime(t *testing.T) { + store.Clear() testDateTime := "2022-12-30T12:34:56-05:00" tr := fullSetup(t, false) - t.Setenv("LOCKBOX_DEFAULTS_MODTIME", testDateTime) + store.SetString("LOCKBOX_DEFAULTS_MODTIME", testDateTime) tr.Insert("test/xyz", "test") q, err := fullSetup(t, true).Get("test/xyz", backend.JSONValue) if err != nil { @@ -225,8 +227,8 @@ func TestSetModTime(t *testing.T) { t.Errorf("invalid date/time") } + store.Clear() tr = fullSetup(t, false) - t.Setenv("LOCKBOX_DEFAULTS_MODTIME", "") tr.Insert("test/xyz", "test") q, err = fullSetup(t, true).Get("test/xyz", backend.JSONValue) if err != nil { @@ -241,7 +243,7 @@ func TestSetModTime(t *testing.T) { } tr = fullSetup(t, false) - t.Setenv("LOCKBOX_DEFAULTS_MODTIME", "garbage") + store.SetString("LOCKBOX_DEFAULTS_MODTIME", "garbage") err = tr.Insert("test/xyz", "test") if err == nil || !strings.Contains(err.Error(), "parsing time") { t.Errorf("invalid error: %v", err) diff --git a/internal/config/core.go b/internal/config/core.go @@ -6,20 +6,17 @@ import ( "net/url" "os" "path/filepath" - "slices" - "sort" "strings" "time" + "github.com/seanenck/lockbox/internal/config/store" "github.com/seanenck/lockbox/internal/util" - "mvdan.cc/sh/v3/shell" ) const ( yes = "true" no = "false" detectEnvironment = "detect" - noEnvironment = "none" tomlFile = "lockbox.toml" // sub categories clipCategory keyCategory = "CLIP_" @@ -74,31 +71,9 @@ type ( } ) -func shlex(in string) ([]string, error) { - return shell.Fields(in, os.Getenv) -} - -func getExpand(key string) string { - return os.ExpandEnv(os.Getenv(key)) -} - -func environOrDefault(envKey, defaultValue string) string { - val := getExpand(envKey) - if strings.TrimSpace(val) == "" { - return defaultValue - } - return val -} - // NewConfigFiles will get the list of candidate config files func NewConfigFiles() []string { - v := EnvConfig.Get() - if v == "" || v == noEnvironment { - return []string{} - } - if err := EnvConfig.Set(noEnvironment); err != nil { - return nil - } + v := os.Expand(os.Getenv(EnvConfig.Key()), os.Getenv) if v != detectEnvironment { return []string{v} } @@ -116,40 +91,6 @@ func NewConfigFiles() []string { return options } -// IsUnset will indicate if a variable is an unset (and unset it) or return that it isn't -func IsUnset(k, v string) (bool, error) { - if strings.TrimSpace(v) == "" { - return true, os.Unsetenv(k) - } - return false, nil -} - -// Environ will list the current environment keys -func Environ(set ...string) []string { - var results []string - filtered := len(set) > 0 - for _, k := range os.Environ() { - for _, r := range registry { - rawKey := r.self().Key() - if rawKey == EnvConfig.Key() { - continue - } - key := fmt.Sprintf("%s=", rawKey) - if !strings.HasPrefix(k, key) { - continue - } - if filtered { - if !slices.Contains(set, rawKey) { - continue - } - } - results = append(results, k) - } - } - sort.Strings(results) - return results -} - func environmentRegister[T printer](obj T) T { registry[obj.self().Key()] = obj return obj @@ -170,8 +111,8 @@ func formatterTOTP(key, value string) string { if strings.HasPrefix(value, otpAuth) { return value } - override := environOrDefault(key, "") - if override != "" { + override, ok := store.GetString(key) + if ok { return fmt.Sprintf(override, value) } v := url.Values{} @@ -194,17 +135,9 @@ func CanColor() (bool, error) { if _, noColor := os.LookupEnv("NO_COLOR"); noColor { return false, nil } - interactive, err := EnvInteractive.Get() - if err != nil { - return false, err - } - colors := interactive + colors := EnvInteractive.Get() if colors { - isColored, err := EnvColorEnabled.Get() - if err != nil { - return false, err - } - colors = isColored + colors = EnvColorEnabled.Get() } return colors, nil } diff --git a/internal/config/core_test.go b/internal/config/core_test.go @@ -3,52 +3,16 @@ package config_test import ( "fmt" "os" - "strings" "testing" "github.com/seanenck/lockbox/internal/config" + "github.com/seanenck/lockbox/internal/config/store" ) -func isSet(key string) bool { - for _, item := range os.Environ() { - if strings.HasPrefix(item, fmt.Sprintf("%s=", key)) { - return true - } - } - return false -} - -func TestSet(t *testing.T) { - os.Clearenv() - config.EnvStore.Set("TEST") - if config.EnvStore.Get() != "TEST" { - t.Errorf("invalid set/get") - } - if !isSet("LOCKBOX_STORE") { - t.Error("should be set") - } - config.EnvStore.Set("") - if isSet("LOCKBOX_STORE") { - t.Error("should be set") - } -} - -func TestKeyValue(t *testing.T) { - val := config.EnvStore.KeyValue("TEST") - if val != "LOCKBOX_STORE=TEST" { - t.Errorf("invalid keyvalue") - } -} - func TestNewEnvFiles(t *testing.T) { os.Clearenv() - t.Setenv("LOCKBOX_CONFIG_TOML", "none") - f := config.NewConfigFiles() - if len(f) != 0 { - t.Errorf("invalid files: %v", f) - } t.Setenv("LOCKBOX_CONFIG_TOML", "test") - f = config.NewConfigFiles() + f := config.NewConfigFiles() if len(f) != 1 || f[0] != "test" { t.Errorf("invalid files: %v", f) } @@ -72,49 +36,8 @@ func TestNewEnvFiles(t *testing.T) { } } -func TestIsUnset(t *testing.T) { - os.Clearenv() - o, err := config.IsUnset("test", " ") - if err != nil || !o { - t.Error("was unset") - } - o, err = config.IsUnset("test", "") - if err != nil || !o { - t.Error("was unset") - } - o, err = config.IsUnset("test", "a") - if err != nil || o { - t.Error("was set") - } - t.Setenv("UNSET_TEST", "abc") - config.IsUnset("UNSET_TEST", "") - if isSet("UNSET_TEST") { - t.Error("found unset var") - } -} - -func TestEnviron(t *testing.T) { - os.Clearenv() - e := config.Environ() - if len(e) != 0 { - t.Error("invalid environ") - } - t.Setenv("LOCKBOX_STORE", "1") - t.Setenv("LOCKBOX_2", "2") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "2") - t.Setenv("LOCKBOX_ENV", "2") - e = config.Environ() - if len(e) != 2 || fmt.Sprintf("%v", e) != "[LOCKBOX_CREDENTIALS_PASSWORD=2 LOCKBOX_STORE=1]" { - t.Errorf("invalid environ: %v", e) - } - e = config.Environ("LOCKBOX_STORE", "LOCKBOX_OTHER") - if len(e) != 1 || fmt.Sprintf("%v", e) != "[LOCKBOX_STORE=1]" { - t.Errorf("invalid environ: %v", e) - } -} - func TestCanColor(t *testing.T) { - os.Clearenv() + store.Clear() if can, _ := config.CanColor(); !can { t.Error("should be able to color") } @@ -122,18 +45,18 @@ func TestCanColor(t *testing.T) { "INTERACTIVE": true, "COLOR_ENABLED": true, } { - os.Clearenv() + store.Clear() key := fmt.Sprintf("LOCKBOX_%s", raw) - t.Setenv(key, "true") + store.SetBool(key, true) if can, _ := config.CanColor(); can != expect { t.Errorf("expect != actual: %s", key) } - t.Setenv(key, "false") + store.SetBool(key, false) if can, _ := config.CanColor(); can == expect { t.Errorf("expect == actual: %s", key) } } - os.Clearenv() + store.Clear() t.Setenv("NO_COLOR", "1") if can, _ := config.CanColor(); can { t.Error("should NOT be able to color") diff --git a/internal/config/env.go b/internal/config/env.go @@ -3,9 +3,9 @@ package config import ( "fmt" - "os" - "strconv" "strings" + + "github.com/seanenck/lockbox/internal/config/store" ) type ( @@ -53,84 +53,56 @@ func (e environmentBase) Key() string { } // Get will get the boolean value for the setting -func (e EnvironmentBool) Get() (bool, error) { - return parseStringYesNo(e, getExpand(e.Key())) -} - -func parseStringYesNo(e EnvironmentBool, in string) (bool, error) { - read := strings.ToLower(strings.TrimSpace(in)) - switch read { - case no: - return false, nil - case yes: - return true, nil - case "": - return e.defaultValue, nil +func (e EnvironmentBool) Get() bool { + val, ok := store.GetBool(e.Key()) + if !ok { + val = e.defaultValue } - - return false, fmt.Errorf("invalid yes/no env value for %s", e.Key()) + return val } // Get will get the integer value for the setting -func (e EnvironmentInt) Get() (int, error) { - val := e.defaultValue - use := getExpand(e.Key()) - if use != "" { - i, err := strconv.Atoi(use) - if err != nil { - return -1, err - } - invalid := false - check := "" - if e.allowZero { - check = "=" - } - switch i { - case 0: - invalid = !e.allowZero - default: - invalid = i < 0 - } - if invalid { - return -1, fmt.Errorf("%s must be >%s 0", e.shortDesc, check) - } - val = i +func (e EnvironmentInt) Get() (int64, error) { + i, ok := store.GetInt64(e.Key()) + if !ok { + i = int64(e.defaultValue) + } + invalid := false + check := "" + if e.allowZero { + check = "=" } - return val, nil + switch i { + case 0: + invalid = !e.allowZero + default: + invalid = i < 0 + } + if invalid { + return -1, fmt.Errorf("%s must be >%s 0", e.shortDesc, check) + } + return i, nil } // Get will read the string from the environment func (e EnvironmentString) Get() string { - if !e.canDefault { - return getExpand(e.Key()) + val, ok := store.GetString(e.Key()) + if !ok { + if !e.canDefault { + return "" + } + val = e.defaultValue } - return environOrDefault(e.Key(), e.defaultValue) + return val } // Get will read (and shlex) the value if set -func (e EnvironmentCommand) Get() ([]string, error) { - value := environOrDefault(e.Key(), "") - if strings.TrimSpace(value) == "" { - return nil, nil - } - return shlex(value) -} - -// KeyValue will get the string representation of the key+value -func (e environmentBase) KeyValue(value string) string { - return fmt.Sprintf("%s=%s", e.Key(), value) -} - -// Setenv will do an environment set for the value to key -func (e environmentBase) Set(value string) error { - unset, err := IsUnset(e.Key(), value) - if err != nil { - return err - } - if unset { - return nil +func (e EnvironmentCommand) Get() []string { + val, ok := store.GetArray(e.Key()) + if !ok { + return []string{} } - return os.Setenv(e.Key(), value) + return val } // Get will retrieve the value with the formatted input included diff --git a/internal/config/key.go b/internal/config/key.go @@ -6,6 +6,8 @@ import ( "fmt" "os/exec" "strings" + + "github.com/seanenck/lockbox/internal/config/store" ) type ( @@ -15,7 +17,7 @@ type ( AskPassword func() (string, error) // Key is a wrapper to help manage the returned key Key struct { - inputKey string + inputKey []string mode KeyModeType valid bool } @@ -35,7 +37,6 @@ const ( // NewKey will create a new key func NewKey(defaultKeyModeType KeyModeType) (Key, error) { - useKey := envPassword.Get() keyMode := EnvPasswordMode.Get() if keyMode == "" { keyMode = string(defaultKeyModeType) @@ -43,15 +44,12 @@ func NewKey(defaultKeyModeType KeyModeType) (Key, error) { requireEmptyKey := false switch keyMode { case string(IgnoreKeyMode): - return Key{mode: IgnoreKeyMode, inputKey: "", valid: true}, nil + return Key{mode: IgnoreKeyMode, inputKey: []string{}, valid: true}, nil case string(noKeyMode): requireEmptyKey = true case string(commandKeyMode), string(plainKeyMode): case string(AskKeyMode): - isInteractive, err := EnvInteractive.Get() - if err != nil { - return Key{}, err - } + isInteractive := EnvInteractive.Get() if !isInteractive { return Key{}, errors.New("ask key mode requested in non-interactive mode") } @@ -59,7 +57,13 @@ func NewKey(defaultKeyModeType KeyModeType) (Key, error) { default: return Key{}, fmt.Errorf("unknown key mode: %s", keyMode) } - isEmpty := strings.TrimSpace(useKey) == "" + useKey, ok := store.GetArray(envPassword.Key()) + isEmpty := !ok || len(useKey) == 0 + if !isEmpty { + if strings.TrimSpace(useKey[0]) == "" { + isEmpty = true + } + } if requireEmptyKey { if !isEmpty { return Key{}, errors.New("key can NOT be set in this key mode") @@ -92,7 +96,10 @@ func (k Key) Read(ask AskPassword) (string, error) { if k.empty() && !k.Ask() { return "", nil } - useKey := k.inputKey + var useKey string + if len(k.inputKey) > 0 { + useKey = k.inputKey[0] + } switch k.mode { case AskKeyMode: read, err := ask() @@ -101,11 +108,15 @@ func (k Key) Read(ask AskPassword) (string, error) { } useKey = read case commandKeyMode: - parts, err := shlex(useKey) - if err != nil { - return "", err + exe := k.inputKey[0] + var args []string + for idx, k := range k.inputKey { + if idx == 0 { + continue + } + args = append(args, k) } - cmd := exec.Command(parts[0], parts[1:]...) + cmd := exec.Command(exe, args...) b, err := cmd.Output() if err != nil { return "", fmt.Errorf("key command failed: %w", err) @@ -113,7 +124,7 @@ func (k Key) Read(ask AskPassword) (string, error) { useKey = string(b) } key := strings.TrimSpace(useKey) - if strings.TrimSpace(key) == "" { + if key == "" { return "", errors.New("key is empty") } return key, nil diff --git a/internal/config/key_test.go b/internal/config/key_test.go @@ -6,75 +6,75 @@ import ( "testing" "github.com/seanenck/lockbox/internal/config" + "github.com/seanenck/lockbox/internal/config/store" ) func TestDefaultKey(t *testing.T) { - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "test") - if _, err := config.NewKey(config.IgnoreKeyMode); err != nil { + store.Clear() + if _, err := config.NewKey(config.DefaultKeyMode); err == nil || err.Error() != "key MUST be set in this key mode" { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "") - if _, err := config.NewKey(config.DefaultKeyMode); err == nil || err.Error() != "key MUST be set in this key mode" { + store.Clear() + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"test"}) + if _, err := config.NewKey(config.IgnoreKeyMode); err != nil { t.Errorf("invalid error: %v", err) } } func TestNewKeyErrors(t *testing.T) { - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "invalid") + store.Clear() + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "invalid") if _, err := config.NewKey(config.IgnoreKeyMode); err == nil || err.Error() != "unknown key mode: invalid" { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "none") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", " test") + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "none") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"test"}) if _, err := config.NewKey(config.IgnoreKeyMode); err == nil || err.Error() != "key can NOT be set in this key mode" { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ask") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "test") + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ask") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"test"}) if _, err := config.NewKey(config.IgnoreKeyMode); err == nil || err.Error() != "key can NOT be set in this key mode" { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "command") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", " ") + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "command") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{}) if _, err := config.NewKey(config.IgnoreKeyMode); err == nil || err.Error() != "key MUST be set in this key mode" { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "") + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{" "}) if _, err := config.NewKey(config.IgnoreKeyMode); err == nil || err.Error() != "key MUST be set in this key mode" { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_INTERACTIVE", "true") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ask") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "") + store.SetBool("LOCKBOX_INTERACTIVE", true) + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ask") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{}) if _, err := config.NewKey(config.IgnoreKeyMode); err != nil { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_INTERACTIVE", "false") + store.SetBool("LOCKBOX_INTERACTIVE", false) if _, err := config.NewKey(config.IgnoreKeyMode); err == nil || err.Error() != "ask key mode requested in non-interactive mode" { t.Errorf("invalid error: %v", err) } } func TestAskKey(t *testing.T) { - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "test") + store.Clear() + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"test"}) k, _ := config.NewKey(config.IgnoreKeyMode) if k.Ask() { t.Error("invalid ask key") } - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ask") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "") - t.Setenv("LOCKBOX_INTERACTIVE", "false") + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ask") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{}) + store.SetBool("LOCKBOX_INTERACTIVE", false) k, _ = config.NewKey(config.IgnoreKeyMode) if k.Ask() { t.Error("invalid ask key") } - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ask") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "") - t.Setenv("LOCKBOX_INTERACTIVE", "true") + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ask") + store.SetBool("LOCKBOX_INTERACTIVE", true) k, _ = config.NewKey(config.IgnoreKeyMode) if !k.Ask() { t.Error("invalid ask key") @@ -103,19 +103,21 @@ func TestAskKey(t *testing.T) { } func TestIgnoreKey(t *testing.T) { - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ignore") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "test") + store.Clear() + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ignore") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"test"}) if _, err := config.NewKey(config.IgnoreKeyMode); err != nil { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ignore") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "") + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ignore") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{}) if _, err := config.NewKey(config.IgnoreKeyMode); err != nil { t.Errorf("invalid error: %v", err) } } func TestReadErrors(t *testing.T) { + store.Clear() k := config.Key{} if _, err := k.Read(nil); err == nil || err.Error() != "invalid function given" { t.Errorf("invalid error: %v", err) @@ -129,8 +131,9 @@ func TestReadErrors(t *testing.T) { } func TestPlainKey(t *testing.T) { - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", " test ") + store.Clear() + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "plaintext") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{" test "}) k, err := config.NewKey(config.IgnoreKeyMode) if err != nil { t.Errorf("invalid error: %v", err) @@ -145,8 +148,9 @@ func TestPlainKey(t *testing.T) { } func TestReadIgnoreOrNoKey(t *testing.T) { - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ignore") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "test") + store.Clear() + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ignore") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"test"}) k, err := config.NewKey(config.IgnoreKeyMode) if err != nil { t.Errorf("invalid error: %v", err) @@ -158,8 +162,8 @@ func TestReadIgnoreOrNoKey(t *testing.T) { if err != nil || val != "" { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ignore") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "") + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "ignore") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{}) k, err = config.NewKey(config.IgnoreKeyMode) if err != nil { t.Errorf("invalid error: %v", err) @@ -168,7 +172,7 @@ func TestReadIgnoreOrNoKey(t *testing.T) { if err != nil || val != "" { t.Errorf("invalid error: %v", err) } - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "none") + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "none") k, err = config.NewKey(config.IgnoreKeyMode) if err != nil { t.Errorf("invalid error: %v", err) @@ -180,8 +184,9 @@ func TestReadIgnoreOrNoKey(t *testing.T) { } func TestCommandKey(t *testing.T) { - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "command") - t.Setenv("LOCKBOX_CREDENTIALS_PASSWORD", "thisisagarbagekey") + store.Clear() + store.SetString("LOCKBOX_CREDENTIALS_PASSWORD_MODE", "command") + store.SetArray("LOCKBOX_CREDENTIALS_PASSWORD", []string{"thisisagarbagekey"}) k, err := config.NewKey(config.IgnoreKeyMode) if err != nil { t.Errorf("invalid error: %v", err) diff --git a/internal/config/store/core.go b/internal/config/store/core.go @@ -0,0 +1,94 @@ +package store + +import ( + "slices" +) + +type backing struct { + integers map[string]int64 + strings map[string]string + booleans map[string]bool + arrays map[string][]string + all map[string]struct{} +} + +type KeyValue struct { + Key string + Value interface{} +} + +var configuration = newConfig() + +func newConfig() backing { + c := backing{} + c.arrays = make(map[string][]string) + c.integers = make(map[string]int64) + c.booleans = make(map[string]bool) + c.strings = make(map[string]string) + return c +} + +func Clear() { + configuration = newConfig() +} + +func List(filter ...string) []KeyValue { + var results []KeyValue + results = append(results, list(configuration.integers, GetInt64, filter)...) + results = append(results, list(configuration.booleans, GetBool, filter)...) + results = append(results, list(configuration.strings, GetString, filter)...) + results = append(results, list(configuration.arrays, GetArray, filter)...) + return results +} + +func list[T any](m map[string]T, conv func(string) (T, bool), filter []string) []KeyValue { + filtered := len(filter) > 0 + var result []KeyValue + for k := range m { + if filtered { + if !slices.Contains(filter, k) { + continue + } + } + val, _ := conv(k) + result = append(result, KeyValue{Key: k, Value: val}) + } + return result +} + +func GetInt64(key string) (int64, bool) { + return get(key, configuration.integers) +} + +func GetBool(key string) (bool, bool) { + return get(key, configuration.booleans) +} + +func GetString(key string) (string, bool) { + return get(key, configuration.strings) +} + +func GetArray(key string) ([]string, bool) { + return get(key, configuration.arrays) +} + +func get[T any](key string, m map[string]T) (T, bool) { + val, ok := m[key] + return val, ok +} + +func SetInt64(key string, val int64) { + configuration.integers[key] = val +} + +func SetBool(key string, val bool) { + configuration.booleans[key] = val +} + +func SetString(key string, val string) { + configuration.strings[key] = val +} + +func SetArray(key string, val []string) { + configuration.arrays[key] = val +} diff --git a/internal/config/store/core_test.go b/internal/config/store/core_test.go @@ -0,0 +1,111 @@ +package store_test + +import ( + "fmt" + "slices" + "strings" + "testing" + + "github.com/seanenck/lockbox/internal/config/store" +) + +func TestClear(t *testing.T) { + store.Clear() + store.SetString("abc", "abc") + store.SetBool("xyz", true) + store.SetArray("sss", []string{}) + store.SetInt64("aaa", 1) + if len(store.List()) != 4 { + t.Error("invalid list") + } + store.Clear() + if len(store.List()) != 0 { + t.Error("invalid list") + } +} + +func checkItem(keyValue store.KeyValue, key string, value string) error { + if keyValue.Key != key || fmt.Sprintf("%v", keyValue.Value) != value { + return fmt.Errorf("invalid value: %v", keyValue) + } + return nil +} + +func TestList(t *testing.T) { + store.Clear() + store.SetString("abc", "abc") + store.SetBool("xyz", true) + store.SetArray("sss", []string{}) + store.SetInt64("aaa", 1) + l := store.List() + if len(l) != 4 { + t.Error("invalid list") + } + slices.SortFunc(l, func(x, y store.KeyValue) int { + return strings.Compare(x.Key, y.Key) + }) + if err := checkItem(l[0], "aaa", "1"); err != nil { + t.Errorf("invalid error: %v", err) + } + if err := checkItem(l[1], "abc", "abc"); err != nil { + t.Errorf("invalid error: %v", err) + } + if err := checkItem(l[2], "sss", "[]"); err != nil { + t.Errorf("invalid error: %v", err) + } + if err := checkItem(l[3], "xyz", "true"); err != nil { + t.Errorf("invalid error: %v", err) + } +} + +func TestGetSetBool(t *testing.T) { + store.Clear() + store.SetBool("xyz", true) + val, ok := store.GetBool("xyz") + if !val || !ok { + t.Error("invalid get") + } + _, ok = store.GetBool("zzz") + if ok { + t.Error("invalid get") + } +} + +func TestGetSetString(t *testing.T) { + store.Clear() + store.SetString("xyz", "sss") + val, ok := store.GetString("xyz") + if val != "sss" || !ok { + t.Error("invalid get") + } + _, ok = store.GetString("zzz") + if ok { + t.Error("invalid get") + } +} + +func TestGetSetArray(t *testing.T) { + store.Clear() + store.SetArray("xyz", []string{"xyz", "xxx"}) + val, ok := store.GetArray("xyz") + if fmt.Sprintf("%v", val) != "[xyz xxx]" || !ok { + t.Error("invalid get") + } + _, ok = store.GetArray("zzz") + if ok { + t.Error("invalid get") + } +} + +func TestGetSetInt64(t *testing.T) { + store.Clear() + store.SetInt64("xyz", 1) + val, ok := store.GetInt64("xyz") + if val != 1 || !ok { + t.Error("invalid get") + } + _, ok = store.GetInt64("zzz") + if ok { + t.Error("invalid get") + } +} diff --git a/internal/config/toml.go b/internal/config/toml.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/BurntSushi/toml" + "github.com/seanenck/lockbox/internal/config/store" "github.com/seanenck/lockbox/internal/util" ) @@ -26,11 +27,6 @@ type ( tomlType string // Loader indicates how included files should be sourced Loader func(string) (io.Reader, error) - // ShellEnv is the output shell environment settings parsed from TOML config - ShellEnv struct { - Key string - Value string - } ) // DefaultTOML will load the internal, default TOML with additional comment markups @@ -86,8 +82,6 @@ func DefaultTOML() (string, error) { # include additional configs, allowing globs ('*'), nesting # depth allowed up to %d include levels # -# this field is not configurable via environment variables -# and it is not considered part of the environment either # it is ONLY used during TOML configuration loading %s = [] `, maxDepth, isInclude), "\n"} { @@ -128,10 +122,10 @@ func generateDetailText(data printer) (string, error) { t, _ := data.toml() var text []string for _, line := range []string{ - fmt.Sprintf("environment: %s", key), fmt.Sprintf("description:\n%s\n", description), fmt.Sprintf("requirement: %s", requirement), fmt.Sprintf("option: %s", strings.Join(allow, "|")), + fmt.Sprintf("env: %s", key), fmt.Sprintf("default: %s", value), fmt.Sprintf("type: %s", t), "", @@ -145,10 +139,10 @@ func generateDetailText(data printer) (string, error) { } // LoadConfig will read the input reader and use the loader to source configuration files -func LoadConfig(r io.Reader, loader Loader) ([]ShellEnv, error) { +func LoadConfig(r io.Reader, loader Loader) error { maps, err := readConfigs(r, 1, loader) if err != nil { - return nil, err + return err } m := make(map[string]interface{}) for _, config := range maps { @@ -156,56 +150,48 @@ func LoadConfig(r io.Reader, loader Loader) ([]ShellEnv, error) { m[k] = v } } - var res []ShellEnv for k, v := range m { export := environmentPrefix + strings.ToUpper(k) env, ok := registry[export] if !ok { - return nil, fmt.Errorf("unknown key: %s (%s)", k, export) + return fmt.Errorf("unknown key: %s (%s)", k, export) } - var value string isType, _ := env.toml() switch isType { case tomlArray: - array, err := parseStringArray(v) + array, err := parseStringArray(v, true) if err != nil { - return nil, err + return err } - value = strings.Join(array, " ") + store.SetArray(export, array) case tomlInt: i, ok := v.(int64) if !ok { - return nil, fmt.Errorf("non-int64 found where expected: %v", v) + return fmt.Errorf("non-int64 found where expected: %v", v) } if i < 0 { - return nil, fmt.Errorf("%d is negative (not allowed here)", i) + return fmt.Errorf("%d is negative (not allowed here)", i) } - value = fmt.Sprintf("%d", i) + store.SetInt64(export, i) case tomlBool: switch t := v.(type) { case bool: - if t { - value = yes - } else { - value = no - } + store.SetBool(export, t) default: - return nil, fmt.Errorf("non-bool found where expected: %v", v) + return fmt.Errorf("non-bool found where expected: %v", v) } case tomlString: s, ok := v.(string) if !ok { - return nil, fmt.Errorf("non-string found where expected: %v", v) + return fmt.Errorf("non-string found where expected: %v", v) } - value = s + store.SetString(export, os.Expand(s, os.Getenv)) default: - return nil, fmt.Errorf("unknown field, can't determine type: %s (%v)", k, v) + return 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 + return nil } func readConfigs(r io.Reader, depth int, loader Loader) ([]map[string]interface{}, error) { @@ -221,7 +207,7 @@ func readConfigs(r io.Reader, depth int, loader Loader) ([]map[string]interface{ includes, ok := m[isInclude] if ok { delete(m, isInclude) - including, err := parseStringArray(includes) + including, err := parseStringArray(includes, false) if err != nil { return nil, err } @@ -253,14 +239,18 @@ func readConfigs(r io.Reader, depth int, loader Loader) ([]map[string]interface{ return maps, nil } -func parseStringArray(value interface{}) ([]string, error) { +func parseStringArray(value interface{}, expand bool) ([]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) + val := s + if expand { + val = os.Expand(s, os.Getenv) + } + res = append(res, val) default: return nil, fmt.Errorf("value is not string in array: %v", item) } @@ -308,12 +298,5 @@ func LoadConfigFile(path string) error { 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 + return LoadConfig(reader, configLoader) } diff --git a/internal/config/toml_test.go b/internal/config/toml_test.go @@ -2,22 +2,24 @@ package config_test import ( "errors" + "fmt" "io" "os" "path/filepath" - "slices" "strings" "testing" "github.com/seanenck/lockbox/internal/config" + "github.com/seanenck/lockbox/internal/config/store" ) func TestLoadIncludes(t *testing.T) { + store.Clear() 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 err := config.LoadConfig(r, func(p string) (io.Reader, error) { if p == "xyz/abc" { return strings.NewReader("include = [\"$TEST/abc\"]"), nil } else { @@ -28,7 +30,7 @@ func TestLoadIncludes(t *testing.T) { } data = `include = ["abc"]` r = strings.NewReader(data) - if _, err := config.LoadConfig(r, func(p string) (io.Reader, error) { + if err := config.LoadConfig(r, func(p string) (io.Reader, error) { if p == "xyz/abc" { return strings.NewReader("include = [\"aaa\"]"), nil } else { @@ -39,7 +41,7 @@ func TestLoadIncludes(t *testing.T) { } data = `include = 1` r = strings.NewReader(data) - if _, err := config.LoadConfig(r, func(p string) (io.Reader, error) { + if err := config.LoadConfig(r, func(p string) (io.Reader, error) { if p == "xyz/abc" { return strings.NewReader("include = [\"aaa\"]"), nil } else { @@ -50,7 +52,7 @@ func TestLoadIncludes(t *testing.T) { } data = `include = [1]` r = strings.NewReader(data) - if _, err := config.LoadConfig(r, func(p string) (io.Reader, error) { + if err := config.LoadConfig(r, func(p string) (io.Reader, error) { if p == "xyz/abc" { return strings.NewReader("include = [\"aaa\"]"), nil } else { @@ -63,22 +65,26 @@ func TestLoadIncludes(t *testing.T) { store="xyz" ` r = strings.NewReader(data) - env, err := config.LoadConfig(r, func(p string) (io.Reader, error) { + if 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 { + }); 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) + if len(store.List()) != 1 { + t.Errorf("invalid store") + } + val, ok := store.GetString("LOCKBOX_STORE") + if val != "abc" || !ok { + t.Errorf("invalid object: %v", val) } } func TestArrayLoad(t *testing.T) { + store.Clear() defer os.Clearenv() t.Setenv("TEST", "abc") data := `store="xyz" @@ -86,7 +92,7 @@ func TestArrayLoad(t *testing.T) { copy_command = ["'xyz/$TEST'", "s", 1] ` r := strings.NewReader(data) - _, err := config.LoadConfig(r, func(p string) (io.Reader, error) { + 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" { @@ -98,17 +104,21 @@ store="xyz" copy_command = ["'xyz/$TEST'", "s"] ` r = strings.NewReader(data) - env, err := config.LoadConfig(r, func(p string) (io.Reader, error) { + if err := config.LoadConfig(r, func(p string) (io.Reader, error) { return nil, nil - }) - if err != nil { + }); 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_COMMAND" || env[0].Value != "'xyz/abc' s" { - t.Errorf("invalid object: %v", env) + if len(store.List()) != 2 { + t.Errorf("invalid store") + } + val, ok := store.GetString("LOCKBOX_STORE") + if val != "xyz" || !ok { + t.Errorf("invalid object: %v", val) + } + a, ok := store.GetArray("LOCKBOX_CLIP_COPY_COMMAND") + if fmt.Sprintf("%v", a) != "['xyz/abc' s]" || !ok { + t.Errorf("invalid object: %v", a) } data = `include = [] store="xyz" @@ -116,27 +126,32 @@ store="xyz" copy_command = ["'xyz/$TEST'", "s"] ` r = strings.NewReader(data) - env, err = config.LoadConfig(r, func(p string) (io.Reader, error) { + if err := config.LoadConfig(r, func(p string) (io.Reader, error) { return nil, nil - }) - if err != nil { + }); 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_COMMAND" || env[0].Value != "'xyz/abc' s" { - t.Errorf("invalid object: %v", env) + if len(store.List()) != 2 { + t.Errorf("invalid store") + } + val, ok = store.GetString("LOCKBOX_STORE") + if val != "xyz" || !ok { + t.Errorf("invalid object: %v", val) + } + a, ok = store.GetArray("LOCKBOX_CLIP_COPY_COMMAND") + if fmt.Sprintf("%v", a) != "['xyz/abc' s]" || !ok { + t.Errorf("invalid object: %v", val) } } func TestReadInt(t *testing.T) { + store.Clear() data := ` [clip] timeout = true ` r := strings.NewReader(data) - _, err := config.LoadConfig(r, func(p string) (io.Reader, error) { + err := config.LoadConfig(r, func(p string) (io.Reader, error) { return nil, nil }) if err == nil || err.Error() != "non-int64 found where expected: true" { @@ -147,21 +162,24 @@ timeout = true timeout = 1 ` r = strings.NewReader(data) - env, err := config.LoadConfig(r, func(p string) (io.Reader, error) { + if err := config.LoadConfig(r, func(p string) (io.Reader, error) { return nil, nil - }) - if err != nil { + }); err != nil { t.Errorf("invalid error: %v", err) } - if len(env) != 1 || env[0].Key != "LOCKBOX_CLIP_TIMEOUT" || env[0].Value != "1" { - t.Errorf("invalid object: %v", env) + if len(store.List()) != 1 { + t.Errorf("invalid store") + } + val, ok := store.GetInt64("LOCKBOX_CLIP_TIMEOUT") + if val != 1 || !ok { + t.Errorf("invalid object: %v", val) } data = `include = [] [clip] timeout = -1 ` r = strings.NewReader(data) - _, err = config.LoadConfig(r, func(p string) (io.Reader, error) { + err = config.LoadConfig(r, func(p string) (io.Reader, error) { return nil, nil }) if err == nil || err.Error() != "-1 is negative (not allowed here)" { @@ -170,6 +188,7 @@ timeout = -1 } func TestReadBool(t *testing.T) { + store.Clear() defer os.Clearenv() t.Setenv("TEST", "abc") data := ` @@ -177,7 +196,7 @@ func TestReadBool(t *testing.T) { enabled = 1 ` r := strings.NewReader(data) - _, err := config.LoadConfig(r, func(p string) (io.Reader, error) { + err := config.LoadConfig(r, func(p string) (io.Reader, error) { return nil, nil }) if err == nil || err.Error() != "non-bool found where expected: 1" { @@ -188,32 +207,39 @@ enabled = 1 enabled = true ` r = strings.NewReader(data) - env, err := config.LoadConfig(r, func(p string) (io.Reader, error) { + if err := config.LoadConfig(r, func(p string) (io.Reader, error) { return nil, nil - }) - if err != nil { + }); err != nil { t.Errorf("invalid error: %v", err) } - if len(env) != 1 || env[0].Key != "LOCKBOX_TOTP_ENABLED" || env[0].Value != "true" { - t.Errorf("invalid object: %v", env) + if len(store.List()) != 1 { + t.Errorf("invalid store") + } + val, ok := store.GetBool("LOCKBOX_TOTP_ENABLED") + if !val || !ok { + t.Errorf("invalid object: %v", val) } data = `include = [] [totp] enabled = false ` r = strings.NewReader(data) - env, err = config.LoadConfig(r, func(p string) (io.Reader, error) { + if err := config.LoadConfig(r, func(p string) (io.Reader, error) { return nil, nil - }) - if err != nil { + }); err != nil { t.Errorf("invalid error: %v", err) } - if len(env) != 1 || env[0].Key != "LOCKBOX_TOTP_ENABLED" || env[0].Value != "false" { - t.Errorf("invalid object: %v", env) + if len(store.List()) != 1 { + t.Errorf("invalid store") + } + val, ok = store.GetBool("LOCKBOX_TOTP_ENABLED") + if val || !ok { + t.Errorf("invalid object: %v", val) } } func TestBadValues(t *testing.T) { + store.Clear() defer os.Clearenv() t.Setenv("TEST", "abc") data := ` @@ -221,7 +247,7 @@ func TestBadValues(t *testing.T) { enabled = "false" ` r := strings.NewReader(data) - _, err := config.LoadConfig(r, func(p string) (io.Reader, error) { + 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)" { @@ -232,7 +258,7 @@ enabled = "false" otp_format = -1 ` r = strings.NewReader(data) - _, err = config.LoadConfig(r, func(p string) (io.Reader, error) { + err = config.LoadConfig(r, func(p string) (io.Reader, error) { return nil, nil }) if err == nil || err.Error() != "non-string found where expected: -1" { @@ -241,9 +267,9 @@ otp_format = -1 } func TestDefaultTOMLToLoadFile(t *testing.T) { + store.Clear() os.Mkdir("testdata", 0o755) defer os.RemoveAll("testdata") - defer os.Clearenv() file := filepath.Join("testdata", "config.toml") loaded, err := config.DefaultTOML() if err != nil { @@ -253,13 +279,7 @@ func TestDefaultTOMLToLoadFile(t *testing.T) { 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 != 30 { - t.Errorf("invalid environment after load: %d", count) + if len(store.List()) != 30 { + t.Errorf("invalid environment after load") } } diff --git a/internal/config/vars.go b/internal/config/vars.go @@ -237,17 +237,13 @@ and '%s' allows for multiple windows.`, util.TimeWindowSpan, util.TimeWindowDeli environmentDefault: newDefaultedEnvironment(detectEnvironment, environmentBase{ subKey: "CONFIG_TOML", - desc: fmt.Sprintf(`Allows setting a specific toml file to read and load into the environment. + desc: fmt.Sprintf(`Allows setting a specific toml file to read and load. -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. - -Note that this value is not output as part of the environment, nor -can it be set via TOML configuration.`, noEnvironment, detectEnvironment, strings.Join(xdgPaths, ","), strings.Join(homePaths, ",")), +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.`, detectEnvironment, strings.Join(xdgPaths, ","), strings.Join(homePaths, ",")), }), canDefault: true, - allowed: []string{detectEnvironment, fileExample, noEnvironment}, + allowed: []string{detectEnvironment, fileExample}, }) // EnvPasswordMode indicates how the password is read EnvPasswordMode = environmentRegister( diff --git a/internal/config/vars_test.go b/internal/config/vars_test.go @@ -5,38 +5,22 @@ import ( "testing" "github.com/seanenck/lockbox/internal/config" + "github.com/seanenck/lockbox/internal/config/store" ) func checkYesNo(key string, t *testing.T, obj config.EnvironmentBool, onEmpty bool) { - t.Setenv(key, "true") - c, err := obj.Get() - if err != nil { - t.Errorf("invalid error: %v", err) - } - if !c { + store.Clear() + if obj.Get() != onEmpty { t.Error("invalid setting") } - t.Setenv(key, "") - c, err = obj.Get() - if err != nil { - t.Errorf("invalid error: %v", err) - } - if c != onEmpty { + store.SetBool(key, true) + if !obj.Get() { t.Error("invalid setting") } - t.Setenv(key, "false") - c, err = obj.Get() - if err != nil { - t.Errorf("invalid error: %v", err) - } - if c { + store.SetBool(key, false) + if obj.Get() { t.Error("invalid setting") } - t.Setenv(key, "afoieae") - _, err = obj.Get() - if err == nil || err.Error() != fmt.Sprintf("invalid yes/no env value for %s", key) { - t.Errorf("unexpected error: %v", err) - } } func TestColorSetting(t *testing.T) { @@ -76,17 +60,18 @@ func TestIsTitle(t *testing.T) { } func TestTOTP(t *testing.T) { - t.Setenv("LOCKBOX_TOTP_ENTRY", "abc") - if config.EnvTOTPEntry.Get() != "abc" { + store.Clear() + if config.EnvTOTPEntry.Get() != "totp" { t.Error("invalid totp token field") } - t.Setenv("LOCKBOX_TOTP_ENTRY", "") - if config.EnvTOTPEntry.Get() != "totp" { + store.SetString("LOCKBOX_TOTP_ENTRY", "abc") + if config.EnvTOTPEntry.Get() != "abc" { t.Error("invalid totp token field") } } func TestFormatTOTP(t *testing.T) { + store.Clear() otp := config.EnvTOTPFormat.Get("otpauth://abc") if otp != "otpauth://abc" { t.Errorf("invalid totp token: %s", otp) @@ -95,14 +80,13 @@ func TestFormatTOTP(t *testing.T) { if otp != "otpauth://totp/lbissuer:lbaccount?algorithm=SHA1&digits=6&issuer=lbissuer&period=30&secret=abc" { t.Errorf("invalid totp token: %s", otp) } - t.Setenv("LOCKBOX_TOTP_OTP_FORMAT", "test/%s") otp = config.EnvTOTPFormat.Get("abc") - if otp != "test/abc" { + if otp != "otpauth://totp/lbissuer:lbaccount?algorithm=SHA1&digits=6&issuer=lbissuer&period=30&secret=abc" { t.Errorf("invalid totp token: %s", otp) } - t.Setenv("LOCKBOX_TOTP_OTP_FORMAT", "") + store.SetString("LOCKBOX_TOTP_OTP_FORMAT", "test/%s") otp = config.EnvTOTPFormat.Get("abc") - if otp != "otpauth://totp/lbissuer:lbaccount?algorithm=SHA1&digits=6&issuer=lbissuer&period=30&secret=abc" { + if otp != "test/abc" { t.Errorf("invalid totp token: %s", otp) } } @@ -123,18 +107,18 @@ func TestWordCount(t *testing.T) { checkInt(config.EnvPasswordGenWordCount, "LOCKBOX_PWGEN_WORD_COUNT", "word count", 8, false, t) } -func checkInt(e config.EnvironmentInt, key, text string, def int, allowZero bool, t *testing.T) { - t.Setenv(key, "") +func checkInt(e config.EnvironmentInt, key, text string, def int64, allowZero bool, t *testing.T) { + store.Clear() val, err := e.Get() if err != nil || val != def { t.Error("invalid read") } - t.Setenv(key, "1") + store.SetInt64(key, 1) val, err = e.Get() if err != nil || val != 1 { t.Error("invalid read") } - t.Setenv(key, "-1") + store.SetInt64(key, -1) zero := "" if allowZero { zero = "=" @@ -142,11 +126,7 @@ func checkInt(e config.EnvironmentInt, key, text string, def int, allowZero bool if _, err := e.Get(); err == nil || err.Error() != fmt.Sprintf("%s must be >%s 0", text, zero) { t.Errorf("invalid err: %v", err) } - t.Setenv(key, "alk;ja") - if _, err := e.Get(); err == nil || err.Error() != "strconv.Atoi: parsing \"alk;ja\": invalid syntax" { - t.Errorf("invalid err: %v", err) - } - t.Setenv(key, "0") + store.SetInt64(key, 0) if allowZero { val, err = e.Get() if err != nil || val != 0 { diff --git a/internal/platform/clip/core.go b/internal/platform/clip/core.go @@ -17,7 +17,7 @@ type ( Board struct { copying []string pasting []string - MaxTime int + MaxTime int64 isOSC52 bool } ) @@ -32,29 +32,17 @@ func newBoard(copying, pasting []string) (Board, error) { // New will retrieve the commands to use for clipboard operations. func New() (Board, error) { - canClip, err := config.EnvClipEnabled.Get() - if err != nil { - return Board{}, err - } - if !canClip { + if !config.EnvClipEnabled.Get() { return Board{}, errors.New("clipboard is off") } - overridePaste, err := config.EnvClipPaste.Get() - if err != nil { - return Board{}, err - } - overrideCopy, err := config.EnvClipCopy.Get() - if err != nil { - return Board{}, err - } - if overrideCopy != nil && overridePaste != nil { + overridePaste := config.EnvClipPaste.Get() + overrideCopy := config.EnvClipCopy.Get() + setPaste := len(overridePaste) > 0 + setCopy := len(overrideCopy) > 0 + if setPaste && setCopy { return newBoard(overrideCopy, overridePaste) } - isOSC, err := config.EnvClipOSC52.Get() - if err != nil { - return Board{}, err - } - if isOSC { + if config.EnvClipOSC52.Get() { c := Board{isOSC52: true} return c, nil } @@ -81,10 +69,10 @@ func New() (Board, error) { default: return Board{}, errors.New("clipboard is unavailable") } - if overridePaste != nil { + if setPaste { pasting = overridePaste } - if overrideCopy != nil { + if setCopy { copying = overrideCopy } return newBoard(copying, pasting) diff --git a/internal/platform/clip/core_test.go b/internal/platform/clip/core_test.go @@ -3,14 +3,16 @@ package clip_test import ( "testing" + "github.com/seanenck/lockbox/internal/config/store" "github.com/seanenck/lockbox/internal/platform" "github.com/seanenck/lockbox/internal/platform/clip" ) func TestNoClipboard(t *testing.T) { - t.Setenv("LOCKBOX_CLIP_OSC52", "false") - t.Setenv("LOCKBOX_CLIP_TIMEOUT", "") - t.Setenv("LOCKBOX_CLIP_ENABLED", "false") + store.Clear() + defer store.Clear() + store.SetBool("LOCKBOX_CLIP_OSC52", false) + store.SetBool("LOCKBOX_CLIP_ENABLED", false) _, err := clip.New() if err == nil || err.Error() != "clipboard is off" { t.Errorf("invalid error: %v", err) @@ -18,10 +20,11 @@ func TestNoClipboard(t *testing.T) { } func TestMaxTime(t *testing.T) { - t.Setenv("LOCKBOX_CLIP_ENABLED", "true") - t.Setenv("LOCKBOX_CLIP_OSC52", "false") - t.Setenv("LOCKBOX_PLATFORM", string(platform.Systems.LinuxWaylandSystem)) - t.Setenv("LOCKBOX_CLIP_TIMEOUT", "") + store.Clear() + defer store.Clear() + store.SetBool("LOCKBOX_CLIP_OSC52", false) + store.SetBool("LOCKBOX_CLIP_ENABLED", true) + store.SetString("LOCKBOX_PLATFORM", string(platform.Systems.LinuxWaylandSystem)) c, err := clip.New() if err != nil { t.Errorf("invalid clipboard: %v", err) @@ -29,7 +32,7 @@ func TestMaxTime(t *testing.T) { if c.MaxTime != 45 { t.Error("invalid default") } - t.Setenv("LOCKBOX_CLIP_TIMEOUT", "1") + store.SetInt64("LOCKBOX_CLIP_TIMEOUT", 1) c, err = clip.New() if err != nil { t.Errorf("invalid clipboard: %v", err) @@ -37,24 +40,20 @@ func TestMaxTime(t *testing.T) { if c.MaxTime != 1 { t.Error("invalid default") } - t.Setenv("LOCKBOX_CLIP_TIMEOUT", "-1") + store.SetInt64("LOCKBOX_CLIP_TIMEOUT", -1) _, err = clip.New() if err == nil || err.Error() != "clipboard max time must be > 0" { t.Errorf("invalid max time error: %v", err) } - t.Setenv("LOCKBOX_CLIP_TIMEOUT", "$&(+") - _, err = clip.New() - if err == nil || err.Error() != "strconv.Atoi: parsing \"$&(+\": invalid syntax" { - t.Errorf("invalid max time error: %v", err) - } } func TestClipboardInstances(t *testing.T) { - t.Setenv("LOCKBOX_CLIP_ENABLED", "true") - t.Setenv("LOCKBOX_CLIP_TIMEOUT", "") - t.Setenv("LOCKBOX_CLIP_OSC52", "false") + store.Clear() + defer store.Clear() + store.SetBool("LOCKBOX_CLIP_OSC52", false) + store.SetBool("LOCKBOX_CLIP_ENABLED", true) for _, item := range platform.Systems.List() { - t.Setenv("LOCKBOX_PLATFORM", item) + store.SetString("LOCKBOX_PLATFORM", item) _, err := clip.New() if err != nil { t.Errorf("invalid clipboard: %v", err) @@ -63,7 +62,9 @@ func TestClipboardInstances(t *testing.T) { } func TestOSC52(t *testing.T) { - t.Setenv("LOCKBOX_CLIP_OSC52", "true") + store.Clear() + defer store.Clear() + store.SetBool("LOCKBOX_CLIP_OSC52", true) c, _ := clip.New() _, _, ok := c.Args(true) if ok { @@ -76,10 +77,14 @@ func TestOSC52(t *testing.T) { } func TestArgsOverride(t *testing.T) { - t.Setenv("LOCKBOX_CLIP_PASTE_COMMAND", "abc xyz 111") - t.Setenv("LOCKBOX_CLIP_OSC52", "false") - t.Setenv("LOCKBOX_PLATFORM", string(platform.Systems.WindowsLinuxSystem)) - c, _ := clip.New() + store.Clear() + defer store.Clear() + store.SetArray("LOCKBOX_CLIP_PASTE_COMMAND", []string{"abc", "xyz", "111"}) + store.SetString("LOCKBOX_PLATFORM", string(platform.Systems.WindowsLinuxSystem)) + c, err := clip.New() + if err != nil { + t.Errorf("invalid error: %v", err) + } cmd, args, ok := c.Args(true) if cmd != "clip.exe" || len(args) != 0 || !ok { t.Error("invalid parse") @@ -88,8 +93,11 @@ func TestArgsOverride(t *testing.T) { if cmd != "abc" || len(args) != 2 || args[0] != "xyz" || args[1] != "111" || !ok { t.Error("invalid parse") } - t.Setenv("LOCKBOX_CLIP_COPY_COMMAND", "zzz lll 123") - c, _ = clip.New() + store.SetArray("LOCKBOX_CLIP_COPY_COMMAND", []string{"zzz", "lll", "123"}) + c, err = clip.New() + if err != nil { + t.Errorf("invalid error: %v", err) + } cmd, args, ok = c.Args(true) if cmd != "zzz" || len(args) != 2 || args[0] != "lll" || args[1] != "123" || !ok { t.Error("invalid parse") @@ -98,9 +106,12 @@ func TestArgsOverride(t *testing.T) { if cmd != "abc" || len(args) != 2 || args[0] != "xyz" || args[1] != "111" || !ok { t.Error("invalid parse") } - t.Setenv("LOCKBOX_CLIP_PASTE_COMMAND", "") - t.Setenv("LOCKBOX_CLIP_COPY_COMMAND", "") - c, _ = clip.New() + store.Clear() + store.SetString("LOCKBOX_PLATFORM", string(platform.Systems.WindowsLinuxSystem)) + c, err = clip.New() + if err != nil { + t.Errorf("invalid error: %v", err) + } cmd, args, ok = c.Args(true) if cmd != "clip.exe" || len(args) != 0 || !ok { t.Error("invalid parse") diff --git a/internal/platform/core.go b/internal/platform/core.go @@ -64,11 +64,11 @@ func NewSystem(candidate string) (System, error) { } if strings.TrimSpace(os.Getenv("WAYLAND_DISPLAY")) == "" { if strings.TrimSpace(os.Getenv("DISPLAY")) == "" { - return unknownSystem, errors.New("unable to detect linux clipboard mode") + return unknownSystem, errors.New("unable to detect linux system") } return Systems.LinuxXSystem, nil } return Systems.LinuxWaylandSystem, nil } - return unknownSystem, errors.New("unable to detect clipboard mode") + return unknownSystem, errors.New("unable to detect system") } diff --git a/justfile b/justfile @@ -11,10 +11,12 @@ build: go build {{goflags}} -ldflags "{{ldflags}} -X main.version={{version}}" -o "{{object}}" cmd/main.go unittest: - LOCKBOX_CONFIG_TOML=none go test ./... + LOCKBOX_CONFIG_TOML= go test ./... -check: unittest build - cd tests && LOCKBOX_CONFIG_TOML=none ./run.sh +check: unittest tests + +tests: build + cd tests && LOCKBOX_CONFIG_TOML= ./run.sh clean: rm -f "{{object}}"