lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 87da1a20d339586a37b6b1fe952d3ed27ae48876
parent 7d9e7a76cf679d71242ff2ae4c5f0ef94794b4c8
Author: Sean Enck <sean@ttypty.com>
Date:   Sat,  7 Dec 2024 14:54:29 -0500

migrate core back out into proper subcomponents

Diffstat:
Minternal/app/core.go | 4++--
Minternal/app/totp.go | 6+++---
Minternal/backend/query.go | 12++++++------
Minternal/config/core.go | 12++++++------
Minternal/config/toml.go | 4++--
Minternal/config/vars.go | 13+++++++------
Dinternal/core/colors.go | 53-----------------------------------------------------
Dinternal/core/colors_test.go | 40----------------------------------------
Dinternal/core/core.go | 19-------------------
Dinternal/core/core_test.go | 20--------------------
Dinternal/core/json.go | 41-----------------------------------------
Dinternal/core/json_test.go | 33---------------------------------
Dinternal/core/text.go | 61-------------------------------------------------------------
Dinternal/core/text_test.go | 26--------------------------
Ainternal/output/json.go | 43+++++++++++++++++++++++++++++++++++++++++++
Ainternal/output/json_test.go | 33+++++++++++++++++++++++++++++++++
Minternal/platform/core.go | 4++--
Ainternal/util/reflect.go | 19+++++++++++++++++++
Ainternal/util/reflect_test.go | 20++++++++++++++++++++
Ainternal/util/text.go | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/util/text_test.go | 26++++++++++++++++++++++++++
Ainternal/util/time.go | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainternal/util/time_test.go | 40++++++++++++++++++++++++++++++++++++++++
23 files changed, 323 insertions(+), 320 deletions(-)

diff --git a/internal/app/core.go b/internal/app/core.go @@ -13,8 +13,8 @@ import ( "text/template" "github.com/seanenck/lockbox/internal/backend" - "github.com/seanenck/lockbox/internal/core" "github.com/seanenck/lockbox/internal/platform" + "github.com/seanenck/lockbox/internal/util" ) const ( @@ -299,7 +299,7 @@ func processDoc(header, file string, doc Documentation) (string, error) { if err := t.Execute(&buf, doc); err != nil { return "", err } - return fmt.Sprintf("%s\n%s", header, core.TextWrap(0, buf.String())), nil + return fmt.Sprintf("%s\n%s", header, util.TextWrap(0, buf.String())), nil } func setDocFlag(f string) string { diff --git a/internal/app/totp.go b/internal/app/totp.go @@ -12,8 +12,8 @@ import ( "github.com/seanenck/lockbox/internal/backend" "github.com/seanenck/lockbox/internal/config" - "github.com/seanenck/lockbox/internal/core" "github.com/seanenck/lockbox/internal/platform/clip" + "github.com/seanenck/lockbox/internal/util" ) var ( @@ -76,12 +76,12 @@ func clearFunc() { fmt.Print("\033[H\033[2J") } -func colorWhenRules() ([]core.ColorWindow, error) { +func colorWhenRules() ([]util.TimeWindow, error) { envTime := config.EnvTOTPColorBetween.Get() if envTime == config.TOTPDefaultBetween { return config.TOTPDefaultColorWindow, nil } - return core.ParseColorWindow(envTime) + return util.ParseTimeWindow(envTime) } func (w totpWrapper) generateCode() (string, error) { diff --git a/internal/backend/query.go b/internal/backend/query.go @@ -10,7 +10,7 @@ import ( "strings" "github.com/seanenck/lockbox/internal/config" - "github.com/seanenck/lockbox/internal/core" + "github.com/seanenck/lockbox/internal/output" "github.com/tobischo/gokeepasslib/v3" ) @@ -175,16 +175,16 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { if err != nil { return nil, err } - jsonMode := core.JSONOutputs.Blank + jsonMode := output.JSONModes.Blank if args.Values == JSONValue { - m, err := core.ParseJSONOutput(config.EnvJSONMode.Get()) + m, err := output.ParseJSONMode(config.EnvJSONMode.Get()) if err != nil { return nil, err } jsonMode = m } var hashLength int - if jsonMode == core.JSONOutputs.Hash { + if jsonMode == output.JSONModes.Hash { hashLength, err = config.EnvJSONHashLength.Get() if err != nil { return nil, err @@ -203,9 +203,9 @@ func (t *Transaction) QueryCallback(args QueryOptions) (QuerySeq2, error) { case JSONValue: data := "" switch jsonMode { - case core.JSONOutputs.Raw: + case output.JSONModes.Raw: data = val - case core.JSONOutputs.Hash: + case output.JSONModes.Hash: data = fmt.Sprintf("%x", sha512.Sum512([]byte(val))) if hashLength > 0 && len(data) > hashLength { data = data[0:hashLength] diff --git a/internal/config/core.go b/internal/config/core.go @@ -11,7 +11,7 @@ import ( "strings" "time" - "github.com/seanenck/lockbox/internal/core" + "github.com/seanenck/lockbox/internal/util" "mvdan.cc/sh/v3/shell" ) @@ -44,24 +44,24 @@ const ( requiredKeyOrKeyFile = "a key, a key file, or both must be set" // ModTimeFormat is the expected modtime format ModTimeFormat = time.RFC3339 - exampleColorWindow = "start" + core.ColorWindowSpan + "end" + exampleColorWindow = "start" + util.TimeWindowSpan + "end" ) var ( - exampleColorWindows = []string{fmt.Sprintf("[%s]", strings.Join([]string{exampleColorWindow, exampleColorWindow, exampleColorWindow + "..."}, core.ColorWindowDelimiter))} + exampleColorWindows = []string{fmt.Sprintf("[%s]", strings.Join([]string{exampleColorWindow, exampleColorWindow, exampleColorWindow + "..."}, util.TimeWindowDelimiter))} configDirOffsetFile = filepath.Join(configDirName, tomlFile) xdgPaths = []string{configDirOffsetFile, tomlFile} homePaths = []string{filepath.Join(configDir, configDirOffsetFile), filepath.Join(configDir, tomlFile)} registry = map[string]printer{} // TOTPDefaultColorWindow is the default coloring rules for totp - TOTPDefaultColorWindow = []core.ColorWindow{{Start: 0, End: 5}, {Start: 30, End: 35}} + TOTPDefaultColorWindow = []util.TimeWindow{{Start: 0, End: 5}, {Start: 30, End: 35}} // TOTPDefaultBetween is the default color window as a string TOTPDefaultBetween = func() string { var results []string for _, w := range TOTPDefaultColorWindow { - results = append(results, fmt.Sprintf("%d%s%d", w.Start, core.ColorWindowSpan, w.End)) + results = append(results, fmt.Sprintf("%d%s%d", w.Start, util.TimeWindowSpan, w.End)) } - return strings.Join(results, core.ColorWindowDelimiter) + return strings.Join(results, util.TimeWindowDelimiter) }() ) diff --git a/internal/config/toml.go b/internal/config/toml.go @@ -10,7 +10,7 @@ import ( "strings" "github.com/BurntSushi/toml" - "github.com/seanenck/lockbox/internal/core" + "github.com/seanenck/lockbox/internal/util" ) const ( @@ -119,7 +119,7 @@ func generateDetailText(data printer) (string, error) { value = "(unset)" } key := env.Key() - description := strings.TrimSpace(core.TextWrap(2, env.desc)) + description := strings.TrimSpace(util.TextWrap(2, env.desc)) requirement := "optional/default" r := strings.TrimSpace(env.requirement) if r != "" { diff --git a/internal/config/vars.go b/internal/config/vars.go @@ -5,8 +5,9 @@ import ( "fmt" "strings" - "github.com/seanenck/lockbox/internal/core" + "github.com/seanenck/lockbox/internal/output" "github.com/seanenck/lockbox/internal/platform" + "github.com/seanenck/lockbox/internal/util" ) var ( @@ -29,7 +30,7 @@ var ( environmentBase{ cat: jsonCategory, subKey: "HASH_LENGTH", - desc: fmt.Sprintf("Maximum string length of the JSON value when '%s' mode is set for JSON output.", core.JSONOutputs.Hash), + desc: fmt.Sprintf("Maximum string length of the JSON value when '%s' mode is set for JSON output.", output.JSONModes.Hash), }), shortDesc: "hash length", allowZero: true, @@ -181,7 +182,7 @@ var ( cat: totpCategory, desc: fmt.Sprintf(`Override when to set totp generated outputs to different colors, 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), +and '%s' allows for multiple windows.`, util.TimeWindowSpan, util.TimeWindowDelimiter), }), isArray: true, canDefault: true, @@ -215,14 +216,14 @@ and '%s' allows for multiple windows.`, core.ColorWindowSpan, core.ColorWindowDe // EnvJSONMode controls how JSON is output in the 'data' field EnvJSONMode = environmentRegister( EnvironmentString{ - environmentDefault: newDefaultedEnvironment(string(core.JSONOutputs.Hash), + environmentDefault: newDefaultedEnvironment(string(output.JSONModes.Hash), environmentBase{ cat: jsonCategory, subKey: "MODE", - desc: fmt.Sprintf("Changes what the data field in JSON outputs will contain.\n\nUse '%s' with CAUTION.", core.JSONOutputs.Raw), + desc: fmt.Sprintf("Changes what the data field in JSON outputs will contain.\n\nUse '%s' with CAUTION.", output.JSONModes.Raw), }), canDefault: true, - allowed: core.JSONOutputs.List(), + allowed: output.JSONModes.List(), }) // EnvTOTPFormat supports formatting the TOTP tokens for generation of tokens EnvTOTPFormat = environmentRegister(EnvironmentFormatter{environmentBase: environmentBase{ diff --git a/internal/core/colors.go b/internal/core/colors.go @@ -1,53 +0,0 @@ -// Package core has to assist with some color components -package core - -import ( - "errors" - "fmt" - "strconv" - "strings" -) - -const ( - // ColorWindowDelimiter indicates how windows are split in env/config keys - ColorWindowDelimiter = " " - // ColorWindowSpan indicates the delineation betwee start -> end (start:end) - ColorWindowSpan = ":" -) - -// ColorWindow for handling terminal colors based on timing -type ColorWindow struct { - Start int - End int -} - -// ParseColorWindow will handle parsing a window of colors for TOTP operations -func ParseColorWindow(windowString string) ([]ColorWindow, error) { - var rules []ColorWindow - for _, item := range strings.Split(windowString, ColorWindowDelimiter) { - line := strings.TrimSpace(item) - if line == "" { - continue - } - parts := strings.Split(line, ColorWindowSpan) - if len(parts) != 2 { - return nil, fmt.Errorf("invalid colorization rule found: %s", line) - } - s, err := strconv.Atoi(parts[0]) - if err != nil { - return nil, err - } - e, err := strconv.Atoi(parts[1]) - if err != nil { - return nil, err - } - if s < 0 || e < 0 || e < s || s > 59 || e > 59 { - return nil, fmt.Errorf("invalid time found for colorization rule: %s", line) - } - rules = append(rules, ColorWindow{Start: s, End: e}) - } - if len(rules) == 0 { - return nil, errors.New("invalid colorization rules for totp, none found") - } - return rules, nil -} diff --git a/internal/core/colors_test.go b/internal/core/colors_test.go @@ -1,40 +0,0 @@ -package core_test - -import ( - "testing" - - "github.com/seanenck/lockbox/internal/core" -) - -func TestParseWindows(t *testing.T) { - if _, err := core.ParseColorWindow(""); err.Error() != "invalid colorization rules for totp, none found" { - t.Errorf("invalid error: %v", err) - } - if _, err := core.ParseColorWindow(" 2"); err.Error() != "invalid colorization rule found: 2" { - t.Errorf("invalid error: %v", err) - } - if _, err := core.ParseColorWindow(" 1:200"); err.Error() != "invalid time found for colorization rule: 1:200" { - t.Errorf("invalid error: %v", err) - } - if _, err := core.ParseColorWindow(" 1:-1"); err.Error() != "invalid time found for colorization rule: 1:-1" { - t.Errorf("invalid error: %v", err) - } - if _, err := core.ParseColorWindow(" 200:1"); err.Error() != "invalid time found for colorization rule: 200:1" { - t.Errorf("invalid error: %v", err) - } - if _, err := core.ParseColorWindow(" -1:1"); err.Error() != "invalid time found for colorization rule: -1:1" { - t.Errorf("invalid error: %v", err) - } - if _, err := core.ParseColorWindow(" 2:1"); err.Error() != "invalid time found for colorization rule: 2:1" { - t.Errorf("invalid error: %v", err) - } - if _, err := core.ParseColorWindow("xxx:1"); err.Error() != "strconv.Atoi: parsing \"xxx\": invalid syntax" { - t.Errorf("invalid error: %v", err) - } - if _, err := core.ParseColorWindow(" 1:xxx"); err.Error() != "strconv.Atoi: parsing \"xxx\": invalid syntax" { - t.Errorf("invalid error: %v", err) - } - if _, err := core.ParseColorWindow("1:2 11:22"); err != nil { - t.Errorf("invalid error: %v", err) - } -} diff --git a/internal/core/core.go b/internal/core/core.go @@ -1,19 +0,0 @@ -// Package core has helpers -package core - -import ( - "fmt" - "reflect" - "sort" -) - -// ListFields will get the values of strings on an "all string" struct -func ListFields(p any) []string { - v := reflect.ValueOf(p) - var vals []string - for i := 0; i < v.NumField(); i++ { - vals = append(vals, fmt.Sprintf("%v", v.Field(i).Interface())) - } - sort.Strings(vals) - return vals -} diff --git a/internal/core/core_test.go b/internal/core/core_test.go @@ -1,20 +0,0 @@ -package core_test - -import ( - "fmt" - "testing" - - "github.com/seanenck/lockbox/internal/core" -) - -type mock struct { - Name string - Field string -} - -func TestListFields(t *testing.T) { - fields := core.ListFields(mock{"abc", "xyz"}) - if len(fields) != 2 || fmt.Sprintf("%v", fields) != "[abc xyz]" { - t.Errorf("invalid fields: %v", fields) - } -} diff --git a/internal/core/json.go b/internal/core/json.go @@ -1,41 +0,0 @@ -// Package core defines JSON outputs -package core - -import ( - "fmt" - "strings" -) - -// JSONOutputs are the JSON data output types for exporting/output of values -var JSONOutputs = JSONOutputTypes{ - Hash: "hash", - Blank: "empty", - Raw: "plaintext", -} - -type ( - // JSONOutputMode is the output mode definition - JSONOutputMode string - - // JSONOutputTypes indicate how JSON data can be exported for values - JSONOutputTypes struct { - Hash JSONOutputMode - Blank JSONOutputMode - Raw JSONOutputMode - } -) - -// List will list the output modes on the struct -func (p JSONOutputTypes) List() []string { - return ListFields(p) -} - -// ParseJSONOutput handles detecting the JSON output mode -func ParseJSONOutput(value string) (JSONOutputMode, error) { - val := JSONOutputMode(strings.ToLower(strings.TrimSpace(value))) - switch val { - case JSONOutputs.Hash, JSONOutputs.Blank, JSONOutputs.Raw: - return val, nil - } - return JSONOutputs.Blank, fmt.Errorf("invalid JSON output mode: %s", val) -} diff --git a/internal/core/json_test.go b/internal/core/json_test.go @@ -1,33 +0,0 @@ -package core_test - -import ( - "fmt" - "testing" - - "github.com/seanenck/lockbox/internal/core" -) - -func TestJSONList(t *testing.T) { - list := core.JSONOutputs.List() - if len(list) != 3 || fmt.Sprintf("%v", list) != "[empty hash plaintext]" { - t.Errorf("invalid list result: %v", list) - } -} - -func TestParseJSONMode(t *testing.T) { - m, err := core.ParseJSONOutput("hAsH ") - if m != core.JSONOutputs.Hash || err != nil { - t.Error("invalid mode read") - } - m, err = core.ParseJSONOutput("EMPTY") - if m != core.JSONOutputs.Blank || err != nil { - t.Error("invalid mode read") - } - m, err = core.ParseJSONOutput(" PLAINtext ") - if m != core.JSONOutputs.Raw || err != nil { - t.Error("invalid mode read") - } - if _, err = core.ParseJSONOutput("a"); err == nil || err.Error() != "invalid JSON output mode: a" { - t.Errorf("invalid error: %v", err) - } -} diff --git a/internal/core/text.go b/internal/core/text.go @@ -1,61 +0,0 @@ -package core - -import ( - "bytes" - "fmt" - "strings" -) - -// TextWrap performs simple block text word wrapping -func TextWrap(indent uint, in string) string { - var sections []string - var cur []string - for _, line := range strings.Split(strings.TrimSpace(in), "\n") { - trimmed := strings.TrimSpace(line) - if trimmed == "" { - if len(cur) > 0 { - sections = append(sections, strings.Join(cur, " ")) - cur = []string{} - } - continue - } - cur = append(cur, line) - } - if len(cur) > 0 { - sections = append(sections, strings.Join(cur, " ")) - } - var out bytes.Buffer - indenting := "" - var cnt uint - for cnt < indent { - indenting = fmt.Sprintf("%s ", indenting) - cnt++ - } - indenture := int(80 - indent) - for _, s := range sections { - for _, line := range strings.Split(wrap(s, indenture), "\n") { - fmt.Fprintf(&out, "%s%s\n", indenting, line) - } - fmt.Fprint(&out, "\n") - } - return out.String() -} - -func wrap(in string, maxLength int) string { - var lines []string - var cur []string - for _, p := range strings.Split(in, " ") { - state := strings.Join(cur, " ") - l := len(p) - if len(state)+l >= maxLength { - lines = append(lines, strings.Join(cur, " ")) - cur = []string{p} - } else { - cur = append(cur, p) - } - } - if len(cur) > 0 { - lines = append(lines, strings.Join(cur, " ")) - } - return strings.Join(lines, "\n") -} diff --git a/internal/core/text_test.go b/internal/core/text_test.go @@ -1,26 +0,0 @@ -package core_test - -import ( - "testing" - - "github.com/seanenck/lockbox/internal/core" -) - -func TestWrap(t *testing.T) { - w := core.TextWrap(0, "") - if w != "" { - t.Errorf("invalid wrap: %s", w) - } - w = core.TextWrap(0, "abc\n\nabc\nxyz\n") - if w != "abc\n\nabc xyz\n\n" { - t.Errorf("invalid wrap: %s", w) - } - w = core.TextWrap(0, "abc\n\nabc\nxyz\n\nx") - if w != "abc\n\nabc xyz\n\nx\n\n" { - t.Errorf("invalid wrap: %s", w) - } - w = core.TextWrap(5, "abc\n\nabc\nxyz\n\nx") - if w != " abc\n\n abc xyz\n\n x\n\n" { - t.Errorf("invalid wrap: %s", w) - } -} diff --git a/internal/output/json.go b/internal/output/json.go @@ -0,0 +1,43 @@ +// Package output defines JSON settings/modes +package output + +import ( + "fmt" + "strings" + + "github.com/seanenck/lockbox/internal/util" +) + +// JSONModes are the JSON data output types for exporting/output of values +var JSONModes = JSONTypes{ + Hash: "hash", + Blank: "empty", + Raw: "plaintext", +} + +type ( + // JSONMode is the output mode definition + JSONMode string + + // JSONTypes indicate how JSON data can be exported for values + JSONTypes struct { + Hash JSONMode + Blank JSONMode + Raw JSONMode + } +) + +// List will list the output modes on the struct +func (p JSONTypes) List() []string { + return util.ListFields(p) +} + +// ParseJSONMode handles detecting the JSON output mode +func ParseJSONMode(value string) (JSONMode, error) { + val := JSONMode(strings.ToLower(strings.TrimSpace(value))) + switch val { + case JSONModes.Hash, JSONModes.Blank, JSONModes.Raw: + return val, nil + } + return JSONModes.Blank, fmt.Errorf("invalid JSON output mode: %s", val) +} diff --git a/internal/output/json_test.go b/internal/output/json_test.go @@ -0,0 +1,33 @@ +package output_test + +import ( + "fmt" + "testing" + + "github.com/seanenck/lockbox/internal/output" +) + +func TestJSONList(t *testing.T) { + list := output.JSONModes.List() + if len(list) != 3 || fmt.Sprintf("%v", list) != "[empty hash plaintext]" { + t.Errorf("invalid list result: %v", list) + } +} + +func TestParseJSONMode(t *testing.T) { + m, err := output.ParseJSONMode("hAsH ") + if m != output.JSONModes.Hash || err != nil { + t.Error("invalid mode read") + } + m, err = output.ParseJSONMode("EMPTY") + if m != output.JSONModes.Blank || err != nil { + t.Error("invalid mode read") + } + m, err = output.ParseJSONMode(" PLAINtext ") + if m != output.JSONModes.Raw || err != nil { + t.Error("invalid mode read") + } + if _, err = output.ParseJSONMode("a"); err == nil || err.Error() != "invalid JSON output mode: a" { + t.Errorf("invalid error: %v", err) + } +} diff --git a/internal/platform/core.go b/internal/platform/core.go @@ -7,7 +7,7 @@ import ( "os/exec" "strings" - "github.com/seanenck/lockbox/internal/core" + "github.com/seanenck/lockbox/internal/util" ) // Systems are the known platforms for lockbox @@ -35,7 +35,7 @@ type ( // List will list the platform types on the struct func (p SystemTypes) List() []string { - return core.ListFields(p) + return util.ListFields(p) } // NewSystem gets a new system platform. diff --git a/internal/util/reflect.go b/internal/util/reflect.go @@ -0,0 +1,19 @@ +// Package util has reflection helpers +package util + +import ( + "fmt" + "reflect" + "sort" +) + +// ListFields will get the values of strings on an "all string" struct +func ListFields(p any) []string { + v := reflect.ValueOf(p) + var vals []string + for i := 0; i < v.NumField(); i++ { + vals = append(vals, fmt.Sprintf("%v", v.Field(i).Interface())) + } + sort.Strings(vals) + return vals +} diff --git a/internal/util/reflect_test.go b/internal/util/reflect_test.go @@ -0,0 +1,20 @@ +package util_test + +import ( + "fmt" + "testing" + + "github.com/seanenck/lockbox/internal/util" +) + +type mock struct { + Name string + Field string +} + +func TestListFields(t *testing.T) { + fields := util.ListFields(mock{"abc", "xyz"}) + if len(fields) != 2 || fmt.Sprintf("%v", fields) != "[abc xyz]" { + t.Errorf("invalid fields: %v", fields) + } +} diff --git a/internal/util/text.go b/internal/util/text.go @@ -0,0 +1,61 @@ +package util + +import ( + "bytes" + "fmt" + "strings" +) + +// TextWrap performs simple block text word wrapping +func TextWrap(indent uint, in string) string { + var sections []string + var cur []string + for _, line := range strings.Split(strings.TrimSpace(in), "\n") { + trimmed := strings.TrimSpace(line) + if trimmed == "" { + if len(cur) > 0 { + sections = append(sections, strings.Join(cur, " ")) + cur = []string{} + } + continue + } + cur = append(cur, line) + } + if len(cur) > 0 { + sections = append(sections, strings.Join(cur, " ")) + } + var out bytes.Buffer + indenting := "" + var cnt uint + for cnt < indent { + indenting = fmt.Sprintf("%s ", indenting) + cnt++ + } + indenture := int(80 - indent) + for _, s := range sections { + for _, line := range strings.Split(wrap(s, indenture), "\n") { + fmt.Fprintf(&out, "%s%s\n", indenting, line) + } + fmt.Fprint(&out, "\n") + } + return out.String() +} + +func wrap(in string, maxLength int) string { + var lines []string + var cur []string + for _, p := range strings.Split(in, " ") { + state := strings.Join(cur, " ") + l := len(p) + if len(state)+l >= maxLength { + lines = append(lines, strings.Join(cur, " ")) + cur = []string{p} + } else { + cur = append(cur, p) + } + } + if len(cur) > 0 { + lines = append(lines, strings.Join(cur, " ")) + } + return strings.Join(lines, "\n") +} diff --git a/internal/util/text_test.go b/internal/util/text_test.go @@ -0,0 +1,26 @@ +package util_test + +import ( + "testing" + + "github.com/seanenck/lockbox/internal/util" +) + +func TestWrap(t *testing.T) { + w := util.TextWrap(0, "") + if w != "" { + t.Errorf("invalid wrap: %s", w) + } + w = util.TextWrap(0, "abc\n\nabc\nxyz\n") + if w != "abc\n\nabc xyz\n\n" { + t.Errorf("invalid wrap: %s", w) + } + w = util.TextWrap(0, "abc\n\nabc\nxyz\n\nx") + if w != "abc\n\nabc xyz\n\nx\n\n" { + t.Errorf("invalid wrap: %s", w) + } + w = util.TextWrap(5, "abc\n\nabc\nxyz\n\nx") + if w != " abc\n\n abc xyz\n\n x\n\n" { + t.Errorf("invalid wrap: %s", w) + } +} diff --git a/internal/util/time.go b/internal/util/time.go @@ -0,0 +1,53 @@ +// Package util has to assist with some time windowing +package util + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + // TimeWindowDelimiter indicates how windows are split in env/config keys + TimeWindowDelimiter = " " + // TimeWindowSpan indicates the delineation between start -> end (start:end) + TimeWindowSpan = ":" +) + +// TimeWindow for handling terminal colors based on timing +type TimeWindow struct { + Start int + End int +} + +// ParseTimeWindow will handle parsing a window of colors for TOTP operations +func ParseTimeWindow(windowString string) ([]TimeWindow, error) { + var rules []TimeWindow + for _, item := range strings.Split(windowString, TimeWindowDelimiter) { + line := strings.TrimSpace(item) + if line == "" { + continue + } + parts := strings.Split(line, TimeWindowSpan) + if len(parts) != 2 { + return nil, fmt.Errorf("invalid colorization rule found: %s", line) + } + s, err := strconv.Atoi(parts[0]) + if err != nil { + return nil, err + } + e, err := strconv.Atoi(parts[1]) + if err != nil { + return nil, err + } + if s < 0 || e < 0 || e < s || s > 59 || e > 59 { + return nil, fmt.Errorf("invalid time found for colorization rule: %s", line) + } + rules = append(rules, TimeWindow{Start: s, End: e}) + } + if len(rules) == 0 { + return nil, errors.New("invalid colorization rules for totp, none found") + } + return rules, nil +} diff --git a/internal/util/time_test.go b/internal/util/time_test.go @@ -0,0 +1,40 @@ +package util_test + +import ( + "testing" + + "github.com/seanenck/lockbox/internal/util" +) + +func TestParseWindows(t *testing.T) { + if _, err := util.ParseTimeWindow(""); err.Error() != "invalid colorization rules for totp, none found" { + t.Errorf("invalid error: %v", err) + } + if _, err := util.ParseTimeWindow(" 2"); err.Error() != "invalid colorization rule found: 2" { + t.Errorf("invalid error: %v", err) + } + if _, err := util.ParseTimeWindow(" 1:200"); err.Error() != "invalid time found for colorization rule: 1:200" { + t.Errorf("invalid error: %v", err) + } + if _, err := util.ParseTimeWindow(" 1:-1"); err.Error() != "invalid time found for colorization rule: 1:-1" { + t.Errorf("invalid error: %v", err) + } + if _, err := util.ParseTimeWindow(" 200:1"); err.Error() != "invalid time found for colorization rule: 200:1" { + t.Errorf("invalid error: %v", err) + } + if _, err := util.ParseTimeWindow(" -1:1"); err.Error() != "invalid time found for colorization rule: -1:1" { + t.Errorf("invalid error: %v", err) + } + if _, err := util.ParseTimeWindow(" 2:1"); err.Error() != "invalid time found for colorization rule: 2:1" { + t.Errorf("invalid error: %v", err) + } + if _, err := util.ParseTimeWindow("xxx:1"); err.Error() != "strconv.Atoi: parsing \"xxx\": invalid syntax" { + t.Errorf("invalid error: %v", err) + } + if _, err := util.ParseTimeWindow(" 1:xxx"); err.Error() != "strconv.Atoi: parsing \"xxx\": invalid syntax" { + t.Errorf("invalid error: %v", err) + } + if _, err := util.ParseTimeWindow("1:2 11:22"); err != nil { + t.Errorf("invalid error: %v", err) + } +}