diff --git a/back/security.go b/back/security.go new file mode 100644 index 0000000..49bd416 --- /dev/null +++ b/back/security.go @@ -0,0 +1,9 @@ +package back + +// EraseBytesSecurely overwrites all +// bytes in a slice with zeros. +func EraseBytesSecurely(input []byte) { + for i := range input { + input[i] = 0 + } +} diff --git a/front/input.go b/front/input_OTHER.go similarity index 65% rename from front/input.go rename to front/input_OTHER.go index c116f83..dd668e3 100644 --- a/front/input.go +++ b/front/input_OTHER.go @@ -1,3 +1,5 @@ +//go:build !windows + package front import ( @@ -5,8 +7,6 @@ import ( "fmt" "os" "strings" - - "golang.org/x/term" ) // Input prompts the user for input and returns the input as a string. @@ -17,14 +17,6 @@ func Input(prompt string) string { return strings.TrimRight(userInput, "\n\r ") // remove trailing newlines, carriage returns, and spaces } -// InputHidden prompts the user for input and returns the input as a byte array, hiding the input from the terminal. -func InputHidden(prompt string) []byte { - fmt.Print(prompt + " ") - byteInput, _ := term.ReadPassword(int(os.Stdin.Fd())) - fmt.Println() - return byteInput -} - // InputBinary prompts the user with a yes/no question and returns the response as a boolean. func InputBinary(prompt string) bool { reader := bufio.NewReader(os.Stdin) @@ -49,11 +41,3 @@ func InputInt(prompt string, min, max int) int { fmt.Println() } } - -// InputMenuGen prompts the user with a menu and returns the user's choice as an integer. -func InputMenuGen(prompt string, options []string) int { - for i, option := range options { - fmt.Printf("%d. %s\n", i+1, option) - } - return InputInt("\n"+prompt, 1, len(options)) -} diff --git a/front/input_WIN.go b/front/input_WIN.go new file mode 100644 index 0000000..c562704 --- /dev/null +++ b/front/input_WIN.go @@ -0,0 +1,109 @@ +//go:build windows + +package front + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + "syscall" + "time" + "unsafe" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetStdHandle = kernel32.NewProc("GetStdHandle") + procPeekConsoleInput = kernel32.NewProc("PeekConsoleInputW") + procReadConsoleInput = kernel32.NewProc("ReadConsoleInputW") +) + +const ( + stdInputHandle = ^uintptr(10) - 1 // STD_INPUT_HANDLE = -10 +) + +type inputRecord struct { + EventType uint16 + _ [2]byte // padding + Event [16]byte +} + +// pollForInput checks if input is available without blocking +func pollForInput() bool { + handle, _, _ := procGetStdHandle.Call(stdInputHandle) + var numEvents uint32 + var record inputRecord + + ret, _, _ := procPeekConsoleInput.Call( + handle, + uintptr(unsafe.Pointer(&record)), + 1, + uintptr(unsafe.Pointer(&numEvents)), + ) + + return ret != 0 && numEvents > 0 +} + +// Input prompts the user for input and returns the input as a string. +// Uses polling to be more responsive to interrupts on Windows. +func Input(prompt string) string { + fmt.Print(prompt + " ") + + // Poll until input is available + for !pollForInput() { + time.Sleep(50 * time.Millisecond) + } + + reader := bufio.NewReader(os.Stdin) + userInput, _ := reader.ReadString('\n') + return strings.TrimRight(userInput, "\n\r ") // remove trailing newlines, carriage returns, and spaces +} + +// InputBinary prompts the user with a yes/no question and returns the response as a boolean. +// Uses polling to be more responsive to interrupts on Windows. +func InputBinary(prompt string) bool { + fmt.Print(prompt + " (y/N) ") + + // Poll until input is available + for !pollForInput() { + time.Sleep(50 * time.Millisecond) + } + + reader := bufio.NewReader(os.Stdin) + char, _, _ := reader.ReadRune() + if char == 'y' || char == 'Y' { + return true + } + return false +} + +// InputInt prompts the user for input and returns the input as an integer. +// A negative min/max value will disable the respective limit. +// Uses polling to be more responsive to interrupts on Windows. +func InputInt(prompt string, min, max int) int { + reader := bufio.NewReader(os.Stdin) + for { + fmt.Print(prompt + " ") + + // Poll until input is available + for !pollForInput() { + time.Sleep(50 * time.Millisecond) + } + + line, err := reader.ReadString('\n') + if err != nil { + fmt.Println() + continue + } + + line = strings.TrimSpace(line) + userInput, err := strconv.Atoi(line) + + if err == nil && (userInput >= min || min < 0) && (userInput <= max || max < 0) { + return userInput + } + fmt.Println() + } +} diff --git a/front/input_common.go b/front/input_common.go new file mode 100644 index 0000000..554864d --- /dev/null +++ b/front/input_common.go @@ -0,0 +1,24 @@ +package front + +import ( + "fmt" + "os" + + "golang.org/x/term" +) + +// InputMenuGen prompts the user with a menu and returns the user's choice as an integer. +func InputMenuGen(prompt string, options []string) int { + for i, option := range options { + fmt.Printf("%d. %s\n", i+1, option) + } + return InputInt("\n"+prompt, 1, len(options)) +} + +// InputHidden prompts the user for input and returns the input as a byte array, hiding the input from the terminal. +func InputHidden(prompt string) []byte { + fmt.Print(prompt + " ") + byteInput, _ := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + return byteInput +} diff --git a/go.mod b/go.mod index d4dce3f..3583b19 100644 --- a/go.mod +++ b/go.mod @@ -1,7 +1,7 @@ module github.com/rwinkhart/go-boilerplate -go 1.25.5 +go 1.25.6 -require golang.org/x/term v0.38.0 +require golang.org/x/term v0.39.0 -require golang.org/x/sys v0.39.0 // indirect +require golang.org/x/sys v0.40.0 // indirect diff --git a/go.sum b/go.sum index 08d079a..a4e0649 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,4 @@ -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=