From 83df9e28ab7952e720c30a3fcc307f76ad783c41 Mon Sep 17 00:00:00 2001 From: Garrett D'Amore Date: Wed, 31 Aug 2022 10:43:49 -0700 Subject: [PATCH] fixes #546 terminfo tput escape sequence errors 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. --- terminfo/terminfo.go | 173 +++++++++++++------------------------- terminfo/terminfo_test.go | 51 +++++++++++ 2 files changed, 109 insertions(+), 115 deletions(-) diff --git a/terminfo/terminfo.go b/terminfo/terminfo.go index 7028b51b..cf157864 100644 --- a/terminfo/terminfo.go +++ b/terminfo/terminfo.go @@ -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) { @@ -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 } @@ -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) } @@ -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) @@ -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 } @@ -352,18 +318,16 @@ func (pb *paramsBuffer) PutString(s string) { pb.out.WriteString(s) } -var pb = ¶msBuffer{} - // 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 = ¶msBuffer{} pb.Start(s) @@ -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 { @@ -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 } @@ -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 % @@ -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 @@ -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 @@ -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 @@ -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() @@ -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)) } } diff --git a/terminfo/terminfo_test.go b/terminfo/terminfo_test.go index 1032db6b..ae23d14b 100644 --- a/terminfo/terminfo_test.go +++ b/terminfo/terminfo_test.go @@ -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) {