Fix off-by-one that dropped the 16th SGR trigger parameter - #722
Conversation
iTermParserAddCSIParameter refused to store a value once count reached 15, so it accepted only 15 of the 16 slots in CSIParam.p. The terminal's own CSI parser accepts 16 (VT100CSIParser.m), so an SGR trigger and the terminal disagreed about the same parameter string. Besides losing the last parameter, SGRTrigger attaches subparameters with iTermParserAddCSISubparameter(&csi, csi.count - 1, sub), so when the 16th parameter was dropped its subparameters landed on the 15th parameter instead — e.g. the components of a 24-bit color were applied to the wrong parameter.
The function always returned the first subparameter of the requested parameter: i started at 0, the loop returned on the first match because i == 0, and the i-- afterwards was unreachable. It has no callers today, so nothing is broken by it, but it silently returns the wrong value for any subparameter_index other than 0.
When a CSI sequence has more parameters than CSIParam can hold, the extra parameters are discarded, but their subparameters were still attached to the last parameter that did fit. That changes its meaning: in an SGR sequence with 17 parameters ending in 4;3:5, the :5 landed on the 4, turning plain underline into dashed underline. Both paths that build a CSIParam had this problem: the terminal's own parser, where the subparameter branch used count - 1 without checking whether the parameter had been stored, and SGRTrigger, which calls iTermParserAddCSIParameter and could not tell that the value had been dropped. iTermParserAddCSIParameter now reports whether it stored the value. Adds regression tests for the parameter-list boundary.
|
Update: I've pushed a third commit that makes this a complete fix, and I built the app to check it. The same corruption existed in the terminal's own parser, not just in triggers. My original patch only moved the boundary: with 17 parameters the 17th is discarded, but I measured this by compiling the real The fix: parameters past the limit set an overflow flag, so their subparameters are discarded with them, and Testing. I added five regression tests to (
|
|
Thank you, @semx, for the careful fix and the standalone-harness verification writeup. This is exactly the kind of subtle parser off-by-one that is easy to miss, and pairing it with tests plus a release note made it easy to review. Merged. 🙏 |
Follow-up to #722. When the 16th CSI parameter was an implied blank created by a doubled semicolon, parameterOverflowed was set the moment count reached VT100CSIPARAM_MAX, so a subparameter attached to that still-valid 16th parameter was discarded. Only treat the list as overflowed when the semicolon did not open a storable blank parameter. Adds a VT100CSIParserTest case and a cat-able manual test in tests/sgr-16th-param.txt.
|
Thanks for catching the blank-16th case — that was a real hole in my patch and I should have covered it. I reproduced it against my merged commit and verified your fix with the same standalone harness I used originally, compiling I then ran the whole boundary set against your version to make sure nothing I'd fixed regressed:
The last row is the one your new test doesn't cover — a doubled semicolon past the limit, where the blank parameter genuinely can't be stored and its subparameter must go with it. It passes today; if you'd like it pinned down, the assertion is |
Two small fixes in
sources/VT100/iTermParser.h. The first one is user-visible; the second is latent.1. The 16th SGR trigger parameter was dropped
iTermParserAddCSIParameterbailed out oncecountreached 15:so it filled only 15 of the 16 slots in
CSIParam.p. The terminal's own CSI parser accepts 16 (VT100CSIParser.m:239,param->count < VT100CSIPARAM_MAX), so the trigger and the terminal disagreed about the same parameter string.The only caller is
SGRTrigger.performAction(sources/Triggers/SGRTrigger.swift:136), which then attaches subparameters withiTermParserAddCSISubparameter(&csi, csi.count - 1, sub). So when the 16th parameter was discarded,countdidn't advance and its subparameters landed on the 15th parameter — a 24-bit color's components got applied to the wrong parameter.I verified this by building the real
VT100CSIParser.mand the real header helpers into a standalone harness, running the same parameter string through both the terminal parser and a mirror of the SGRTrigger loop:After the fix both sides produce identical output for 15 parameters, 16 parameters, and the subparameter case above. Writing
p[15]is in bounds (phasVT100CSIPARAM_MAX= 16 entries), and the 17th parameter is still discarded.2.
iTermParserGetCSISubparameterignoredsubparameter_indexistarted at 0, the loop returned on the first matching parameter becausei == 0, and thei--after it was unreachable — so the function returned the first subparameter for every requested index. It has no callers today, so nothing is broken by it; I fixed it as a separate commit so it can be dropped independently if you'd rather leave it alone. Verified with the same standalone harness: before the change, indices 0/1/2 all returned the first value; after, they return 11/22/33 as documented.Notes
docs/notes-3.7.txtfor the first fix.tools/run_tests.expect— I don't have a full build of the app on this machine (submodules +make paranoid-deps), so the verification above is from a standalone harness that compiles the real parser sources rather than from the test suite. Happy to add aModernTestscase for this if you'd like one.