Fix advancing invalid UTF characters - #945
Conversation
|
I admit that I find the JIT code hard to follow (although I am getting more familiar with it). GPT explained it to me, and the change made sense. I couldn't find any bugs either. Great! |
|
GPT says.... Verdict: the fix is logically correct, and I do not see a blocking correctness issue. It restores the essential bump-along invariant:
The old code instead skipped continuation-looking code units after PR and bug contextPR #945 — Fix advancing invalid UTF characters addresses issue #944 — JIT matching issue with lookbehinds and invalid UTF matching. The issue’s reproducer is approximately: At offset zero, the lookbehind cannot succeed because there is no preceding character. The next match attempt must therefore be at the offset immediately after The old JIT bump-along loop advanced over That matches the diagnosis in the issue discussion: the bug is in the JIT’s unanchored main-loop advancement optimization, not lookbehind itself. Why the old logic was wrongThe old UTF-8 branch effectively did this: STR_PTR++; // consume the current byte
-if (invalid_utf) {
- while (STR_PTR < STR_END &&
- is_continuation_byte(*STR_PTR))
- STR_PTR++;
-}That is safe only if the byte at the original position is known to be the start of a valid UTF character. Under For example: The bytes after The same conceptual problem existed in UTF-16: after any current code unit, the old loop skipped following low surrogates without first establishing that the current code unit was a high surrogate forming a valid pair. What the new logic doesThe new UTF-8 code reads the current byte before incrementing the pointer. After the mandatory one-byte increment:
-if (common->utf && !common->invalid_utf) readuchar = TRUE;
+if (common->utf) readuchar = TRUE;
if (common->invalid_utf)
{
- /* Blindly skip following continuation bytes. */
- while (STR_PTR < STR_END && next_byte_is_continuation)
- STR_PTR++;
+ jump = CMP(SLJIT_LESS, TMP1, 0, SLJIT_IMM, 0xc0);
+ OP1(SLJIT_MOV, TMP3, 0, STR_PTR, 0);
+ add_jump(compiler, &common->utfreadchar_invalid,
+ JUMP(SLJIT_FAST_CALL));
+ OP2U(SLJIT_SUB | SLJIT_SET_Z, TMP1, 0,
+ SLJIT_IMM, INVALID_UTF_CHAR);
+ SELECT(SLJIT_EQUAL, STR_PTR, TMP3, 0, STR_PTR);
JUMPHERE(jump);
}Here, That is the correct rule for enumerating possible match starts without entering the middle of a valid character and without hiding offsets inside malformed input. UTF-16 analysisThe UTF-16 path follows the same rule: if (common->invalid_utf)
{
- /* Blindly skip following low surrogates. */
- while (next_code_unit_is_low_surrogate)
- STR_PTR++;
+ OP2(SLJIT_SUB, TMP2, 0, TMP1, 0, SLJIT_IMM, 0xd800);
+ jump = CMP(SLJIT_GREATER_EQUAL, TMP2, 0,
+ SLJIT_IMM, 0xe000 - 0xd800);
+ add_jump(compiler, &common->utfreadchar_invalid,
+ JUMP(SLJIT_FAST_CALL));
JUMPHERE(jump);
}The helper’s contract is strengthened so that invalid input does not consume the second code unit: -/* STR_PTR is undefined for invalid characters. */
+/* STR_PTR is unchanged for invalid characters. */
-exit_invalid[0] =
- CMP(SLJIT_GREATER_EQUAL, TMP1, 0, SLJIT_IMM, 0xdc00);
+exit_invalid[0] =
+ CMP(SLJIT_GREATER_EQUAL, TMP2, 0, SLJIT_IMM, 0x400);
...
exit_invalid[2] =
CMP(SLJIT_GREATER_EQUAL, TMP1, 0, SLJIT_IMM, 0x400);
+JUMPHERE(exit_invalid[2]);
+OP2(SLJIT_SUB, STR_PTR, 0, STR_PTR, 0,
+ SLJIT_IMM, IN_UCHARS(1));
JUMPHERE(exit_invalid[0]);
JUMPHERE(exit_invalid[1]);Walking the relevant cases:
The pointer rollback is correctly limited to the path where the helper has already loaded and tentatively consumed a second code unit. The “first unit is not a high surrogate” and end-of-buffer exits happen before that increment, so they need no rollback. Why reusing
|
|
This will be a small performance drop in the, but it seems unavoidable. |
No description provided.