Common (Format): fixes module format strings mishandling a value that carries more than one leading ANSI escape sequence

This commit is contained in:
Carter Li
2026-09-21 13:16:43 +08:00
parent b726ee5a85
commit 6842ae0757
3 changed files with 78 additions and 24 deletions
+60 -24
View File
@@ -438,31 +438,67 @@ static bool parseQuickJSString(FFstrbuf* buffer, const char* script, uint32_t sc
}
#endif
static bool skipAnsiEscape(FFstrbuf* in, FFstrbuf* out, FFstrbuf* trailingEscape) {
if (__builtin_expect(in->chars[0] == '\e' && in->chars[1] == '[', false)) {
// skip ANSI escape codes at the start of the string for truncation
const char* p = in->chars + 2;
while (*p && (ffCharIsDigit(*p) || *p == ';')) {
++p;
}
if (*p && isascii(*p)) {
++p;
}
uint32_t prefixLen = (uint32_t) (p - in->chars);
ffStrbufAppendNS(out, prefixLen, in->chars);
ffStrbufSubstrAfter(in, prefixLen - 1);
if (trailingEscape) {
// likely have a `CSI m` reset at the end of the string
uint32_t iLastEscape = ffStrbufLastIndexC(in, '\e');
if (iLastEscape != in->length) {
ffStrbufSetNS(trailingEscape, in->length - iLastEscape, in->chars + iLastEscape);
ffStrbufSubstrBefore(in, iLastEscape);
}
}
return true;
// Returns the length of the ANSI CSI sequence at `str`, or 0 if there is none.
// A CSI sequence is `ESC [` + parameter bytes (0x30-0x3F) + intermediate bytes (0x20-0x2F) + final byte (0x40-0x7E)
static uint32_t ansiEscapeLength(const char* str) {
if (str[0] != '\e' || str[1] != '[') {
return 0;
}
return false;
const char* p = str + 2;
while (*p >= '0' && *p <= '?') {
++p;
}
while (*p >= ' ' && *p <= '/') {
++p;
}
if (*p < '@' || *p > '~') {
return 0; // not a complete CSI sequence
}
return (uint32_t) (p - str) + 1;
}
static bool skipAnsiEscape(FFstrbuf* in, FFstrbuf* out, FFstrbuf* trailingEscape) {
// Skip all ANSI escape codes at the start of the string, so that they don't count
// towards the truncation / padding length. A value may carry more than one of them,
// e.g. `\e[31m\e[1m`.
uint32_t prefixLen = 0;
while (in->length - prefixLen >= 2) {
uint32_t len = ansiEscapeLength(in->chars + prefixLen);
if (len == 0) {
break;
}
prefixLen += len;
}
if (prefixLen == 0) {
return false;
}
ffStrbufAppendNS(out, prefixLen, in->chars);
ffStrbufSubstrAfter(in, prefixLen - 1);
if (trailingEscape) {
// likely have one or more `CSI m` resets at the end of the string
uint32_t end = in->length;
while (end >= 3) {
uint32_t start = end - 1;
while (start > 0 && in->chars[start] != '\e') {
--start;
}
uint32_t len = ansiEscapeLength(in->chars + start);
if (len == 0 || start + len != end) {
break;
}
end = start;
}
if (end < in->length) {
ffStrbufSetNS(trailingEscape, in->length - end, in->chars + end);
ffStrbufSubstrBefore(in, end);
}
}
return true;
}
static bool parseFormatString(FFstrbuf* buffer, const FFstrbuf* formatstr, uint32_t numArgs, const FFformatarg* arguments) {