CL 212377 changed end of input character on windows - from \n to \r. But CL 212377 did not adjust ReadConsole accordingly. For example, after CL 212377 \n was still used to end of password processing, and \r was ignored. This CL swaps these rules - \r is now used to end password processing, and \n are ignored. The change only affects windows, all non windows code should work as before. This CL also adjusts TestReadPasswordLineEnd to fit new rules. Fixes golang/go#36609 Change-Id: I027bf80d10e7d4d4b17ff0264935d14b8bea9097 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/215417 Run-TryBot: Alex Brainman <alex.brainman@gmail.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Filippo Valsorda <filippo@golang.org>
422 lines
9.0 KiB
Go
422 lines
9.0 KiB
Go
// Copyright 2011 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// +build aix darwin dragonfly freebsd linux,!appengine netbsd openbsd windows plan9 solaris
|
|
|
|
package terminal
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"runtime"
|
|
"testing"
|
|
)
|
|
|
|
type MockTerminal struct {
|
|
toSend []byte
|
|
bytesPerRead int
|
|
received []byte
|
|
}
|
|
|
|
func (c *MockTerminal) Read(data []byte) (n int, err error) {
|
|
n = len(data)
|
|
if n == 0 {
|
|
return
|
|
}
|
|
if n > len(c.toSend) {
|
|
n = len(c.toSend)
|
|
}
|
|
if n == 0 {
|
|
return 0, io.EOF
|
|
}
|
|
if c.bytesPerRead > 0 && n > c.bytesPerRead {
|
|
n = c.bytesPerRead
|
|
}
|
|
copy(data, c.toSend[:n])
|
|
c.toSend = c.toSend[n:]
|
|
return
|
|
}
|
|
|
|
func (c *MockTerminal) Write(data []byte) (n int, err error) {
|
|
c.received = append(c.received, data...)
|
|
return len(data), nil
|
|
}
|
|
|
|
func TestClose(t *testing.T) {
|
|
c := &MockTerminal{}
|
|
ss := NewTerminal(c, "> ")
|
|
line, err := ss.ReadLine()
|
|
if line != "" {
|
|
t.Errorf("Expected empty line but got: %s", line)
|
|
}
|
|
if err != io.EOF {
|
|
t.Errorf("Error should have been EOF but got: %s", err)
|
|
}
|
|
}
|
|
|
|
var keyPressTests = []struct {
|
|
in string
|
|
line string
|
|
err error
|
|
throwAwayLines int
|
|
}{
|
|
{
|
|
err: io.EOF,
|
|
},
|
|
{
|
|
in: "\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "foo\r",
|
|
line: "foo",
|
|
},
|
|
{
|
|
in: "a\x1b[Cb\r", // right
|
|
line: "ab",
|
|
},
|
|
{
|
|
in: "a\x1b[Db\r", // left
|
|
line: "ba",
|
|
},
|
|
{
|
|
in: "a\177b\r", // backspace
|
|
line: "b",
|
|
},
|
|
{
|
|
in: "\x1b[A\r", // up
|
|
},
|
|
{
|
|
in: "\x1b[B\r", // down
|
|
},
|
|
{
|
|
in: "\016\r", // ^P
|
|
},
|
|
{
|
|
in: "\014\r", // ^N
|
|
},
|
|
{
|
|
in: "line\x1b[A\x1b[B\r", // up then down
|
|
line: "line",
|
|
},
|
|
{
|
|
in: "line1\rline2\x1b[A\r", // recall previous line.
|
|
line: "line1",
|
|
throwAwayLines: 1,
|
|
},
|
|
{
|
|
// recall two previous lines and append.
|
|
in: "line1\rline2\rline3\x1b[A\x1b[Axxx\r",
|
|
line: "line1xxx",
|
|
throwAwayLines: 2,
|
|
},
|
|
{
|
|
// Ctrl-A to move to beginning of line followed by ^K to kill
|
|
// line.
|
|
in: "a b \001\013\r",
|
|
line: "",
|
|
},
|
|
{
|
|
// Ctrl-A to move to beginning of line, Ctrl-E to move to end,
|
|
// finally ^K to kill nothing.
|
|
in: "a b \001\005\013\r",
|
|
line: "a b ",
|
|
},
|
|
{
|
|
in: "\027\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "a\027\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "a \027\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "a b\027\r",
|
|
line: "a ",
|
|
},
|
|
{
|
|
in: "a b \027\r",
|
|
line: "a ",
|
|
},
|
|
{
|
|
in: "one two thr\x1b[D\027\r",
|
|
line: "one two r",
|
|
},
|
|
{
|
|
in: "\013\r",
|
|
line: "",
|
|
},
|
|
{
|
|
in: "a\013\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
in: "ab\x1b[D\013\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
in: "Ξεσκεπάζω\r",
|
|
line: "Ξεσκεπάζω",
|
|
},
|
|
{
|
|
in: "£\r\x1b[A\177\r", // non-ASCII char, enter, up, backspace.
|
|
line: "",
|
|
throwAwayLines: 1,
|
|
},
|
|
{
|
|
in: "£\r££\x1b[A\x1b[B\177\r", // non-ASCII char, enter, 2x non-ASCII, up, down, backspace, enter.
|
|
line: "£",
|
|
throwAwayLines: 1,
|
|
},
|
|
{
|
|
// Ctrl-D at the end of the line should be ignored.
|
|
in: "a\004\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
// a, b, left, Ctrl-D should erase the b.
|
|
in: "ab\x1b[D\004\r",
|
|
line: "a",
|
|
},
|
|
{
|
|
// a, b, c, d, left, left, ^U should erase to the beginning of
|
|
// the line.
|
|
in: "abcd\x1b[D\x1b[D\025\r",
|
|
line: "cd",
|
|
},
|
|
{
|
|
// Bracketed paste mode: control sequences should be returned
|
|
// verbatim in paste mode.
|
|
in: "abc\x1b[200~de\177f\x1b[201~\177\r",
|
|
line: "abcde\177",
|
|
},
|
|
{
|
|
// Enter in bracketed paste mode should still work.
|
|
in: "abc\x1b[200~d\refg\x1b[201~h\r",
|
|
line: "efgh",
|
|
throwAwayLines: 1,
|
|
},
|
|
{
|
|
// Lines consisting entirely of pasted data should be indicated as such.
|
|
in: "\x1b[200~a\r",
|
|
line: "a",
|
|
err: ErrPasteIndicator,
|
|
},
|
|
}
|
|
|
|
func TestKeyPresses(t *testing.T) {
|
|
for i, test := range keyPressTests {
|
|
for j := 1; j < len(test.in); j++ {
|
|
c := &MockTerminal{
|
|
toSend: []byte(test.in),
|
|
bytesPerRead: j,
|
|
}
|
|
ss := NewTerminal(c, "> ")
|
|
for k := 0; k < test.throwAwayLines; k++ {
|
|
_, err := ss.ReadLine()
|
|
if err != nil {
|
|
t.Errorf("Throwaway line %d from test %d resulted in error: %s", k, i, err)
|
|
}
|
|
}
|
|
line, err := ss.ReadLine()
|
|
if line != test.line {
|
|
t.Errorf("Line resulting from test %d (%d bytes per read) was '%s', expected '%s'", i, j, line, test.line)
|
|
break
|
|
}
|
|
if err != test.err {
|
|
t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var renderTests = []struct {
|
|
in string
|
|
received string
|
|
err error
|
|
}{
|
|
{
|
|
// Cursor move after keyHome (left 4) then enter (right 4, newline)
|
|
in: "abcd\x1b[H\r",
|
|
received: "> abcd\x1b[4D\x1b[4C\r\n",
|
|
},
|
|
{
|
|
// Write, home, prepend, enter. Prepends rewrites the line.
|
|
in: "cdef\x1b[Hab\r",
|
|
received: "> cdef" + // Initial input
|
|
"\x1b[4Da" + // Move cursor back, insert first char
|
|
"cdef" + // Copy over original string
|
|
"\x1b[4Dbcdef" + // Repeat for second char with copy
|
|
"\x1b[4D" + // Put cursor back in position to insert again
|
|
"\x1b[4C\r\n", // Put cursor at the end of the line and newline.
|
|
},
|
|
}
|
|
|
|
func TestRender(t *testing.T) {
|
|
for i, test := range renderTests {
|
|
for j := 1; j < len(test.in); j++ {
|
|
c := &MockTerminal{
|
|
toSend: []byte(test.in),
|
|
bytesPerRead: j,
|
|
}
|
|
ss := NewTerminal(c, "> ")
|
|
_, err := ss.ReadLine()
|
|
if err != test.err {
|
|
t.Errorf("Error resulting from test %d (%d bytes per read) was '%v', expected '%v'", i, j, err, test.err)
|
|
break
|
|
}
|
|
if test.received != string(c.received) {
|
|
t.Errorf("Results rendered from test %d (%d bytes per read) was '%s', expected '%s'", i, j, c.received, test.received)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPasswordNotSaved(t *testing.T) {
|
|
c := &MockTerminal{
|
|
toSend: []byte("password\r\x1b[A\r"),
|
|
bytesPerRead: 1,
|
|
}
|
|
ss := NewTerminal(c, "> ")
|
|
pw, _ := ss.ReadPassword("> ")
|
|
if pw != "password" {
|
|
t.Fatalf("failed to read password, got %s", pw)
|
|
}
|
|
line, _ := ss.ReadLine()
|
|
if len(line) > 0 {
|
|
t.Fatalf("password was saved in history")
|
|
}
|
|
}
|
|
|
|
var setSizeTests = []struct {
|
|
width, height int
|
|
}{
|
|
{40, 13},
|
|
{80, 24},
|
|
{132, 43},
|
|
}
|
|
|
|
func TestTerminalSetSize(t *testing.T) {
|
|
for _, setSize := range setSizeTests {
|
|
c := &MockTerminal{
|
|
toSend: []byte("password\r\x1b[A\r"),
|
|
bytesPerRead: 1,
|
|
}
|
|
ss := NewTerminal(c, "> ")
|
|
ss.SetSize(setSize.width, setSize.height)
|
|
pw, _ := ss.ReadPassword("Password: ")
|
|
if pw != "password" {
|
|
t.Fatalf("failed to read password, got %s", pw)
|
|
}
|
|
if string(c.received) != "Password: \r\n" {
|
|
t.Errorf("failed to set the temporary prompt expected %q, got %q", "Password: ", c.received)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadPasswordLineEnd(t *testing.T) {
|
|
type testType struct {
|
|
input string
|
|
want string
|
|
}
|
|
var tests = []testType{
|
|
{"\r\n", ""},
|
|
{"test\r\n", "test"},
|
|
{"test\r", "test"},
|
|
{"test\n", "test"},
|
|
{"testtesttesttes\n", "testtesttesttes"},
|
|
{"testtesttesttes\r\n", "testtesttesttes"},
|
|
{"testtesttesttesttest\n", "testtesttesttesttest"},
|
|
{"testtesttesttesttest\r\n", "testtesttesttesttest"},
|
|
{"\btest", "test"},
|
|
{"t\best", "est"},
|
|
{"te\bst", "tst"},
|
|
{"test\b", "tes"},
|
|
{"test\b\r\n", "tes"},
|
|
{"test\b\n", "tes"},
|
|
{"test\b\r", "tes"},
|
|
}
|
|
eol := "\n"
|
|
if runtime.GOOS == "windows" {
|
|
eol = "\r"
|
|
}
|
|
tests = append(tests, testType{eol, ""})
|
|
for _, test := range tests {
|
|
buf := new(bytes.Buffer)
|
|
if _, err := buf.WriteString(test.input); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
have, err := readPasswordLine(buf)
|
|
if err != nil {
|
|
t.Errorf("readPasswordLine(%q) failed: %v", test.input, err)
|
|
continue
|
|
}
|
|
if string(have) != test.want {
|
|
t.Errorf("readPasswordLine(%q) returns %q, but %q is expected", test.input, string(have), test.want)
|
|
continue
|
|
}
|
|
|
|
if _, err = buf.WriteString(test.input); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
have, err = readPasswordLine(buf)
|
|
if err != nil {
|
|
t.Errorf("readPasswordLine(%q) failed: %v", test.input, err)
|
|
continue
|
|
}
|
|
if string(have) != test.want {
|
|
t.Errorf("readPasswordLine(%q) returns %q, but %q is expected", test.input, string(have), test.want)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMakeRawState(t *testing.T) {
|
|
fd := int(os.Stdout.Fd())
|
|
if !IsTerminal(fd) {
|
|
t.Skip("stdout is not a terminal; skipping test")
|
|
}
|
|
|
|
st, err := GetState(fd)
|
|
if err != nil {
|
|
t.Fatalf("failed to get terminal state from GetState: %s", err)
|
|
}
|
|
|
|
if runtime.GOOS == "darwin" && (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64") {
|
|
t.Skip("MakeRaw not allowed on iOS; skipping test")
|
|
}
|
|
|
|
defer Restore(fd, st)
|
|
raw, err := MakeRaw(fd)
|
|
if err != nil {
|
|
t.Fatalf("failed to get terminal state from MakeRaw: %s", err)
|
|
}
|
|
|
|
if *st != *raw {
|
|
t.Errorf("states do not match; was %v, expected %v", raw, st)
|
|
}
|
|
}
|
|
|
|
func TestOutputNewlines(t *testing.T) {
|
|
// \n should be changed to \r\n in terminal output.
|
|
buf := new(bytes.Buffer)
|
|
term := NewTerminal(buf, ">")
|
|
|
|
term.Write([]byte("1\n2\n"))
|
|
output := string(buf.Bytes())
|
|
const expected = "1\r\n2\r\n"
|
|
|
|
if output != expected {
|
|
t.Errorf("incorrect output: was %q, expected %q", output, expected)
|
|
}
|
|
}
|