commit 4542849384ce3eca17c6777f660519f3e9c3fc1d
parent f27884b36a12c7fad1f5c536085dd48674458c44
Author: Sean Enck <sean@ttypty.com>
Date: Sun, 28 May 2023 09:09:05 -0400
purge pgl
Diffstat:
20 files changed, 363 insertions(+), 168 deletions(-)
diff --git a/cmd/main.go b/cmd/main.go
@@ -14,8 +14,8 @@ import (
"github.com/enckse/lockbox/internal/cli"
"github.com/enckse/lockbox/internal/inputs"
"github.com/enckse/lockbox/internal/platform"
+ "github.com/enckse/lockbox/internal/system"
"github.com/enckse/lockbox/internal/totp"
- "github.com/enckse/pgl/os/exit"
)
//go:embed "vers.txt"
@@ -23,7 +23,7 @@ var version string
func main() {
if err := run(); err != nil {
- exit.Die(err)
+ app.Die(err.Error())
}
}
@@ -105,7 +105,7 @@ func run() error {
func clearClipboard() error {
idx := 0
- val, err := inputs.Stdin(false)
+ val, err := system.Stdin(false)
if err != nil {
return err
}
diff --git a/go.mod b/go.mod
@@ -4,7 +4,6 @@ go 1.19
require (
github.com/aymanbagabas/go-osc52 v1.2.2
- github.com/enckse/pgl v1.0.11
github.com/pquerna/otp v1.4.0
github.com/tobischo/gokeepasslib/v3 v3.5.1
mvdan.cc/sh/v3 v3.6.0
diff --git a/go.sum b/go.sum
@@ -9,8 +9,6 @@ github.com/boombuler/barcode v1.0.1 h1:NDBbPmhS+EqABEs5Kg3n/5ZNjy73Pz7SIV+KCeqyX
github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
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/enckse/pgl v1.0.11 h1:2JGsmc3eZ+3P62yZNRrHejbZ+g95giGhh16sULNRQnI=
-github.com/enckse/pgl v1.0.11/go.mod h1:r5bqGzwqnJIeY6UbGT5u38keJ5+ZySlsWeaYYzdBhMg=
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
diff --git a/internal/app/core.go b/internal/app/core.go
@@ -2,12 +2,12 @@
package app
import (
+ "fmt"
"io"
"os"
"github.com/enckse/lockbox/internal/backend"
- "github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/pgl/os/exit"
+ "github.com/enckse/lockbox/internal/system"
)
type (
@@ -52,13 +52,19 @@ func (a *DefaultCommand) Transaction() *backend.Transaction {
// Confirm will confirm with the user (dying if something abnormal happens)
func (a *DefaultCommand) Confirm(prompt string) bool {
- yesNo, err := inputs.ConfirmYesNoPrompt(prompt)
+ yesNo, err := system.ConfirmYesNoPrompt(prompt)
if err != nil {
- exit.Dief("failed to read stdin for confirmation: %v", err)
+ Die(fmt.Sprintf("failed to read stdin for confirmation: %v", err))
}
return yesNo
}
+// Die will print a message and exit (non-zero)
+func Die(msg string) {
+ fmt.Fprintf(os.Stderr, "%s\n", msg)
+ os.Exit(1)
+}
+
// SetArgs allow updating the command args
func (a *DefaultCommand) SetArgs(args ...string) {
a.args = args
@@ -66,10 +72,10 @@ func (a *DefaultCommand) SetArgs(args ...string) {
// IsPipe will indicate if we're receiving pipe input
func (a *DefaultCommand) IsPipe() bool {
- return inputs.IsInputFromPipe()
+ return system.IsInputFromPipe()
}
// Input will read user input
func (a *DefaultCommand) Input(pipe, multi bool) ([]byte, error) {
- return inputs.GetUserInputPassword(pipe, multi)
+ return system.GetUserInputPassword(pipe, multi)
}
diff --git a/internal/app/rekey.go b/internal/app/rekey.go
@@ -6,11 +6,11 @@ import (
"fmt"
"os"
"os/exec"
+ "strings"
"github.com/enckse/lockbox/internal/backend"
"github.com/enckse/lockbox/internal/cli"
"github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/pgl/types/values"
)
type (
@@ -91,8 +91,8 @@ func ReKey(cmd CommandOptions, r Keyer) error {
if _, err := fmt.Fprintf(writer, "rekeying: %s\n", path); err != nil {
return err
}
- modTime, empty := values.EmptyStringTrimmed(entry.ModTime)
- if empty {
+ modTime := strings.TrimSpace(entry.ModTime)
+ if modTime == "" {
return errors.New("did not read modtime")
}
var insertEnv []string
diff --git a/internal/backend/actions.go b/internal/backend/actions.go
@@ -8,7 +8,7 @@ import (
"time"
"github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/pgl/os/env"
+ "github.com/enckse/lockbox/internal/system"
"github.com/tobischo/gokeepasslib/v3"
"github.com/tobischo/gokeepasslib/v3/wrappers"
)
@@ -22,7 +22,7 @@ func (t *Transaction) act(cb action) error {
return err
}
k := string(key)
- file := env.GetOrDefault(inputs.KeyFileEnv, "")
+ file := system.EnvironOrDefault(inputs.KeyFileEnv, "")
if !t.exists {
if err := create(t.file, k, file); err != nil {
return err
@@ -164,7 +164,7 @@ func (t *Transaction) Move(src QueryEntity, dst string) error {
if strings.TrimSpace(src.Value) == "" {
return errors.New("empty secret not allowed")
}
- mod := env.GetOrDefault(inputs.ModTimeEnv, "")
+ mod := system.EnvironOrDefault(inputs.ModTimeEnv, "")
modTime := time.Now()
if mod != "" {
p, err := time.Parse(inputs.ModTimeFormat, mod)
diff --git a/internal/backend/core.go b/internal/backend/core.go
@@ -7,7 +7,7 @@ import (
"strings"
"github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/pgl/os/paths"
+ "github.com/enckse/lockbox/internal/system"
"github.com/tobischo/gokeepasslib/v3"
)
@@ -23,7 +23,7 @@ func loadFile(file string, must bool) (*Transaction, error) {
if !strings.HasSuffix(file, ".kdbx") {
return nil, errors.New("should use a .kdbx extension")
}
- exists := paths.Exist(file)
+ exists := system.PathExists(file)
if must {
if !exists {
return nil, errors.New("invalid file, does not exist")
@@ -61,7 +61,7 @@ func splitComponents(path string) ([]string, string, error) {
func getCredentials(key, keyFile string) (*gokeepasslib.DBCredentials, error) {
if len(keyFile) > 0 {
- if !paths.Exist(keyFile) {
+ if !system.PathExists(keyFile) {
return nil, errors.New("no keyfile found on disk")
}
return gokeepasslib.NewPasswordAndKeyCredentials(key, keyFile)
diff --git a/internal/backend/hooks.go b/internal/backend/hooks.go
@@ -9,8 +9,7 @@ import (
"strings"
"github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/pgl/os/env"
- "github.com/enckse/pgl/os/paths"
+ "github.com/enckse/lockbox/internal/system"
)
// NewHook will create a new hook type
@@ -18,11 +17,11 @@ func NewHook(path string, a ActionMode) (Hook, error) {
if strings.TrimSpace(path) == "" {
return Hook{}, errors.New("empty path is not allowed for hooks")
}
- dir := env.GetOrDefault(inputs.HookDirEnv, "")
+ dir := system.EnvironOrDefault(inputs.HookDirEnv, "")
if dir == "" {
return Hook{enabled: false}, nil
}
- if !paths.Exist(dir) {
+ if !system.PathExists(dir) {
return Hook{}, errors.New("hook directory does NOT exist")
}
entries, err := os.ReadDir(dir)
diff --git a/internal/backend/query.go b/internal/backend/query.go
@@ -10,7 +10,6 @@ import (
"strings"
"github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/pgl/types/collections"
"github.com/tobischo/gokeepasslib/v3"
)
@@ -73,7 +72,8 @@ func (t *Transaction) QueryCallback(args QueryOptions) ([]QueryEntity, error) {
if args.Mode == noneMode {
return nil, errors.New("no query mode specified")
}
- entities := collections.Map[string, QueryEntity]{}
+ entities := make(map[string]QueryEntity)
+ var keys []string
isSort := args.Mode != ExactMode
decrypt := args.Values != BlankValue
err := t.act(func(ctx Context) error {
@@ -104,7 +104,8 @@ func (t *Transaction) QueryCallback(args QueryOptions) ([]QueryEntity, error) {
}
}
}
- entities.Set(path, QueryEntity{backing: entry})
+ entities[path] = QueryEntity{backing: entry}
+ keys = append(keys, path)
})
if decrypt {
return ctx.db.UnlockProtectedEntries()
@@ -114,7 +115,6 @@ func (t *Transaction) QueryCallback(args QueryOptions) ([]QueryEntity, error) {
if err != nil {
return nil, err
}
- keys := entities.Keys()
if isSort {
sort.Strings(keys)
}
@@ -130,7 +130,7 @@ func (t *Transaction) QueryCallback(args QueryOptions) ([]QueryEntity, error) {
for _, k := range keys {
entity := QueryEntity{Path: k}
if args.Values != BlankValue {
- e, ok := entities.Get(k)
+ e, ok := entities[k]
if !ok {
return nil, errors.New("failed to read entity back from map")
}
diff --git a/internal/inputs/env.go b/internal/inputs/env.go
@@ -12,7 +12,7 @@ import (
"strings"
"time"
- "github.com/enckse/pgl/os/env"
+ "github.com/enckse/lockbox/internal/system"
"mvdan.cc/sh/v3/shell"
)
@@ -70,7 +70,7 @@ const (
JSONDataOutputEnv = prefixKey + "JSON_DATA_OUTPUT"
)
-var isYesNoArgs = []string{env.Yes, env.No}
+var isYesNoArgs = []string{system.Yes, system.No}
type (
environmentOutput struct {
@@ -185,13 +185,13 @@ func getKey(keyMode, name string) ([]byte, error) {
}
func isYesNoEnv(defaultValue bool, envKey string) (bool, error) {
- read := env.GetValue(envKey)
+ read := system.EnvironValue(envKey)
switch read {
- case env.NoValue:
+ case system.NoValue:
return false, nil
- case env.YesValue:
+ case system.YesValue:
return true, nil
- case env.EmptyValue:
+ case system.EmptyValue:
return defaultValue, nil
}
@@ -230,7 +230,7 @@ func IsInteractive() (bool, error) {
// TOTPToken gets the name of the totp special case tokens
func TOTPToken() string {
- return env.GetOrDefault(fieldTOTPEnv, defaultTOTPField)
+ return system.EnvironOrDefault(fieldTOTPEnv, defaultTOTPField)
}
func (o environmentOutput) formatEnvironmentVariable(required bool, name, val, desc string, allowed []string) string {
@@ -262,10 +262,10 @@ func ListEnvironmentVariables(showValues bool) []string {
results = append(results, e.formatEnvironmentVariable(true, StoreEnv, "", "directory to the database file", []string{"file"}))
results = append(results, e.formatEnvironmentVariable(true, keyModeEnv, commandKeyMode, "how to retrieve the database store password", []string{commandKeyMode, plainKeyMode}))
results = append(results, e.formatEnvironmentVariable(true, keyEnv, "", fmt.Sprintf("the database key ('%s' mode) or command to run ('%s' mode)\nto retrieve the database password", plainKeyMode, commandKeyMode), []string{commandArgsExample, "password"}))
- results = append(results, e.formatEnvironmentVariable(false, noClipEnv, env.No, "disable clipboard operations", isYesNoArgs))
- results = append(results, e.formatEnvironmentVariable(false, noColorEnv, env.No, "disable terminal colors", isYesNoArgs))
- results = append(results, e.formatEnvironmentVariable(false, interactiveEnv, env.Yes, "enable interactive mode", isYesNoArgs))
- results = append(results, e.formatEnvironmentVariable(false, readOnlyEnv, env.No, "operate in readonly mode", isYesNoArgs))
+ results = append(results, e.formatEnvironmentVariable(false, noClipEnv, system.No, "disable clipboard operations", isYesNoArgs))
+ results = append(results, e.formatEnvironmentVariable(false, noColorEnv, system.No, "disable terminal colors", isYesNoArgs))
+ results = append(results, e.formatEnvironmentVariable(false, interactiveEnv, system.Yes, "enable interactive mode", isYesNoArgs))
+ results = append(results, e.formatEnvironmentVariable(false, readOnlyEnv, system.No, "operate in readonly mode", isYesNoArgs))
results = append(results, e.formatEnvironmentVariable(false, fieldTOTPEnv, defaultTOTPField, "attribute name to store TOTP tokens within the database", []string{"string"}))
results = append(results, e.formatEnvironmentVariable(false, formatTOTPEnv, strings.ReplaceAll(strings.ReplaceAll(FormatTOTP("%s"), "%25s", "%s"), "&", " \\\n &"), "override the otpauth url used to store totp tokens. It must have ONE format\nstring ('%s') to insert the totp base code", []string{"otpauth//url/%s/args..."}))
results = append(results, e.formatEnvironmentVariable(false, MaxTOTPTime, MaxTOTPTimeDefault, "time, in seconds, in which to show a TOTP token before automatically exiting", []string{"integer"}))
@@ -274,9 +274,9 @@ func ListEnvironmentVariables(showValues bool) []string {
results = append(results, e.formatEnvironmentVariable(false, ClipCopyEnv, detectedValue, "override the detected platform copy command", []string{commandArgsExample}))
results = append(results, e.formatEnvironmentVariable(false, clipMaxEnv, fmt.Sprintf("%d", defaultMaxClipboard), "override the amount of time before totp clears the clipboard (e.g. 10),\nmust be an integer", []string{"integer"}))
results = append(results, e.formatEnvironmentVariable(false, PlatformEnv, detectedValue, "override the detected platform", PlatformSet()))
- results = append(results, e.formatEnvironmentVariable(false, noTOTPEnv, env.No, "disable TOTP integrations", isYesNoArgs))
+ results = append(results, e.formatEnvironmentVariable(false, noTOTPEnv, system.No, "disable TOTP integrations", isYesNoArgs))
results = append(results, e.formatEnvironmentVariable(false, HookDirEnv, "", "the path to hooks to execute on actions against the database", []string{"directory"}))
- results = append(results, e.formatEnvironmentVariable(false, clipOSC52Env, env.No, "enable OSC52 clipboard mode", isYesNoArgs))
+ results = append(results, e.formatEnvironmentVariable(false, clipOSC52Env, system.No, "enable OSC52 clipboard mode", isYesNoArgs))
results = append(results, e.formatEnvironmentVariable(false, KeyFileEnv, "", "additional keyfile to access/protect the database", []string{"keyfile"}))
results = append(results, e.formatEnvironmentVariable(false, ModTimeEnv, ModTimeFormat, fmt.Sprintf("input modification time to set for the entry\n(expected format: %s)", ModTimeFormat), []string{"modtime"}))
results = append(results, e.formatEnvironmentVariable(false, JSONDataOutputEnv, string(JSONDataOutputHash), fmt.Sprintf("changes what the data field in JSON outputs will contain\nuse '%s' with CAUTION", JSONDataOutputRaw), []string{string(JSONDataOutputRaw), string(JSONDataOutputHash), string(JSONDataOutputBlank)}))
diff --git a/internal/inputs/json.go b/internal/inputs/json.go
@@ -5,7 +5,7 @@ import (
"fmt"
"strings"
- "github.com/enckse/pgl/os/env"
+ "github.com/enckse/lockbox/internal/system"
)
const (
@@ -24,7 +24,7 @@ type (
// ParseJSONOutput handles detecting the JSON output mode
func ParseJSONOutput() (JSONOutputMode, error) {
- val := strings.ToLower(strings.TrimSpace(env.GetOrDefault(JSONDataOutputEnv, string(JSONDataOutputHash))))
+ val := strings.ToLower(strings.TrimSpace(system.EnvironOrDefault(JSONDataOutputEnv, string(JSONDataOutputHash))))
switch JSONOutputMode(val) {
case JSONDataOutputHash:
return JSONDataOutputHash, nil
diff --git a/internal/inputs/stdin.go b/internal/inputs/stdin.go
@@ -1,116 +0,0 @@
-// Package inputs handles stdin management/access.
-package inputs
-
-import (
- "errors"
- "fmt"
- "os"
- "strings"
- "syscall"
-
- "github.com/enckse/pgl/os/stdin"
-)
-
-func termEcho(on bool) {
- // Common settings and variables for both stty calls.
- attrs := syscall.ProcAttr{
- Dir: "",
- Env: []string{},
- Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
- Sys: nil,
- }
- var ws syscall.WaitStatus
- cmd := "echo"
- if !on {
- cmd = "-echo"
- }
-
- // Enable/disable echoing.
- pid, err := syscall.ForkExec(
- "/bin/stty",
- []string{"stty", cmd},
- &attrs)
- if err != nil {
- panic(err)
- }
-
- // Wait for the stty process to complete.
- _, err = syscall.Wait4(pid, &ws, 0, nil)
- if err != nil {
- panic(err)
- }
-}
-
-// GetUserInputPassword will read the user's input from stdin via multiple means.
-func GetUserInputPassword(piping, multiLine bool) ([]byte, error) {
- var password string
- if !multiLine && !piping {
- input, err := confirmInputsMatch()
- if err != nil {
- return nil, err
- }
- password = input
- } else {
- input, err := Stdin(false)
- if err != nil {
- return nil, err
- }
- password = input
- }
- if password == "" {
- return nil, errors.New("password can NOT be empty")
- }
- return []byte(password), nil
-}
-
-func confirmInputsMatch() (string, error) {
- termEcho(false)
- defer func() {
- termEcho(true)
- }()
- fmt.Print("please enter password: ")
- first, err := Stdin(true)
- if err != nil {
- return "", err
- }
- fmt.Print("\nplease re-enter password: ")
- second, err := Stdin(true)
- if err != nil {
- return "", err
- }
- if first != second {
- return "", errors.New("passwords do NOT match")
- }
- return first, nil
-}
-
-// Stdin will get one (or more) lines of stdin as string.
-func Stdin(one bool) (string, error) {
- var b []byte
- var err error
- if one {
- b, err = stdin.ReadLine()
- } else {
- b, err = stdin.ReadAll()
- }
- if err != nil {
- return "", err
- }
- return strings.TrimSpace(string(b)), nil
-}
-
-// IsInputFromPipe will indicate if connected to stdin pipe.
-func IsInputFromPipe() bool {
- fileInfo, _ := os.Stdin.Stat()
- return fileInfo.Mode()&os.ModeCharDevice == 0
-}
-
-// ConfirmYesNoPrompt will ask a yes/no question.
-func ConfirmYesNoPrompt(prompt string) (bool, error) {
- fmt.Printf("%s? (y/N) ", prompt)
- resp, err := Stdin(true)
- if err != nil {
- return false, err
- }
- return resp == "Y" || resp == "y", nil
-}
diff --git a/internal/inputs/totp.go b/internal/inputs/totp.go
@@ -6,7 +6,7 @@ import (
"net/url"
"strings"
- "github.com/enckse/pgl/os/env"
+ "github.com/enckse/lockbox/internal/system"
)
const (
@@ -19,7 +19,7 @@ func FormatTOTP(value string) string {
if strings.HasPrefix(value, otpAuth) {
return value
}
- override := env.GetOrDefault(formatTOTPEnv, "")
+ override := system.EnvironOrDefault(formatTOTPEnv, "")
if override != "" {
return fmt.Sprintf(override, value)
}
diff --git a/internal/platform/clipboard.go b/internal/platform/clipboard.go
@@ -10,7 +10,7 @@ import (
osc "github.com/aymanbagabas/go-osc52"
"github.com/enckse/lockbox/internal/inputs"
- "github.com/enckse/pgl/os/env"
+ "github.com/enckse/lockbox/internal/system"
)
type (
@@ -32,7 +32,7 @@ func newClipboard(copying, pasting []string) (Clipboard, error) {
}
func overrideCommand(v string) ([]string, error) {
- value := env.GetOrDefault(v, "")
+ value := system.EnvironOrDefault(v, "")
if strings.TrimSpace(value) == "" {
return nil, nil
}
diff --git a/internal/system/env.go b/internal/system/env.go
@@ -0,0 +1,53 @@
+// Package system handles simple environment variable processing
+package system
+
+import (
+ "os"
+ "strings"
+)
+
+const (
+ // Yes is the string expected for yes values
+ Yes = "yes"
+ // No is the string expected for no values
+ No = "no"
+)
+
+type (
+ // ReadValue is the output of reading a known bool/yes/no
+ ReadValue uint
+)
+
+const (
+ // UnknownValue indicates an unknown value was read
+ UnknownValue ReadValue = iota
+ // YesValue means yes was set
+ YesValue
+ // NoValue means no was set
+ NoValue
+ // EmptyValue means that the value was not set (empty string)
+ EmptyValue
+)
+
+// EnvironOrDefault will get the environment value OR default if env is not set.
+func EnvironOrDefault(envKey, defaultValue string) string {
+ val := os.Getenv(envKey)
+ if strings.TrimSpace(val) == "" {
+ return defaultValue
+ }
+ return val
+}
+
+// EnvironValue read a simple yes/no from an environment value
+func EnvironValue(envKey string) ReadValue {
+ value := strings.ToLower(strings.TrimSpace(os.Getenv(envKey)))
+ switch value {
+ case No:
+ return NoValue
+ case Yes:
+ return YesValue
+ case "":
+ return EmptyValue
+ }
+ return UnknownValue
+}
diff --git a/internal/system/env_test.go b/internal/system/env_test.go
@@ -0,0 +1,54 @@
+package system_test
+
+import (
+ "os"
+ "testing"
+
+ "github.com/enckse/lockbox/internal/system"
+)
+
+func TestEnvDefault(t *testing.T) {
+ os.Clearenv()
+ val := system.EnvironOrDefault("TEST", "value")
+ if val != "value" {
+ t.Error("invalid read")
+ }
+ os.Setenv("TEST", " ")
+ val = system.EnvironOrDefault("TEST", "value")
+ if val != "value" {
+ t.Error("invalid read")
+ }
+ os.Setenv("TEST", " a")
+ val = system.EnvironOrDefault("TEST", "value")
+ if val != " a" {
+ t.Error("invalid read")
+ }
+}
+
+func TestReadValue(t *testing.T) {
+ os.Clearenv()
+ val := system.EnvironValue("test")
+ if val != system.EmptyValue {
+ t.Error("bad read")
+ }
+ os.Setenv("TEST", "a")
+ val = system.EnvironValue("TEST")
+ if val != system.UnknownValue {
+ t.Error("bad read")
+ }
+ os.Setenv("TEST", " YeS ")
+ val = system.EnvironValue("TEST")
+ if val != system.YesValue {
+ t.Error("bad read")
+ }
+ os.Setenv("TEST", " NO ")
+ val = system.EnvironValue("TEST")
+ if val != system.NoValue {
+ t.Error("bad read")
+ }
+ os.Setenv("TEST", "FALSESSS")
+ val = system.EnvironValue("TEST")
+ if val != system.UnknownValue {
+ t.Error("bad read")
+ }
+}
diff --git a/internal/system/paths.go b/internal/system/paths.go
@@ -0,0 +1,15 @@
+// Package system is responsible for pathing operations/commands
+package system
+
+import (
+ "errors"
+ "os"
+)
+
+// PathExists indicates whether a path exists (true) or not (false)
+func PathExists(file string) bool {
+ if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
+ return false
+ }
+ return true
+}
diff --git a/internal/system/paths_test.go b/internal/system/paths_test.go
@@ -0,0 +1,21 @@
+package system_test
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/enckse/lockbox/internal/system"
+)
+
+func TestPathExist(t *testing.T) {
+ testDir := filepath.Join("testdata", "exists")
+ os.RemoveAll(testDir)
+ if system.PathExists(testDir) {
+ t.Error("test dir SHOULD NOT exist")
+ }
+ os.MkdirAll(testDir, 0o755)
+ if !system.PathExists(testDir) {
+ t.Error("test dir SHOULD exist")
+ }
+}
diff --git a/internal/system/stdin.go b/internal/system/stdin.go
@@ -0,0 +1,166 @@
+// Package system handles stdin processing
+package system
+
+import (
+ "bufio"
+ "bytes"
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+ "syscall"
+)
+
+type (
+ stdinReaderFunc func(string) (bool, error)
+)
+
+func termEcho(on bool) {
+ // Common settings and variables for both stty calls.
+ attrs := syscall.ProcAttr{
+ Dir: "",
+ Env: []string{},
+ Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
+ Sys: nil,
+ }
+ var ws syscall.WaitStatus
+ cmd := "echo"
+ if !on {
+ cmd = "-echo"
+ }
+
+ // Enable/disable echoing.
+ pid, err := syscall.ForkExec(
+ "/bin/stty",
+ []string{"stty", cmd},
+ &attrs)
+ if err != nil {
+ panic(err)
+ }
+
+ // Wait for the stty process to complete.
+ _, err = syscall.Wait4(pid, &ws, 0, nil)
+ if err != nil {
+ panic(err)
+ }
+}
+
+// GetUserInputPassword will read the user's input from stdin via multiple means.
+func GetUserInputPassword(piping, multiLine bool) ([]byte, error) {
+ var password string
+ if !multiLine && !piping {
+ input, err := confirmInputsMatch()
+ if err != nil {
+ return nil, err
+ }
+ password = input
+ } else {
+ input, err := Stdin(false)
+ if err != nil {
+ return nil, err
+ }
+ password = input
+ }
+ if password == "" {
+ return nil, errors.New("password can NOT be empty")
+ }
+ return []byte(password), nil
+}
+
+func confirmInputsMatch() (string, error) {
+ termEcho(false)
+ defer func() {
+ termEcho(true)
+ }()
+ fmt.Print("please enter password: ")
+ first, err := Stdin(true)
+ if err != nil {
+ return "", err
+ }
+ fmt.Print("\nplease re-enter password: ")
+ second, err := Stdin(true)
+ if err != nil {
+ return "", err
+ }
+ if first != second {
+ return "", errors.New("passwords do NOT match")
+ }
+ return first, nil
+}
+
+// Stdin will get one (or more) lines of stdin as string.
+func Stdin(one bool) (string, error) {
+ var b []byte
+ var err error
+ if one {
+ b, err = readLine()
+ } else {
+ b, err = readAll()
+ }
+ if err != nil {
+ return "", err
+ }
+ return strings.TrimSpace(string(b)), nil
+}
+
+// IsInputFromPipe will indicate if connected to stdin pipe.
+func IsInputFromPipe() bool {
+ fileInfo, _ := os.Stdin.Stat()
+ return fileInfo.Mode()&os.ModeCharDevice == 0
+}
+
+// ConfirmYesNoPrompt will ask a yes/no question.
+func ConfirmYesNoPrompt(prompt string) (bool, error) {
+ fmt.Printf("%s? (y/N) ", prompt)
+ resp, err := Stdin(true)
+ if err != nil {
+ return false, err
+ }
+ return resp == "Y" || resp == "y", nil
+}
+
+func readAll() ([]byte, error) {
+ return read(false)
+}
+
+func readLine() ([]byte, error) {
+ return read(true)
+}
+
+// ReadFunc will read stdin and execute the given function
+func ReadFunc(reader stdinReaderFunc) error {
+ if reader == nil {
+ return errors.New("invalid reader, nil")
+ }
+ scanner := bufio.NewScanner(os.Stdin)
+ for scanner.Scan() {
+ ok, err := reader(scanner.Text())
+ if err != nil {
+ return err
+ }
+ if !ok {
+ break
+ }
+ }
+ return scanner.Err()
+}
+
+func read(one bool) ([]byte, error) {
+ var b bytes.Buffer
+ err := ReadFunc(func(line string) (bool, error) {
+ if _, err := b.WriteString(line); err != nil {
+ return false, err
+ }
+ if _, err := b.WriteString("\n"); err != nil {
+ return false, err
+ }
+ if one {
+ return false, nil
+ }
+ return true, nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ return b.Bytes(), nil
+}
diff --git a/internal/totp/core.go b/internal/totp/core.go
@@ -16,7 +16,7 @@ import (
"github.com/enckse/lockbox/internal/colors"
"github.com/enckse/lockbox/internal/inputs"
"github.com/enckse/lockbox/internal/platform"
- "github.com/enckse/pgl/os/env"
+ "github.com/enckse/lockbox/internal/system"
coreotp "github.com/pquerna/otp"
otp "github.com/pquerna/otp/totp"
)
@@ -86,7 +86,7 @@ func clear() {
}
func colorWhenRules() ([]inputs.ColorWindow, error) {
- envTime := env.GetOrDefault(inputs.ColorBetweenEnv, inputs.TOTPDefaultBetween)
+ envTime := system.EnvironOrDefault(inputs.ColorBetweenEnv, inputs.TOTPDefaultBetween)
if envTime == inputs.TOTPDefaultBetween {
return inputs.TOTPDefaultColorWindow, nil
}
@@ -160,7 +160,7 @@ func (args *Arguments) display(opts Options) error {
if err != nil {
return err
}
- runString := env.GetOrDefault(inputs.MaxTOTPTime, inputs.MaxTOTPTimeDefault)
+ runString := system.EnvironOrDefault(inputs.MaxTOTPTime, inputs.MaxTOTPTimeDefault)
runFor, err := strconv.Atoi(runString)
if err != nil {
return err