commit 6137c51463c9aa97d07cd7e53e2c499b5450e213
parent a2259cdf6a0abfc84c6f2330b9caecec1f22cba2
Author: Sean Enck <sean@ttypty.com>
Date: Sun, 8 Dec 2024 11:21:18 -0500
eliminate aggressive os.Expand, allow templates and other strings to behave normally without env involved now
Diffstat:
11 files changed, 116 insertions(+), 49 deletions(-)
diff --git a/internal/app/pwgen.go b/internal/app/pwgen.go
@@ -12,6 +12,7 @@ import (
"text/template"
"github.com/seanenck/lockbox/internal/config"
+ "github.com/seanenck/lockbox/internal/util"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
@@ -30,7 +31,6 @@ func GeneratePassword(cmd CommandOptions) error {
return fmt.Errorf("word count must be >= 1")
}
tmplString := config.EnvPasswordGenTemplate.Get()
- tmplString = strings.ReplaceAll(tmplString, config.TemplateVariable, "$")
wordList := config.EnvPasswordGenWordList.Get()
if len(wordList) == 0 {
return errors.New("word list command must set")
@@ -82,25 +82,17 @@ func GeneratePassword(cmd CommandOptions) error {
}
choices = append(choices, use)
}
- type position struct {
- Start int
- End int
- }
- type word struct {
- Text string
- Position position
- }
found := len(choices)
if found == 0 {
return errors.New("no sources given")
}
- var selected []word
+ var selected []util.Word
var cnt int64
totalLength := 0
for cnt < length {
choice := choices[rand.Intn(found)]
textLength := len(choice)
- selected = append(selected, word{choice, position{totalLength, totalLength + textLength}})
+ selected = append(selected, util.Word{Text: choice, Position: util.Position{Start: totalLength, End: totalLength + textLength}})
totalLength += textLength
cnt++
}
diff --git a/internal/app/pwgen_test.go b/internal/app/pwgen_test.go
@@ -90,7 +90,7 @@ func TestGenerate(t *testing.T) {
// NOTE: this allows templating below in golang
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")
- store.SetString("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")
store.Clear()
store.SetBool("LOCKBOX_PWGEN_TITLE", true)
diff --git a/internal/config/core.go b/internal/config/core.go
@@ -16,19 +16,17 @@ import (
const (
// sub categories
- clipCategory keyCategory = "CLIP_"
- totpCategory keyCategory = "TOTP_"
- genCategory keyCategory = "PWGEN_"
- jsonCategory keyCategory = "JSON_"
- credsCategory keyCategory = "CREDENTIALS_"
- defaultCategory keyCategory = "DEFAULTS_"
- hookCategory keyCategory = "HOOKS_"
- // TemplateVariable is used to handle '$' in shell vars (due to expansion)
- TemplateVariable = "[%]"
- environmentPrefix = "LOCKBOX_"
- commandArgsExample = "[cmd args...]"
- fileExample = "<file>"
- requiredKeyOrKeyFile = "a key, a key file, or both must be set"
+ clipCategory keyCategory = "CLIP_"
+ totpCategory keyCategory = "TOTP_"
+ genCategory keyCategory = "PWGEN_"
+ jsonCategory keyCategory = "JSON_"
+ credsCategory keyCategory = "CREDENTIALS_"
+ defaultCategory keyCategory = "DEFAULTS_"
+ hookCategory keyCategory = "HOOKS_"
+ environmentPrefix = "LOCKBOX_"
+ commandArgsExample = "[cmd args...]"
+ fileExample = "<file>"
+ requiredKeyOrKeyFile = "a key, a key file, or both must be set"
// ModTimeFormat is the expected modtime format
ModTimeFormat = time.RFC3339
exampleColorWindow = "start" + util.TimeWindowSpan + "end"
@@ -66,7 +64,7 @@ type (
printer interface {
values() (string, []string)
self() environmentBase
- toml() (tomlType, string)
+ toml() (tomlType, string, bool)
}
)
diff --git a/internal/config/env.go b/internal/config/env.go
@@ -35,6 +35,7 @@ type (
canDefault bool
allowed []string
isArray bool
+ expand bool
}
// EnvironmentCommand are settings that are parsed as shell commands
EnvironmentCommand struct {
@@ -138,25 +139,25 @@ func (e EnvironmentCommand) values() (string, []string) {
return detectedValue, []string{commandArgsExample}
}
-func (e EnvironmentInt) toml() (tomlType, string) {
- return tomlInt, "0"
+func (e EnvironmentInt) toml() (tomlType, string, bool) {
+ return tomlInt, "0", false
}
-func (e EnvironmentBool) toml() (tomlType, string) {
- return tomlBool, "true"
+func (e EnvironmentBool) toml() (tomlType, string, bool) {
+ return tomlBool, YesValue, false
}
-func (e EnvironmentString) toml() (tomlType, string) {
+func (e EnvironmentString) toml() (tomlType, string, bool) {
if e.isArray {
- return tomlArray, "[]"
+ return tomlArray, "[]", e.expand
}
- return tomlString, "\"\""
+ return tomlString, "\"\"", e.expand
}
-func (e EnvironmentCommand) toml() (tomlType, string) {
- return tomlArray, "[]"
+func (e EnvironmentCommand) toml() (tomlType, string, bool) {
+ return tomlArray, "[]", true
}
-func (e EnvironmentFormatter) toml() (tomlType, string) {
- return tomlString, "\"\""
+func (e EnvironmentFormatter) toml() (tomlType, string, bool) {
+ return tomlString, "\"\"", false
}
diff --git a/internal/config/toml.go b/internal/config/toml.go
@@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"sort"
+ "strconv"
"strings"
"github.com/BurntSushi/toml"
@@ -53,7 +54,7 @@ func DefaultTOML() (string, error) {
default:
sub = strings.Join(parts[1:], "_")
}
- _, field := item.toml()
+ _, field, _ := item.toml()
text, err := generateDetailText(item)
if err != nil {
return "", err
@@ -112,7 +113,7 @@ func generateDetailText(data printer) (string, error) {
if r != "" {
requirement = r
}
- t, _ := data.toml()
+ t, _, expands := data.toml()
var text []string
for _, line := range []string{
fmt.Sprintf("description:\n%s\n", description),
@@ -120,6 +121,7 @@ func generateDetailText(data printer) (string, error) {
fmt.Sprintf("option: %s", strings.Join(allow, "|")),
fmt.Sprintf("%s name: %s", commands.Env, key),
fmt.Sprintf("default: %s", value),
+ fmt.Sprintf("expands: %s", strconv.FormatBool(expands)),
fmt.Sprintf("type: %s", t),
"",
"NOTE: the following value is NOT a default, it is an empty TOML placeholder",
@@ -149,10 +151,10 @@ func LoadConfig(r io.Reader, loader Loader) error {
if !ok {
return fmt.Errorf("unknown key: %s (%s)", k, export)
}
- isType, _ := env.toml()
+ isType, _, expand := env.toml()
switch isType {
case tomlArray:
- array, err := parseStringArray(v, true)
+ array, err := parseStringArray(v, expand)
if err != nil {
return err
}
@@ -178,7 +180,10 @@ func LoadConfig(r io.Reader, loader Loader) error {
if !ok {
return fmt.Errorf("non-string found where expected: %v", v)
}
- store.SetString(export, os.Expand(s, os.Getenv))
+ if expand {
+ s = os.Expand(s, os.Getenv)
+ }
+ store.SetString(export, s)
default:
return fmt.Errorf("unknown field, can't determine type: %s (%v)", k, v)
}
diff --git a/internal/config/toml_test.go b/internal/config/toml_test.go
@@ -189,8 +189,6 @@ timeout = -1
func TestReadBool(t *testing.T) {
store.Clear()
- defer os.Clearenv()
- t.Setenv("TEST", "abc")
data := `
[totp]
enabled = 1
@@ -240,8 +238,6 @@ enabled = false
func TestBadValues(t *testing.T) {
store.Clear()
- defer os.Clearenv()
- t.Setenv("TEST", "abc")
data := `
[totsp]
enabled = "false"
@@ -283,3 +279,35 @@ func TestDefaultTOMLToLoadFile(t *testing.T) {
t.Errorf("invalid environment after load")
}
}
+
+func TestExpands(t *testing.T) {
+ store.Clear()
+ t.Setenv("TEST", "1")
+ data := `include = []
+store = "$TEST"
+clip.copy_command = ["$TEST", "$TEST"]
+[totp]
+otp_format = "$TEST"
+`
+ r := strings.NewReader(data)
+ if err := config.LoadConfig(r, func(p string) (io.Reader, error) {
+ return nil, nil
+ }); err != nil {
+ t.Errorf("invalid error: %v", err)
+ }
+ if len(store.List()) != 3 {
+ t.Errorf("invalid store")
+ }
+ val, ok := store.GetString("LOCKBOX_TOTP_OTP_FORMAT")
+ if val != "$TEST" || !ok {
+ t.Errorf("invalid object: %v", val)
+ }
+ val, ok = store.GetString("LOCKBOX_STORE")
+ if val != "1" || !ok {
+ t.Errorf("invalid object: %v", val)
+ }
+ a, ok := store.GetArray("LOCKBOX_CLIP_COPY_COMMAND")
+ if fmt.Sprintf("%v", a) != "[1 1]" || !ok {
+ t.Errorf("invalid object: %v", a)
+ }
+}
diff --git a/internal/config/vars.go b/internal/config/vars.go
@@ -139,6 +139,7 @@ var (
// EnvStore is the location of the keepass file/store
EnvStore = environmentRegister(
EnvironmentString{
+ expand: true,
environmentDefault: newDefaultedEnvironment("",
environmentBase{
subKey: "STORE",
@@ -151,6 +152,7 @@ var (
// EnvHookDir is the directory of hooks to execute
EnvHookDir = environmentRegister(
EnvironmentString{
+ expand: true,
environmentDefault: newDefaultedEnvironment("",
environmentBase{
cat: hookCategory,
@@ -190,6 +192,7 @@ and '%s' allows for multiple windows.`, util.TimeWindowSpan, util.TimeWindowDeli
// EnvKeyFile is an keyfile for the database
EnvKeyFile = environmentRegister(
EnvironmentString{
+ expand: true,
environmentDefault: newDefaultedEnvironment("",
environmentBase{
cat: credsCategory,
@@ -246,6 +249,7 @@ Set to '%s' to ignore the set key value`, noKeyMode, IgnoreKeyMode),
})
envPassword = environmentRegister(
EnvironmentString{
+ expand: true,
environmentDefault: newDefaultedEnvironment("",
environmentBase{
cat: credsCategory,
@@ -284,11 +288,11 @@ Set to '%s' to ignore the set key value`, noKeyMode, IgnoreKeyMode),
// EnvPasswordGenTemplate is the output template for controlling how output words are placed together
EnvPasswordGenTemplate = environmentRegister(
EnvironmentString{
- environmentDefault: newDefaultedEnvironment(fmt.Sprintf("{{range %si, %sval := .}}{{if %si}}-{{end}}{{%sval.Text}}{{end}}", TemplateVariable, TemplateVariable, TemplateVariable, TemplateVariable),
+ environmentDefault: newDefaultedEnvironment("{{range $i, $val := .}}{{if $i}}-{{end}}{{$val.Text}}{{end}}",
environmentBase{
subKey: "TEMPLATE",
cat: genCategory,
- desc: fmt.Sprintf("The go text template to use to format the chosen words into a password (use '%s' to include a '$' to avoid shell expansion issues). Available fields: Text, Position.Start, and Position.End.", TemplateVariable),
+ desc: fmt.Sprintf("The go text template to use to format the chosen words into a password. Available fields: %s.", util.TextPositionFields()),
}),
allowed: []string{"<go template>"},
canDefault: true,
diff --git a/internal/util/reflect.go b/internal/util/reflect.go
@@ -17,3 +17,16 @@ func ListFields(p any) []string {
sort.Strings(vals)
return vals
}
+
+func readNested(v reflect.Type, root string) []string {
+ var fields []string
+ for i := 0; i < v.NumField(); i++ {
+ field := v.Field(i)
+ if field.Type.Kind() == reflect.Struct {
+ fields = append(fields, readNested(field.Type, fmt.Sprintf("%s.", field.Name))...)
+ } else {
+ fields = append(fields, fmt.Sprintf("%s%s", root, field.Name))
+ }
+ }
+ return fields
+}
diff --git a/internal/util/text.go b/internal/util/text.go
@@ -3,9 +3,23 @@ package util
import (
"bytes"
"fmt"
+ "reflect"
"strings"
)
+type (
+ // Position is the start/end of a word in a greater set
+ Position struct {
+ Start int
+ End int
+ }
+ // Word is the text and position in a greater position
+ Word struct {
+ Text string
+ Position Position
+ }
+)
+
// TextWrap performs simple block text word wrapping
func TextWrap(indent uint, in string) string {
var sections []string
@@ -59,3 +73,8 @@ func wrap(in string, maxLength int) string {
}
return strings.Join(lines, "\n")
}
+
+// TextPositionFields is the displayable set of templated fields
+func TextPositionFields() string {
+ return strings.Join(readNested(reflect.TypeOf(Word{}), ""), ", ")
+}
diff --git a/internal/util/text_test.go b/internal/util/text_test.go
@@ -24,3 +24,10 @@ func TestWrap(t *testing.T) {
t.Errorf("invalid wrap: %s", w)
}
}
+
+func TestTextFields(t *testing.T) {
+ v := util.TextPositionFields()
+ if v != "Text, Position.Start, Position.End" {
+ t.Errorf("unexpected fields: %s", v)
+ }
+}
diff --git a/tests/main.go b/tests/main.go
@@ -323,7 +323,7 @@ func test(profile string) error {
c["pwgen.word_count"] = "1"
r.writeConfig(c)
r.run("", "pwgen")
- c["pwgen.template"] = "\"{{range [%]idx, [%]val := .}}{{if lt [%]val.Position.End 5}}{{ [%]val.Text }}{{end}}{{end}}\""
+ c["pwgen.template"] = "\"{{range $idx, $val := .}}{{if lt $val.Position.End 5}}{{ $val.Text }}{{end}}{{end}}\""
c["pwgen.characters"] = c.quoteString("b")
c["pwgen.word_count"] = "2"
c["pwgen.title"] = "false"