Skip to content

Commit d327a2b

Browse files
committed
Share one VT500 transition table across parsers; clamp oversized CSI/DCS/OSC params.
Based on a patch by https://github.com/klc Tuned it a bit: I took a slightly different path based on your patch, but with some changes: - CSI/DCS values will cap at UInt16.max - CSI/DCS sequences exceeding 24 parameters are dropped (like Ghostty does) - Overflow state persists correctly across input chunks. - OSC selectors now reject malformed values and true Int overflow instead of aliasing to OSC 65535. - OSC 4 palette indices use the strict decimal parser instead. - Made transition table immutable after construction (so we get a let instead of a var) Original PR: #608
1 parent 87a7888 commit d327a2b

2 files changed

Lines changed: 85 additions & 46 deletions

File tree

Sources/SwiftTerm/EscapeSequenceParser.swift

Lines changed: 83 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -84,34 +84,41 @@ enum ParserAction : UInt8 {
8484
case dcsUnhook
8585
}
8686

87-
class TransitionTable {
87+
final class TransitionTable {
8888
// data is packed like this:
8989
// currentState << 8 | characterCode --> action << 4 | nextState
90-
var table: [UInt8]
90+
let table: [UInt8]
9191

92+
fileprivate init (_ table: [UInt8])
93+
{
94+
self.table = table
95+
}
96+
97+
subscript (idx: Int) -> UInt8 {
98+
table [idx]
99+
}
100+
}
101+
102+
fileprivate struct TransitionTableBuilder {
103+
var table: [UInt8]
104+
92105
init (len: Int)
93106
{
94107
table = Array.init (repeating: 0, count: len)
95108
}
96109

97-
func add (code: UInt8, state: ParserState, action: ParserAction, next: ParserState)
110+
mutating func add (code: UInt8, state: ParserState, action: ParserAction, next: ParserState)
98111
{
99112
let v = (UInt8 (action.rawValue) << 4) | next.rawValue
100113
table [(Int (state.rawValue) << 8) | Int(code)] = v
101114
}
102115

103-
func add (codes: [UInt8], state: ParserState, action: ParserAction, next: ParserState)
116+
mutating func add (codes: [UInt8], state: ParserState, action: ParserAction, next: ParserState)
104117
{
105118
for c in codes {
106119
add (code: c, state: state, action: action, next: next)
107120
}
108121
}
109-
110-
subscript (idx: Int) -> UInt8 {
111-
get {
112-
return table [idx]
113-
}
114-
}
115122
}
116123

117124
protocol DcsHandler {
@@ -157,7 +164,7 @@ public class EscapeSequenceParser {
157164

158165
static func buildVt500TransitionTable () -> TransitionTable
159166
{
160-
let table = TransitionTable(len: 4095)
167+
var table = TransitionTableBuilder(len: 4095)
161168
let states = rinclusive(low: .ground, high: .dcsPassthrough)
162169

163170
// table with default transition
@@ -282,7 +289,7 @@ public class EscapeSequenceParser {
282289
table.add (codes: [0x1b, 0x9c], state: .dcsPassthrough, action: .dcsUnhook, next: .ground)
283290
table.add (code: NonAsciiPrintable, state: .oscString, action: .oscPut, next: .oscString)
284291
table.add (code: NonAsciiPrintable, state: .apcString, action: .oscPut, next: .apcString)
285-
return table
292+
return TransitionTable(table.table)
286293
}
287294

288295
// Array of parameters, and "collect" string
@@ -338,20 +345,38 @@ public class EscapeSequenceParser {
338345
var _pars: [Int]
339346
var _parsTxt: [UInt8]
340347
var _collect: cstring
348+
var _parameterLimitExceeded: Bool
341349
var printHandler: PrintHandler = { (slice : ArraySlice<UInt8>) -> () in }
342350
var printStateReset: () -> () = { }
343351

344-
var table: TransitionTable
352+
private static let sharedVt500Table = EscapeSequenceParser.buildVt500TransitionTable()
353+
354+
/// CSI and DCS parameter values use the same 16-bit saturating range as Ghostty.
355+
static let maximumParameterValue = Int(UInt16.max)
356+
357+
/// Sequences beyond this limit are dropped instead of growing parser state without bound.
358+
static let maximumParameterCount = 24
359+
let table: TransitionTable
345360

346361
init (terminal: Terminal? = nil)
347362
{
348363
self.terminal = terminal
349-
table = EscapeSequenceParser.buildVt500TransitionTable()
364+
table = EscapeSequenceParser.sharedVt500Table
350365
_osc = []
351366
_apc = []
352367
_pars = [0]
353368
_parsTxt = []
354369
_collect = []
370+
_parameterLimitExceeded = false
371+
}
372+
373+
@inline(__always)
374+
private static func appendingParameterDigit(_ code: UInt8, to currentValue: Int) -> Int {
375+
let digit = Int(code) - 48
376+
if currentValue > (maximumParameterValue - digit) / 10 {
377+
return maximumParameterValue
378+
}
379+
return currentValue * 10 + digit
355380
}
356381

357382
// MARK: - Dispatch Methods
@@ -596,7 +621,9 @@ public class EscapeSequenceParser {
596621
_osc = []
597622
_apc = []
598623
_pars = [0]
624+
_parsTxt = []
599625
_collect = []
626+
_parameterLimitExceeded = false
600627
activeDcsHandler = nil
601628
printStateReset()
602629
}
@@ -640,6 +667,7 @@ public class EscapeSequenceParser {
640667
var collect = self._collect
641668
var pars = self._pars
642669
var parsTxt = self._parsTxt
670+
var parameterLimitExceeded = self._parameterLimitExceeded
643671
let tableData = table.table
644672
var dcsHandler = activeDcsHandler
645673

@@ -668,11 +696,11 @@ public class EscapeSequenceParser {
668696

669697
// shortcut for CSI params
670698
if currentState == .csiParam && (code > 0x2f && code < 0x3a) {
671-
let newV = pars [pars.count - 1] * 10 + Int(code) - 48
672-
673-
// Prevent attempts at overflowing - crash
674-
let willOverflow = newV > ((Int.max/10)-10)
675-
pars [pars.count - 1] = willOverflow ? 0 : newV
699+
if !parameterLimitExceeded {
700+
pars [pars.count - 1] = EscapeSequenceParser.appendingParameterDigit(
701+
code,
702+
to: pars [pars.count - 1])
703+
}
676704
i += 1
677705
continue
678706
}
@@ -738,18 +766,22 @@ public class EscapeSequenceParser {
738766
error = false;
739767
}
740768
case .csiDispatch:
741-
_parsTxt = parsTxt
742-
dispatchCsi(code: code, pars: pars, collect: collect)
769+
if !parameterLimitExceeded {
770+
_parsTxt = parsTxt
771+
dispatchCsi(code: code, pars: pars, collect: collect)
772+
}
743773
case .param:
744774
if code == 0x3b || code == 0x3a {
745-
parsTxt.append(code)
746-
pars.append (0)
747-
} else {
748-
let newV = pars [pars.count - 1] * 10 + Int(code) - 48
749-
750-
// Prevent attempts at overflowing - crash
751-
let willOverflow = newV > ((Int.max/10)-10)
752-
pars [pars.count - 1] = willOverflow ? 0 : newV
775+
if pars.count >= EscapeSequenceParser.maximumParameterCount {
776+
parameterLimitExceeded = true
777+
} else if !parameterLimitExceeded {
778+
parsTxt.append(code)
779+
pars.append (0)
780+
}
781+
} else if !parameterLimitExceeded {
782+
pars [pars.count - 1] = EscapeSequenceParser.appendingParameterDigit(
783+
code,
784+
to: pars [pars.count - 1])
753785
}
754786
case .escDispatch:
755787
dispatchEsc(collect: collect, code: code)
@@ -765,10 +797,12 @@ public class EscapeSequenceParser {
765797
pars = [0]
766798
parsTxt = []
767799
collect = []
800+
parameterLimitExceeded = false
768801
dcs = -1
769802
printStateReset()
770803
case .dcsHook:
771-
if let handler = dispatchDcs(collect: collect, code: code, pars: pars) {
804+
if !parameterLimitExceeded,
805+
let handler = dispatchDcs(collect: collect, code: code, pars: pars) {
772806
dcsHandler = handler
773807
handler.hook(collect: collect, parameters: pars, flag: code)
774808
}
@@ -790,6 +824,7 @@ public class EscapeSequenceParser {
790824
pars = [0]
791825
parsTxt = []
792826
collect = []
827+
parameterLimitExceeded = false
793828
dcs = -1
794829
printStateReset()
795830
case .oscStart:
@@ -828,18 +863,20 @@ public class EscapeSequenceParser {
828863
}
829864
} else {
830865
if osc.count != 0 && code != ControlCodes.CAN && code != ControlCodes.SUB {
831-
var oscCode: Int
866+
let oscCode: Int?
832867
var content: ArraySlice<UInt8>
833868
let semiColonAscii = 59 // ';'
834869

835870
if let idx = osc.firstIndex(of: UInt8(semiColonAscii)) {
836-
oscCode = EscapeSequenceParser.parseInt(osc[0..<idx])
871+
oscCode = EscapeSequenceParser.parseDecimal(osc[0..<idx])
837872
content = osc[(idx+1)...]
838873
} else {
839-
oscCode = EscapeSequenceParser.parseInt(osc[0...])
874+
oscCode = EscapeSequenceParser.parseDecimal(osc[0...])
840875
content = []
841876
}
842-
dispatchOsc(code: oscCode, data: content)
877+
if let oscCode {
878+
dispatchOsc(code: oscCode, data: content)
879+
}
843880
}
844881
}
845882
if code == 0x1b {
@@ -850,6 +887,7 @@ public class EscapeSequenceParser {
850887
pars = [0]
851888
parsTxt = []
852889
collect = []
890+
parameterLimitExceeded = false
853891
dcs = -1
854892
printStateReset()
855893
}
@@ -868,6 +906,7 @@ public class EscapeSequenceParser {
868906
_collect = collect
869907
_pars = pars
870908
_parsTxt = parsTxt
909+
_parameterLimitExceeded = parameterLimitExceeded
871910

872911
// save active dcs handler reference
873912
activeDcsHandler = dcsHandler
@@ -878,20 +917,20 @@ public class EscapeSequenceParser {
878917

879918
}
880919

881-
static func parseInt (_ str: ArraySlice<UInt8>) -> Int
920+
/// Parses a complete decimal value, rejecting malformed input and integer overflow.
921+
static func parseDecimal (_ str: ArraySlice<UInt8>) -> Int?
882922
{
923+
guard !str.isEmpty else { return nil }
924+
883925
var result = 0
884926
for x in str {
885-
if x < 48 || x > 57 {
886-
return result
887-
}
888-
889-
let newV = result * 10 + Int ((x - 48))
890-
let willOverflow = newV > ((Int.max/10)-10)
891-
if willOverflow {
892-
return 0
927+
guard x >= 48 && x <= 57 else { return nil }
928+
929+
let digit = Int(x - 48)
930+
guard result <= (Int.max - digit) / 10 else {
931+
return nil
893932
}
894-
result = newV
933+
result = result * 10 + digit
895934
}
896935
return result
897936
}

Sources/SwiftTerm/Terminal.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1990,8 +1990,8 @@ open class Terminal {
19901990
guard let p = data [parsePos...].firstIndex(of: UInt8 (ascii: ";")) else {
19911991
return
19921992
}
1993-
let color = EscapeSequenceParser.parseInt(data [parsePos..<p])
1994-
guard color < 256 else {
1993+
guard let color = EscapeSequenceParser.parseDecimal(data [parsePos..<p]),
1994+
color < 256 else {
19951995
return
19961996
}
19971997

0 commit comments

Comments
 (0)