lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 9ea2cbe38d3579a60abc9d0dad5d7dbe049d681b
parent d6c74ab155c60d431d0658edabe199f475c61ccf
Author: Sean Enck <sean@ttypty.com>
Date:   Sat,  7 Dec 2024 11:01:13 -0500

toml+env can now layout full structure

Diffstat:
Minternal/app/core.go | 2--
Dinternal/config/config.toml | 48------------------------------------------------
Minternal/config/core.go | 1+
Minternal/config/env.go | 24++++++++++++++++++++++++
Minternal/config/toml.go | 153+++++++++++++++++++++++++++++++------------------------------------------------
Minternal/config/toml_test.go | 4++--
Minternal/config/vars.go | 2++
Mtests/run.sh | 2+-
8 files changed, 89 insertions(+), 147 deletions(-)

diff --git a/internal/app/core.go b/internal/app/core.go @@ -114,7 +114,6 @@ type ( ReKeyCommand string CompletionsCommand string CompletionsEnv string - ExampleTOML string ReKey struct { KeyFile string NoKey string @@ -252,7 +251,6 @@ func Usage(verbose bool, exe string) ([]string, error) { ReKeyCommand: ReKeyCommand, CompletionsCommand: CompletionsCommand, CompletionsEnv: config.EnvDefaultCompletionKey, - ExampleTOML: config.ExampleTOML, } document.ReKey.KeyFile = setDocFlag(reKeyFlags.KeyFile) document.ReKey.NoKey = reKeyFlags.NoKey diff --git a/internal/config/config.toml b/internal/config/config.toml @@ -1,48 +0,0 @@ -language = "" -platform = "" -readonly = false -store = "" -include = [] -interactive = true - -[defaults] -modtime = "" -completion = true - -[color] -enabled = true - -[clip] -copy_command = [] -paste_command = [] -timeout = 0 -osc52 = false -enabled = true - -[hooks] -enabled = true -directory = "" - -[json] -mode = "" -hash_length = 0 - -[credentials] -key_file = "" -password_mode = "" -password = [] - -[pwgen] -enabled = true -characters = "" -word_count = 0 -template = "" -title = true -words_command = [] - -[totp] -enabled = true -entry = "" -color_windows = [] -otp_format = "" -timeout = 0 diff --git a/internal/config/core.go b/internal/config/core.go @@ -70,6 +70,7 @@ type ( printer interface { values() (string, []string) self() environmentBase + toml() (tomlType, string) } ) diff --git a/internal/config/env.go b/internal/config/env.go @@ -34,6 +34,7 @@ type ( environmentDefault[string] canDefault bool allowed []string + isArray bool } // EnvironmentCommand are settings that are parsed as shell commands EnvironmentCommand struct { @@ -164,3 +165,26 @@ func (e EnvironmentFormatter) values() (string, []string) { func (e EnvironmentCommand) values() (string, []string) { return detectedValue, []string{commandArgsExample} } + +func (e EnvironmentInt) toml() (tomlType, string) { + return tomlInt, "0" +} + +func (e EnvironmentBool) toml() (tomlType, string) { + return tomlBool, "true" +} + +func (e EnvironmentString) toml() (tomlType, string) { + if e.isArray { + return tomlArray, "[]" + } + return tomlString, "\"\"" +} + +func (e EnvironmentCommand) toml() (tomlType, string) { + return tomlArray, "[]" +} + +func (e EnvironmentFormatter) toml() (tomlType, string) { + return tomlString, "\"\"" +} diff --git a/internal/config/toml.go b/internal/config/toml.go @@ -2,12 +2,10 @@ package config import ( "bytes" - _ "embed" "fmt" "io" "os" "path/filepath" - "slices" "sort" "strings" @@ -15,69 +13,37 @@ import ( ) const ( - isInclude = "include" - maxDepth = 10 + isInclude = "include" + maxDepth = 10 + tomlInt = "integer" + tomlBool = "boolean" + tomlString = "string" + tomlArray = "[]string" ) 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 - raw string - } -) - -var ( - // ExampleTOML is an example TOML file of viable fields - //go:embed "config.toml" - ExampleTOML string - arrayTypes = []string{ - EnvClipCopy.Key(), - EnvClipPaste.Key(), - EnvPasswordGenWordList.Key(), - envPassword.Key(), - EnvTOTPColorBetween.Key(), - } - intTypes = []string{ - EnvClipTimeout.Key(), - EnvTOTPTimeout.Key(), - EnvJSONHashLength.Key(), - EnvPasswordGenWordCount.Key(), - } - boolTypes = []string{ - EnvClipOSC52.Key(), - EnvClipEnabled.Key(), - EnvTOTPEnabled.Key(), - EnvColorEnabled.Key(), - EnvHooksEnabled.Key(), - EnvPasswordGenEnabled.Key(), - EnvPasswordGenTitle.Key(), - EnvReadOnly.Key(), - EnvInteractive.Key(), - EnvDefaultCompletion.Key(), - } - reverseMap = map[string][]string{ - "[]": arrayTypes, - "0": intTypes, - "true": boolTypes, } ) // DefaultTOML will load the internal, default TOML with additional comment markups func DefaultTOML() (string, error) { - s, err := LoadConfig(strings.NewReader(ExampleTOML), nil) - if err != nil { - return "", err - } const root = "_root_" unmapped := make(map[string][]string) keys := []string{} - for _, item := range s { - raw := item.raw - parts := strings.Split(raw, "_") + isConfig := EnvConfig.Key() + for envKey, item := range registry { + if envKey == isConfig { + continue + } + tomlKey := strings.ToLower(strings.TrimPrefix(envKey, environmentPrefix)) + parts := strings.Split(tomlKey, "_") length := len(parts) if length == 0 { return "", fmt.Errorf("invalid internal TOML structure: %v", item) @@ -93,14 +59,8 @@ func DefaultTOML() (string, error) { default: sub = strings.Join(parts[1:], "_") } - field := "\"\"" - for to, fromKey := range reverseMap { - if slices.Contains(fromKey, item.Key) { - field = to - break - } - } - text, err := generateDetailText(item.Key) + _, field := item.toml() + text, err := generateDetailText(item) if err != nil { return "", err } @@ -117,7 +77,7 @@ func DefaultTOML() (string, error) { } sort.Strings(keys) builder := strings.Builder{} - configEnv, err := generateDetailText(EnvConfig.Key()) + configEnv, err := generateDetailText(EnvConfig) if err != nil { return "", err } @@ -151,24 +111,22 @@ func DefaultTOML() (string, error) { return builder.String(), nil } -func generateDetailText(key string) (string, error) { - data, ok := registry[key] - if !ok { - return "", fmt.Errorf("unexpected configuration key has no environment settings: %s", key) - } +func generateDetailText(data printer) (string, error) { env := data.self() value, allow := data.values() if len(value) == 0 { value = "(unset)" } + key := env.Key() description := strings.TrimSpace(Wrap(2, env.desc)) requirement := "optional/default" r := strings.TrimSpace(env.requirement) if r != "" { requirement = r } + t, _ := data.toml() var text []string - for _, line := range []string{fmt.Sprintf("environment: %s", key), fmt.Sprintf("description:\n%s\n", description), fmt.Sprintf("default: %s", requirement), fmt.Sprintf("option: %s", strings.Join(allow, "|")), fmt.Sprintf("default: %s", value)} { + 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("default: %s", value), fmt.Sprintf("toml: %s", t)} { for _, comment := range strings.Split(line, "\n") { text = append(text, fmt.Sprintf("# %s", comment)) } @@ -191,43 +149,50 @@ func LoadConfig(r io.Reader, loader Loader) ([]ShellEnv, error) { var res []ShellEnv for k, v := range m { export := environmentPrefix + strings.ToUpper(k) - if _, ok := registry[export]; !ok { + env, ok := registry[export] + if !ok { return nil, fmt.Errorf("unknown key: %s (%s)", k, export) } - value, ok := v.(string) - if !ok { - if slices.Contains(arrayTypes, export) { - array, err := parseStringArray(v) - if err != nil { - return nil, err - } - value = strings.Join(array, " ") - } else if slices.Contains(intTypes, export) { - i, ok := v.(int64) - if !ok { - return nil, fmt.Errorf("non-int64 found where expected: %v", v) - } - if i < 0 { - return nil, fmt.Errorf("%d is negative (not allowed here)", i) - } - value = fmt.Sprintf("%d", i) - } else if slices.Contains(boolTypes, export) { - switch t := v.(type) { - case bool: - if t { - value = yes - } else { - value = no - } - default: - return nil, fmt.Errorf("non-bool found where expected: %v", v) + var value string + isType, _ := env.toml() + switch isType { + case tomlArray: + array, err := parseStringArray(v) + if err != nil { + return nil, err + } + value = strings.Join(array, " ") + case tomlInt: + i, ok := v.(int64) + if !ok { + return nil, fmt.Errorf("non-int64 found where expected: %v", v) + } + if i < 0 { + return nil, fmt.Errorf("%d is negative (not allowed here)", i) + } + value = fmt.Sprintf("%d", i) + case tomlBool: + switch t := v.(type) { + case bool: + if t { + value = yes + } else { + value = no } - } else { - return nil, fmt.Errorf("unknown field, can't determine type: %s (%v)", k, v) + default: + return nil, 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) } + value = s + default: + return nil, fmt.Errorf("unknown field, can't determine type: %s (%v)", k, v) } value = os.Expand(value, os.Getenv) - res = append(res, ShellEnv{Key: export, Value: value, raw: k}) + res = append(res, ShellEnv{Key: export, Value: value}) } return res, nil diff --git a/internal/config/toml_test.go b/internal/config/toml_test.go @@ -113,7 +113,7 @@ copy_command = ["'xyz/$TEST'", "s"] data = `include = [] store="xyz" [clip] -copy_command = "'xyz/$TEST' s" +copy_command = ["'xyz/$TEST'", "s"] ` r = strings.NewReader(data) env, err = config.LoadConfig(r, func(p string) (io.Reader, error) { @@ -235,7 +235,7 @@ otp_format = -1 _, err = config.LoadConfig(r, func(p string) (io.Reader, error) { return nil, nil }) - if err == nil || err.Error() != "unknown field, can't determine type: totp_otp_format (-1)" { + if err == nil || err.Error() != "non-string found where expected: -1" { t.Errorf("invalid error: %v", err) } } diff --git a/internal/config/vars.go b/internal/config/vars.go @@ -194,6 +194,7 @@ var ( must be a list of one (or more) rules where a '%s' delimits the start and end second (0-60 for each), and '%s' allows for multiple windows.`, core.ColorWindowSpan, core.ColorWindowDelimiter), }), + isArray: true, canDefault: true, allowed: exampleColorWindows, }) @@ -283,6 +284,7 @@ Set to '%s' to ignore the set key value`, noKeyMode, IgnoreKeyMode), plainKeyMode, commandKeyMode), }), + isArray: true, allowed: []string{commandArgsExample, "password"}, canDefault: false, }) diff --git a/tests/run.sh b/tests/run.sh @@ -235,7 +235,7 @@ hash_length = $LOCKBOX_JSON_HASH_LENGTH [credentials] key_file = "$LOCKBOX_CREDENTIALS_KEY_FILE" password_mode = "$LOCKBOX_CREDENTIALS_PASSWORD_MODE" -password = "$LOCKBOX_CREDENTIALS_PASSWORD" +password = ["$LOCKBOX_CREDENTIALS_PASSWORD"] EOF } > "$TOML" _unset