Skip to content

Commit

Permalink
fixes #546 terminfo tput escape sequence errors
Browse files Browse the repository at this point in the history
These fix errors discovered while implementing the same logic
in dcell.  While here, the conditional support was simplified
using a similar approach as used in dcell, and test cases were
added.
  • Loading branch information
gdamore committed Aug 31, 2022
1 parent 46afc52 commit c389caa
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 115 deletions.
173 changes: 58 additions & 115 deletions terminfo/terminfo.go
Expand Up @@ -240,15 +240,14 @@ const (
type stack []interface{}

func (st stack) Push(v interface{}) stack {
return append(st, v)
}

func (st stack) Pop() (interface{}, stack) {
if len(st) > 0 {
e := st[len(st)-1]
return e, st[:len(st)-1]
if b, ok := v.(bool); ok {
if b {
return append(st, 1)
} else {
return append(st, 0)
}
}
return 0, st
return append(st, v)
}

func (st stack) PopString() (string, stack) {
Expand All @@ -258,8 +257,6 @@ func (st stack) PopString() (string, stack) {
switch v := e.(type) {
case int:
s = strconv.Itoa(v)
case bool:
s = strconv.FormatBool(v)
case string:
s = v
}
Expand All @@ -275,12 +272,6 @@ func (st stack) PopInt() (int, stack) {
switch v := e.(type) {
case int:
i = v
case bool:
if v {
i = 1
} else {
i = 0
}
case string:
i, _ = strconv.Atoi(v)
}
Expand All @@ -289,42 +280,18 @@ func (st stack) PopInt() (int, stack) {
return 0, st
}

func (st stack) PopBool() (bool, stack) {
var b bool
if len(st) > 0 {
e := st[len(st)-1]
switch v := e.(type) {
case int:
b = v != 0
case bool:
b = v
case string:
b = v != "" && v != "false"
}
return b, st[:len(st)-1]
}
return false, st
}

// static vars
var svars [26]string

// paramsBuffer handles some persistent state for TParam. Technically we
// could probably dispense with this, but caching buffer arrays gives us
// a nice little performance boost. Furthermore, we know that TParam is
// rarely (never?) called re-entrantly, so we can just reuse the same
// buffers, making it thread-safe by stashing a lock.
type paramsBuffer struct {
out bytes.Buffer
buf bytes.Buffer
lk sync.Mutex
}

// Start initializes the params buffer with the initial string data.
// It also locks the paramsBuffer. The caller must call End() when
// finished.
func (pb *paramsBuffer) Start(s string) {
pb.lk.Lock()
pb.out.Reset()
pb.buf.Reset()
pb.buf.WriteString(s)
Expand All @@ -333,7 +300,6 @@ func (pb *paramsBuffer) Start(s string) {
// End returns the final output from TParam, but it also releases the lock.
func (pb *paramsBuffer) End() string {
s := pb.out.String()
pb.lk.Unlock()
return s
}

Expand All @@ -352,18 +318,16 @@ func (pb *paramsBuffer) PutString(s string) {
pb.out.WriteString(s)
}

var pb = &paramsBuffer{}

// TParm takes a terminfo parameterized string, such as setaf or cup, and
// evaluates the string, and returns the result with the parameter
// applied.
func (t *Terminfo) TParm(s string, p ...interface{}) string {
var stk stack
var a, b string
var a string
var ai, bi int
var ab bool
var dvars [26]string
var params [9]interface{}
var pb = &paramsBuffer{}

pb.Start(s)

Expand All @@ -373,7 +337,13 @@ func (t *Terminfo) TParm(s string, p ...interface{}) string {
params[i] = p[i]
}

nest := 0
const (
emit = iota
toEnd
toElse
)

skip := emit

for {

Expand All @@ -383,7 +353,9 @@ func (t *Terminfo) TParm(s string, p ...interface{}) string {
}

if ch != '%' {
pb.PutCh(ch)
if skip == emit {
pb.PutCh(ch)
}
continue
}

Expand All @@ -392,6 +364,17 @@ func (t *Terminfo) TParm(s string, p ...interface{}) string {
// XXX Error
break
}
if skip == toEnd {
if ch == ';' {
skip = emit
}
continue
} else if skip == toElse {
if ch == 'e' || ch == ';' {
skip = emit
}
continue
}

switch ch {
case '%': // quoted %
Expand All @@ -405,18 +388,23 @@ func (t *Terminfo) TParm(s string, p ...interface{}) string {
params[1] = i + 1
}

case 'c', 's':
// NB: these, and 'd' below are special cased for
case 's':
// NB: 's', 'c', and 'd' below are special cased for
// efficiency. They could be handled by the richer
// format support below, less efficiently.
a, stk = stk.PopString()
pb.PutString(a)

case 'c':
// Integer as special character.
ai, stk = stk.PopInt()
pb.PutCh(byte(ai))

case 'd':
ai, stk = stk.PopInt()
pb.PutString(strconv.Itoa(ai))

case '0', '1', '2', '3', '4', 'x', 'X', 'o', ':':
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'x', 'X', 'o', ':':
// This is pretty suboptimal, but this is rarely used.
// None of the mainstream terminals use any of this,
// and it would surprise me if this code is ever
Expand All @@ -438,9 +426,12 @@ func (t *Terminfo) TParm(s string, p ...interface{}) string {
case 'd', 'x', 'X', 'o':
ai, stk = stk.PopInt()
pb.PutString(fmt.Sprintf(f, ai))
case 'c', 's':
case 's':
a, stk = stk.PopString()
pb.PutString(fmt.Sprintf(f, a))
case 'c':
ai, stk = stk.PopInt()
pb.PutString(fmt.Sprintf(f, ai))
}

case 'p': // push parameter
Expand Down Expand Up @@ -468,10 +459,10 @@ func (t *Terminfo) TParm(s string, p ...interface{}) string {
stk = stk.Push(dvars[int(ch-'a')])
}

case '\'': // push(char)
case '\'': // push(char) - the integer value of it
ch, _ = pb.NextCh()
_, _ = pb.NextCh() // must be ' but we don't check
stk = stk.Push(string(ch))
stk = stk.Push(int(ch))

case '{': // push(int)
ai = 0
Expand Down Expand Up @@ -542,12 +533,12 @@ func (t *Terminfo) TParm(s string, p ...interface{}) string {

case '!': // logical NOT
ai, stk = stk.PopInt()
stk = stk.Push(ai != 0)
stk = stk.Push(ai == 0)

case '=': // numeric compare or string compare
b, stk = stk.PopString()
a, stk = stk.PopString()
stk = stk.Push(a == b)
case '=': // numeric compare
bi, stk = stk.PopInt()
ai, stk = stk.PopInt()
stk = stk.Push(ai == bi)

case '>': // greater than, numeric
bi, stk = stk.PopInt()
Expand All @@ -561,68 +552,20 @@ func (t *Terminfo) TParm(s string, p ...interface{}) string {

case '?': // start conditional

case ';':
skip = emit

case 't':
ab, stk = stk.PopBool()
if ab {
// just keep going
break
}
nest = 0
ifloop:
// this loop consumes everything until we hit our else,
// or the end of the conditional
for {
ch, err = pb.NextCh()
if err != nil {
break
}
if ch != '%' {
continue
}
ch, _ = pb.NextCh()
switch ch {
case ';':
if nest == 0 {
break ifloop
}
nest--
case '?':
nest++
case 'e':
if nest == 0 {
break ifloop
}
}
ai, stk = stk.PopInt()
if ai == 0 {
skip = toElse
}

case 'e':
// if we got here, it means we didn't use the else
// in the 't' case above, and we should skip until
// the end of the conditional
nest = 0
elloop:
for {
ch, err = pb.NextCh()
if err != nil {
break
}
if ch != '%' {
continue
}
ch, _ = pb.NextCh()
switch ch {
case ';':
if nest == 0 {
break elloop
}
nest--
case '?':
nest++
}
}

case ';': // endif
skip = toEnd

default:
pb.PutString("%" + string(ch))
}
}

Expand Down
51 changes: 51 additions & 0 deletions terminfo/terminfo_test.go
Expand Up @@ -63,6 +63,57 @@ func TestTerminfoExpansion(t *testing.T) {
if ti.TParm(ti.SetFg, 200) != "\x1b[38;5;200m" {
t.Error("SetFg(200) failed")
}

type testCase struct {
expect string
format string
params []interface{}
}

cases := []testCase{
{expect: "0a", format: "%p1%02x", params: []interface{}{10}},
{expect: "0A", format: "%p1%02X", params: []interface{}{10}},
{expect: "A", format: "%p1%c", params: []interface{}{65}},
{expect: "A", format: "%'A'%c", params: []interface{}{}},
{expect: "65", format: "%'A'%d", params: []interface{}{}},
{expect: "7", format: "%i%p1%p2%+%d", params: []interface{}{2, 3}},
{expect: "abc", format: "%p1%s", params: []interface{}{"abc"}},
{expect: "1%d", format: "1%%d", params: []interface{}{}},
{expect: "abc", format: "%p1%s%", params: []interface{}{"abc"}}, // unterminated %
{expect: " abc", format: "%p1%5s", params: []interface{}{"abc"}},
{expect: "abc ", format: "%p1%:-5s", params: []interface{}{"abc"}},
{expect: "15", format: "%{3}%p1%*%d", params: []interface{}{5}},
{expect: " A", format: "%p1%2c", params: []interface{}{65}},
{expect: "4", format: "%p1%l%d", params: []interface{}{"four"}},
{expect: "0", format: "%pA%d", params: []interface{}{}}, // missing/invalid parameter
{expect: "5", format: "%p1%p2%/%d", params: []interface{}{15, 3}},
{expect: "0", format: "%p1%p2%/%d", params: []interface{}{3, 15}},
{expect: "0", format: "%p1%p2%/%d", params: []interface{}{3, 0}},
{expect: "3", format: "%p1%p2%m%d", params: []interface{}{15, 4}},
{expect: "0", format: "%p1%p2%m%d", params: []interface{}{3, 0}},
{expect: "2", format: "%p1%Pa%{4}%{3}%ga%d", params: []interface{}{2}},
{expect: "2", format: "%p1%PA%{4}%{3}%gA%d", params: []interface{}{2}},
{expect: "0", format: "%p1%PA%{4}%{3}%ga%d", params: []interface{}{2}},
{expect: "0", format: "%p1%Pz%{4}%{3}%gZ%d", params: []interface{}{2}},
{expect: "0", format: "%d", params: []interface{}{}}, // underflow
{expect: "", format: "%s", params: []interface{}{}}, // underflow
{expect: "1", format: "%p1%p2%=%d", params: []interface{}{3, 3}},
{expect: "0", format: "%p1%p2%=%d", params: []interface{}{3, 4}},
{expect: "1", format: "%p1%p2%=%!%d", params: []interface{}{3, 4}},
{expect: "1", format: "%p1%p2%>%d", params: []interface{}{4, 3}},
{expect: "3", format: "%p1%p2%|%d", params: []interface{}{1, 2}},
{expect: "2", format: "%p1%p2%&%d", params: []interface{}{2, 3}},
{expect: "1", format: "%p1%p2%^%d", params: []interface{}{2, 3}},
{expect: "f", format: "%p1%~%{255}%&%x", params: []interface{}{0xf0}},
{expect: "%Z", format: "%Z", params: []interface{}{2, 3}}, // unknown sequence
}

for i := range cases {
if res := ti.TParm(cases[i].format, cases[i].params...); res != cases[i].expect {
t.Errorf("Format case %d failed: Format %q got %q", i, cases[i].format, res)
}
}
t.Logf("Tested %d cases", len(cases))
}

func TestTerminfoDelay(t *testing.T) {
Expand Down

0 comments on commit c389caa

Please sign in to comment.