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