feat(protocol): add bounded terminal capability probe parser
- commit
- 024b0eac9d311a16d6e80b1643f916cece34a9ad
- author
- brice · · 2026-08-20 21:05 UTC
- parent
- 0780e0d
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +227 -0
A protocol/terminalquery/query.go +178 -0
| @@ -0,0 +1,178 @@ | |||
| 1 | + | // Package terminalquery provides bounded parsers for terminal capability | |
| 2 | + | // probes. Probe responses are terminal input, not application input: callers | |
| 3 | + | // can remove only the two exact responses while replaying every unrelated byte. | |
| 4 | + | package terminalquery | |
| 5 | + | ||
| 6 | + | import "bytes" | |
| 7 | + | ||
| 8 | + | const ( | |
| 9 | + | // KittyGraphicsQuery asks a terminal whether it understands the Kitty | |
| 10 | + | // graphics protocol. The image id is deliberately reserved for probing and | |
| 11 | + | // the query carries no image payload. | |
| 12 | + | KittyGraphicsQuery = "\x1b_Gi=31,s=1,v=1,a=q;\x1b\\" | |
| 13 | + | // DeviceAttributesQuery is the primary DA1 query. It is paired with the | |
| 14 | + | // Kitty query so a graphics declaration requires both terminal identity and | |
| 15 | + | // protocol support. | |
| 16 | + | DeviceAttributesQuery = "\x1b[c" | |
| 17 | + | ||
| 18 | + | maxProbeResponseBytes = 256 | |
| 19 | + | ) | |
| 20 | + | ||
| 21 | + | // Probe incrementally recognizes the bounded responses to KittyGraphicsQuery | |
| 22 | + | // and DeviceAttributesQuery. Feed returns bytes that are not recognized probe | |
| 23 | + | // responses in their original order. A response may be split across any | |
| 24 | + | // number of calls. | |
| 25 | + | type Probe struct { | |
| 26 | + | pending []byte | |
| 27 | + | kitty bool | |
| 28 | + | da1 bool | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // Feed consumes one terminal input fragment and returns unrelated input. The | |
| 32 | + | // returned slice is owned by the Probe and is invalidated by the next call. | |
| 33 | + | func (p *Probe) Feed(data []byte) []byte { | |
| 34 | + | if p == nil || len(data) == 0 { | |
| 35 | + | return append([]byte(nil), data...) | |
| 36 | + | } | |
| 37 | + | p.pending = append(p.pending, data...) | |
| 38 | + | return p.scan(false) | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | // Finish flushes an incomplete or unrelated prefix without recognizing any | |
| 42 | + | // further response. It is used when the bounded probe deadline expires. | |
| 43 | + | func (p *Probe) Finish() []byte { | |
| 44 | + | if p == nil { | |
| 45 | + | return nil | |
| 46 | + | } | |
| 47 | + | return p.scan(true) | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // Ready reports whether both independent capability responses were observed. | |
| 51 | + | func (p *Probe) Ready() bool { return p != nil && p.kitty && p.da1 } | |
| 52 | + | ||
| 53 | + | // KittyGraphics reports whether the Kitty graphics response was observed. | |
| 54 | + | func (p *Probe) KittyGraphics() bool { return p != nil && p.kitty } | |
| 55 | + | ||
| 56 | + | // DA1 reports whether a primary device-attributes response was observed. | |
| 57 | + | func (p *Probe) DA1() bool { return p != nil && p.da1 } | |
| 58 | + | ||
| 59 | + | func (p *Probe) scan(flush bool) []byte { | |
| 60 | + | var unrelated []byte | |
| 61 | + | for len(p.pending) != 0 { | |
| 62 | + | kittyStart := bytes.Index(p.pending, []byte("\x1b_G")) | |
| 63 | + | da1Start := bytes.Index(p.pending, []byte("\x1b[")) | |
| 64 | + | start := -1 | |
| 65 | + | if kittyStart >= 0 { | |
| 66 | + | start = kittyStart | |
| 67 | + | } | |
| 68 | + | if da1Start >= 0 && (start < 0 || da1Start < start) { | |
| 69 | + | start = da1Start | |
| 70 | + | } | |
| 71 | + | if start < 0 { | |
| 72 | + | if flush || len(p.pending) > 2 { | |
| 73 | + | keep := 0 | |
| 74 | + | if !flush { | |
| 75 | + | keep = suffixPrefixLen(p.pending) | |
| 76 | + | } | |
| 77 | + | if len(p.pending) > keep { | |
| 78 | + | unrelated = append(unrelated, p.pending[:len(p.pending)-keep]...) | |
| 79 | + | p.pending = p.pending[len(p.pending)-keep:] | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | break | |
| 83 | + | } | |
| 84 | + | if start > 0 { | |
| 85 | + | unrelated = append(unrelated, p.pending[:start]...) | |
| 86 | + | p.pending = p.pending[start:] | |
| 87 | + | } | |
| 88 | + | if p.pending[1] == '_' { | |
| 89 | + | end, complete := probeTerminator(p.pending) | |
| 90 | + | if !complete { | |
| 91 | + | if flush || len(p.pending) > maxProbeResponseBytes { | |
| 92 | + | unrelated = append(unrelated, p.pending[0]) | |
| 93 | + | p.pending = p.pending[1:] | |
| 94 | + | continue | |
| 95 | + | } | |
| 96 | + | break | |
| 97 | + | } | |
| 98 | + | response := p.pending[:end] | |
| 99 | + | if validKittyResponse(response) { | |
| 100 | + | p.kitty = true | |
| 101 | + | p.pending = p.pending[end:] | |
| 102 | + | continue | |
| 103 | + | } | |
| 104 | + | unrelated = append(unrelated, p.pending[0]) | |
| 105 | + | p.pending = p.pending[1:] | |
| 106 | + | continue | |
| 107 | + | } | |
| 108 | + | end, complete := da1Terminator(p.pending) | |
| 109 | + | if !complete { | |
| 110 | + | if flush || len(p.pending) > maxProbeResponseBytes { | |
| 111 | + | unrelated = append(unrelated, p.pending[0]) | |
| 112 | + | p.pending = p.pending[1:] | |
| 113 | + | continue | |
| 114 | + | } | |
| 115 | + | break | |
| 116 | + | } | |
| 117 | + | response := p.pending[:end] | |
| 118 | + | if validDA1Response(response) { | |
| 119 | + | p.da1 = true | |
| 120 | + | p.pending = p.pending[end:] | |
| 121 | + | continue | |
| 122 | + | } | |
| 123 | + | unrelated = append(unrelated, p.pending[0]) | |
| 124 | + | p.pending = p.pending[1:] | |
| 125 | + | } | |
| 126 | + | return unrelated | |
| 127 | + | } | |
| 128 | + | ||
| 129 | + | func probeTerminator(data []byte) (int, bool) { | |
| 130 | + | for i := 3; i+1 < len(data) && i <= maxProbeResponseBytes; i++ { | |
| 131 | + | if data[i] == '\x1b' && data[i+1] == '\\' { | |
| 132 | + | return i + 2, true | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | return 0, false | |
| 136 | + | } | |
| 137 | + | ||
| 138 | + | func da1Terminator(data []byte) (int, bool) { | |
| 139 | + | for i := 2; i < len(data) && i <= maxProbeResponseBytes; i++ { | |
| 140 | + | if data[i] >= 0x40 && data[i] <= 0x7e { | |
| 141 | + | return i + 1, true | |
| 142 | + | } | |
| 143 | + | } | |
| 144 | + | return 0, false | |
| 145 | + | } | |
| 146 | + | ||
| 147 | + | func validKittyResponse(data []byte) bool { | |
| 148 | + | if len(data) < len("\x1b_Gi=31;OK\x1b\\") || !bytes.HasPrefix(data, []byte("\x1b_G")) || !bytes.HasSuffix(data, []byte("\x1b\\")) { | |
| 149 | + | return false | |
| 150 | + | } | |
| 151 | + | head := data[3 : len(data)-2] | |
| 152 | + | return bytes.Contains(head, []byte("i=31")) && bytes.Contains(head, []byte("OK")) | |
| 153 | + | } | |
| 154 | + | ||
| 155 | + | func validDA1Response(data []byte) bool { | |
| 156 | + | if len(data) < 4 || !bytes.HasPrefix(data, []byte("\x1b[")) || data[len(data)-1] != 'c' { | |
| 157 | + | return false | |
| 158 | + | } | |
| 159 | + | // DA1 is a primary response. Secondary DA (ESC[>...c) is deliberately | |
| 160 | + | // not sufficient for a direct graphics declaration. | |
| 161 | + | return data[2] == '?' && len(data) <= maxProbeResponseBytes | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | func suffixPrefixLen(data []byte) int { | |
| 165 | + | for n := min(len(data), 2); n > 0; n-- { | |
| 166 | + | if bytes.HasPrefix([]byte("\x1b_G"), data[len(data)-n:]) || bytes.HasPrefix([]byte("\x1b["), data[len(data)-n:]) { | |
| 167 | + | return n | |
| 168 | + | } | |
| 169 | + | } | |
| 170 | + | return 0 | |
| 171 | + | } | |
| 172 | + | ||
| 173 | + | func min(a, b int) int { | |
| 174 | + | if a < b { | |
| 175 | + | return a | |
| 176 | + | } | |
| 177 | + | return b | |
| 178 | + | } | |
| @@ -0,0 +1,178 @@ | |||||
| 1 | + | // Package terminalquery provides bounded parsers for terminal capability | |||
| 2 | + | // probes. Probe responses are terminal input, not application input: callers | |||
| 3 | + | // can remove only the two exact responses while replaying every unrelated byte. | |||
| 4 | + | package terminalquery | |||
| 5 | + | ||||
| 6 | + | import "bytes" | |||
| 7 | + | ||||
| 8 | + | const ( | |||
| 9 | + | // KittyGraphicsQuery asks a terminal whether it understands the Kitty | |||
| 10 | + | // graphics protocol. The image id is deliberately reserved for probing and | |||
| 11 | + | // the query carries no image payload. | |||
| 12 | + | KittyGraphicsQuery = "\x1b_Gi=31,s=1,v=1,a=q;\x1b\\" | |||
| 13 | + | // DeviceAttributesQuery is the primary DA1 query. It is paired with the | |||
| 14 | + | // Kitty query so a graphics declaration requires both terminal identity and | |||
| 15 | + | // protocol support. | |||
| 16 | + | DeviceAttributesQuery = "\x1b[c" | |||
| 17 | + | ||||
| 18 | + | maxProbeResponseBytes = 256 | |||
| 19 | + | ) | |||
| 20 | + | ||||
| 21 | + | // Probe incrementally recognizes the bounded responses to KittyGraphicsQuery | |||
| 22 | + | // and DeviceAttributesQuery. Feed returns bytes that are not recognized probe | |||
| 23 | + | // responses in their original order. A response may be split across any | |||
| 24 | + | // number of calls. | |||
| 25 | + | type Probe struct { | |||
| 26 | + | pending []byte | |||
| 27 | + | kitty bool | |||
| 28 | + | da1 bool | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // Feed consumes one terminal input fragment and returns unrelated input. The | |||
| 32 | + | // returned slice is owned by the Probe and is invalidated by the next call. | |||
| 33 | + | func (p *Probe) Feed(data []byte) []byte { | |||
| 34 | + | if p == nil || len(data) == 0 { | |||
| 35 | + | return append([]byte(nil), data...) | |||
| 36 | + | } | |||
| 37 | + | p.pending = append(p.pending, data...) | |||
| 38 | + | return p.scan(false) | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | // Finish flushes an incomplete or unrelated prefix without recognizing any | |||
| 42 | + | // further response. It is used when the bounded probe deadline expires. | |||
| 43 | + | func (p *Probe) Finish() []byte { | |||
| 44 | + | if p == nil { | |||
| 45 | + | return nil | |||
| 46 | + | } | |||
| 47 | + | return p.scan(true) | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // Ready reports whether both independent capability responses were observed. | |||
| 51 | + | func (p *Probe) Ready() bool { return p != nil && p.kitty && p.da1 } | |||
| 52 | + | ||||
| 53 | + | // KittyGraphics reports whether the Kitty graphics response was observed. | |||
| 54 | + | func (p *Probe) KittyGraphics() bool { return p != nil && p.kitty } | |||
| 55 | + | ||||
| 56 | + | // DA1 reports whether a primary device-attributes response was observed. | |||
| 57 | + | func (p *Probe) DA1() bool { return p != nil && p.da1 } | |||
| 58 | + | ||||
| 59 | + | func (p *Probe) scan(flush bool) []byte { | |||
| 60 | + | var unrelated []byte | |||
| 61 | + | for len(p.pending) != 0 { | |||
| 62 | + | kittyStart := bytes.Index(p.pending, []byte("\x1b_G")) | |||
| 63 | + | da1Start := bytes.Index(p.pending, []byte("\x1b[")) | |||
| 64 | + | start := -1 | |||
| 65 | + | if kittyStart >= 0 { | |||
| 66 | + | start = kittyStart | |||
| 67 | + | } | |||
| 68 | + | if da1Start >= 0 && (start < 0 || da1Start < start) { | |||
| 69 | + | start = da1Start | |||
| 70 | + | } | |||
| 71 | + | if start < 0 { | |||
| 72 | + | if flush || len(p.pending) > 2 { | |||
| 73 | + | keep := 0 | |||
| 74 | + | if !flush { | |||
| 75 | + | keep = suffixPrefixLen(p.pending) | |||
| 76 | + | } | |||
| 77 | + | if len(p.pending) > keep { | |||
| 78 | + | unrelated = append(unrelated, p.pending[:len(p.pending)-keep]...) | |||
| 79 | + | p.pending = p.pending[len(p.pending)-keep:] | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | break | |||
| 83 | + | } | |||
| 84 | + | if start > 0 { | |||
| 85 | + | unrelated = append(unrelated, p.pending[:start]...) | |||
| 86 | + | p.pending = p.pending[start:] | |||
| 87 | + | } | |||
| 88 | + | if p.pending[1] == '_' { | |||
| 89 | + | end, complete := probeTerminator(p.pending) | |||
| 90 | + | if !complete { | |||
| 91 | + | if flush || len(p.pending) > maxProbeResponseBytes { | |||
| 92 | + | unrelated = append(unrelated, p.pending[0]) | |||
| 93 | + | p.pending = p.pending[1:] | |||
| 94 | + | continue | |||
| 95 | + | } | |||
| 96 | + | break | |||
| 97 | + | } | |||
| 98 | + | response := p.pending[:end] | |||
| 99 | + | if validKittyResponse(response) { | |||
| 100 | + | p.kitty = true | |||
| 101 | + | p.pending = p.pending[end:] | |||
| 102 | + | continue | |||
| 103 | + | } | |||
| 104 | + | unrelated = append(unrelated, p.pending[0]) | |||
| 105 | + | p.pending = p.pending[1:] | |||
| 106 | + | continue | |||
| 107 | + | } | |||
| 108 | + | end, complete := da1Terminator(p.pending) | |||
| 109 | + | if !complete { | |||
| 110 | + | if flush || len(p.pending) > maxProbeResponseBytes { | |||
| 111 | + | unrelated = append(unrelated, p.pending[0]) | |||
| 112 | + | p.pending = p.pending[1:] | |||
| 113 | + | continue | |||
| 114 | + | } | |||
| 115 | + | break | |||
| 116 | + | } | |||
| 117 | + | response := p.pending[:end] | |||
| 118 | + | if validDA1Response(response) { | |||
| 119 | + | p.da1 = true | |||
| 120 | + | p.pending = p.pending[end:] | |||
| 121 | + | continue | |||
| 122 | + | } | |||
| 123 | + | unrelated = append(unrelated, p.pending[0]) | |||
| 124 | + | p.pending = p.pending[1:] | |||
| 125 | + | } | |||
| 126 | + | return unrelated | |||
| 127 | + | } | |||
| 128 | + | ||||
| 129 | + | func probeTerminator(data []byte) (int, bool) { | |||
| 130 | + | for i := 3; i+1 < len(data) && i <= maxProbeResponseBytes; i++ { | |||
| 131 | + | if data[i] == '\x1b' && data[i+1] == '\\' { | |||
| 132 | + | return i + 2, true | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | return 0, false | |||
| 136 | + | } | |||
| 137 | + | ||||
| 138 | + | func da1Terminator(data []byte) (int, bool) { | |||
| 139 | + | for i := 2; i < len(data) && i <= maxProbeResponseBytes; i++ { | |||
| 140 | + | if data[i] >= 0x40 && data[i] <= 0x7e { | |||
| 141 | + | return i + 1, true | |||
| 142 | + | } | |||
| 143 | + | } | |||
| 144 | + | return 0, false | |||
| 145 | + | } | |||
| 146 | + | ||||
| 147 | + | func validKittyResponse(data []byte) bool { | |||
| 148 | + | if len(data) < len("\x1b_Gi=31;OK\x1b\\") || !bytes.HasPrefix(data, []byte("\x1b_G")) || !bytes.HasSuffix(data, []byte("\x1b\\")) { | |||
| 149 | + | return false | |||
| 150 | + | } | |||
| 151 | + | head := data[3 : len(data)-2] | |||
| 152 | + | return bytes.Contains(head, []byte("i=31")) && bytes.Contains(head, []byte("OK")) | |||
| 153 | + | } | |||
| 154 | + | ||||
| 155 | + | func validDA1Response(data []byte) bool { | |||
| 156 | + | if len(data) < 4 || !bytes.HasPrefix(data, []byte("\x1b[")) || data[len(data)-1] != 'c' { | |||
| 157 | + | return false | |||
| 158 | + | } | |||
| 159 | + | // DA1 is a primary response. Secondary DA (ESC[>...c) is deliberately | |||
| 160 | + | // not sufficient for a direct graphics declaration. | |||
| 161 | + | return data[2] == '?' && len(data) <= maxProbeResponseBytes | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | func suffixPrefixLen(data []byte) int { | |||
| 165 | + | for n := min(len(data), 2); n > 0; n-- { | |||
| 166 | + | if bytes.HasPrefix([]byte("\x1b_G"), data[len(data)-n:]) || bytes.HasPrefix([]byte("\x1b["), data[len(data)-n:]) { | |||
| 167 | + | return n | |||
| 168 | + | } | |||
| 169 | + | } | |||
| 170 | + | return 0 | |||
| 171 | + | } | |||
| 172 | + | ||||
| 173 | + | func min(a, b int) int { | |||
| 174 | + | if a < b { | |||
| 175 | + | return a | |||
| 176 | + | } | |||
| 177 | + | return b | |||
| 178 | + | } | |||
A protocol/terminalquery/query_test.go +49 -0
| @@ -0,0 +1,49 @@ | |||
| 1 | + | package terminalquery | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/require" | |
| 7 | + | ) | |
| 8 | + | ||
| 9 | + | func TestProbeRequiresKittyAndDA1(t *testing.T) { | |
| 10 | + | for _, tc := range []struct { | |
| 11 | + | name string | |
| 12 | + | data string | |
| 13 | + | kitty bool | |
| 14 | + | da1 bool | |
| 15 | + | unrelated string | |
| 16 | + | }{ | |
| 17 | + | {name: "both and input", data: "x\x1b[?1;2c" + "\x1b_Gi=31;OK\x1b\\y", kitty: true, da1: true, unrelated: "xy"}, | |
| 18 | + | {name: "secondary DA is insufficient", data: "\x1b[>1;2c\x1b_Gi=31;OK\x1b\\", kitty: true, unrelated: "\x1b[>1;2c"}, | |
| 19 | + | {name: "ordinary input", data: "hello", unrelated: "hello"}, | |
| 20 | + | } { | |
| 21 | + | t.Run(tc.name, func(t *testing.T) { | |
| 22 | + | var p Probe | |
| 23 | + | got := append(p.Feed([]byte(tc.data)), p.Finish()...) | |
| 24 | + | require.Equal(t, tc.kitty, p.KittyGraphics()) | |
| 25 | + | require.Equal(t, tc.da1, p.DA1()) | |
| 26 | + | require.Equal(t, tc.unrelated, string(got)) | |
| 27 | + | }) | |
| 28 | + | } | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | func TestProbeRecognizesFragmentedResponsesAndReplaysInput(t *testing.T) { | |
| 32 | + | var p Probe | |
| 33 | + | var unrelated []byte | |
| 34 | + | for _, fragment := range []string{"a\x1b", "[?1;2", "c", "b\x1b_Gi=31;", "OK\x1b", "\\z"} { | |
| 35 | + | unrelated = append(unrelated, p.Feed([]byte(fragment))...) | |
| 36 | + | } | |
| 37 | + | unrelated = append(unrelated, p.Finish()...) | |
| 38 | + | require.True(t, p.Ready()) | |
| 39 | + | require.Equal(t, "abz", string(unrelated)) | |
| 40 | + | } | |
| 41 | + | ||
| 42 | + | func TestProbeBoundsUnterminatedResponse(t *testing.T) { | |
| 43 | + | var p Probe | |
| 44 | + | input := append([]byte("prefix"), []byte("\x1b_G")...) | |
| 45 | + | input = append(input, make([]byte, maxProbeResponseBytes+1)...) | |
| 46 | + | got := append(p.Feed(input), p.Finish()...) | |
| 47 | + | require.False(t, p.Ready()) | |
| 48 | + | require.Equal(t, input, got) | |
| 49 | + | } | |
| @@ -0,0 +1,49 @@ | |||||
| 1 | + | package terminalquery | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/require" | |||
| 7 | + | ) | |||
| 8 | + | ||||
| 9 | + | func TestProbeRequiresKittyAndDA1(t *testing.T) { | |||
| 10 | + | for _, tc := range []struct { | |||
| 11 | + | name string | |||
| 12 | + | data string | |||
| 13 | + | kitty bool | |||
| 14 | + | da1 bool | |||
| 15 | + | unrelated string | |||
| 16 | + | }{ | |||
| 17 | + | {name: "both and input", data: "x\x1b[?1;2c" + "\x1b_Gi=31;OK\x1b\\y", kitty: true, da1: true, unrelated: "xy"}, | |||
| 18 | + | {name: "secondary DA is insufficient", data: "\x1b[>1;2c\x1b_Gi=31;OK\x1b\\", kitty: true, unrelated: "\x1b[>1;2c"}, | |||
| 19 | + | {name: "ordinary input", data: "hello", unrelated: "hello"}, | |||
| 20 | + | } { | |||
| 21 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 22 | + | var p Probe | |||
| 23 | + | got := append(p.Feed([]byte(tc.data)), p.Finish()...) | |||
| 24 | + | require.Equal(t, tc.kitty, p.KittyGraphics()) | |||
| 25 | + | require.Equal(t, tc.da1, p.DA1()) | |||
| 26 | + | require.Equal(t, tc.unrelated, string(got)) | |||
| 27 | + | }) | |||
| 28 | + | } | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | func TestProbeRecognizesFragmentedResponsesAndReplaysInput(t *testing.T) { | |||
| 32 | + | var p Probe | |||
| 33 | + | var unrelated []byte | |||
| 34 | + | for _, fragment := range []string{"a\x1b", "[?1;2", "c", "b\x1b_Gi=31;", "OK\x1b", "\\z"} { | |||
| 35 | + | unrelated = append(unrelated, p.Feed([]byte(fragment))...) | |||
| 36 | + | } | |||
| 37 | + | unrelated = append(unrelated, p.Finish()...) | |||
| 38 | + | require.True(t, p.Ready()) | |||
| 39 | + | require.Equal(t, "abz", string(unrelated)) | |||
| 40 | + | } | |||
| 41 | + | ||||
| 42 | + | func TestProbeBoundsUnterminatedResponse(t *testing.T) { | |||
| 43 | + | var p Probe | |||
| 44 | + | input := append([]byte("prefix"), []byte("\x1b_G")...) | |||
| 45 | + | input = append(input, make([]byte, maxProbeResponseBytes+1)...) | |||
| 46 | + | got := append(p.Feed(input), p.Finish()...) | |||
| 47 | + | require.False(t, p.Ready()) | |||
| 48 | + | require.Equal(t, input, got) | |||
| 49 | + | } | |||