diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-07 18:29:21 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-07 18:29:21 +0300 |
| commit | dcfe11a7588fcd42471159453810abc9b7720a57 (patch) | |
| tree | c32a77266028e73ca2382b96c96f8438a5fcd621 /vendor/libghostty-vt/src/simd/vt.cpp | |
| parent | 2dfed860103c4e4505ac7f25def9fe37da1e1e6a (diff) | |
build: update libghostty-vt vendor
vendor ghostty 063ac3e and remove the musl libc++ link workaround.
expose the new selection formatter and hyperlink uri apis for follow-up rendering work.
add manual artifact validation for static linux builds.
Diffstat (limited to 'vendor/libghostty-vt/src/simd/vt.cpp')
| -rw-r--r-- | vendor/libghostty-vt/src/simd/vt.cpp | 213 |
1 files changed, 180 insertions, 33 deletions
diff --git a/vendor/libghostty-vt/src/simd/vt.cpp b/vendor/libghostty-vt/src/simd/vt.cpp index d8133abf..5bf4147d 100644 --- a/vendor/libghostty-vt/src/simd/vt.cpp +++ b/vendor/libghostty-vt/src/simd/vt.cpp @@ -5,8 +5,6 @@ #include <hwy/highway.h> #include <simdutf.h> -#include <utf8.h> -#include <vector> #include <simd/index_of.h> #include <simd/vt.h> @@ -19,12 +17,144 @@ namespace hn = hwy::HWY_NAMESPACE; using T = uint8_t; +// Compute the length of the maximal subpart of an ill-formed UTF-8 +// subsequence starting at p[0], per Unicode Table 3-7 and the W3C +// "U+FFFD Substitution of Maximal Subparts" algorithm. +// +// The maximal subpart is the longest initial subsequence that is either: +// (a) the start of a well-formed sequence, or +// (b) a single byte. +// Each maximal subpart maps to exactly one U+FFFD. +static size_t MaximalSubpart(const unsigned char* p, size_t len) { + if (len == 0) return 0; + + unsigned char b0 = p[0]; + + // Continuation bytes (80-BF), overlong leads (C0-C1), or invalid (F5-FF): + // each is its own maximal subpart of length 1. + if (b0 < 0xC2 || b0 > 0xF4) return 1; + + // Determine the expected sequence length and the valid range for each + // continuation byte per Unicode Table 3-7. + size_t seq_len; + unsigned char lo[3], hi[3]; + + if (b0 <= 0xDF) { + seq_len = 2; + lo[0] = 0x80; hi[0] = 0xBF; + } else if (b0 == 0xE0) { + seq_len = 3; + lo[0] = 0xA0; hi[0] = 0xBF; + lo[1] = 0x80; hi[1] = 0xBF; + } else if (b0 <= 0xEC) { + seq_len = 3; + lo[0] = 0x80; hi[0] = 0xBF; + lo[1] = 0x80; hi[1] = 0xBF; + } else if (b0 == 0xED) { + seq_len = 3; + lo[0] = 0x80; hi[0] = 0x9F; + lo[1] = 0x80; hi[1] = 0xBF; + } else if (b0 <= 0xEF) { + seq_len = 3; + lo[0] = 0x80; hi[0] = 0xBF; + lo[1] = 0x80; hi[1] = 0xBF; + } else if (b0 == 0xF0) { + seq_len = 4; + lo[0] = 0x90; hi[0] = 0xBF; + lo[1] = 0x80; hi[1] = 0xBF; + lo[2] = 0x80; hi[2] = 0xBF; + } else if (b0 <= 0xF3) { + seq_len = 4; + lo[0] = 0x80; hi[0] = 0xBF; + lo[1] = 0x80; hi[1] = 0xBF; + lo[2] = 0x80; hi[2] = 0xBF; + } else { // b0 == 0xF4 + seq_len = 4; + lo[0] = 0x80; hi[0] = 0x8F; + lo[1] = 0x80; hi[1] = 0xBF; + lo[2] = 0x80; hi[2] = 0xBF; + } + + // Check continuation bytes against their specific valid ranges. + // The maximal subpart extends as far as bytes match. + size_t valid = 1; // lead byte counts + for (size_t i = 0; i < seq_len - 1 && valid < len; i++) { + unsigned char cb = p[valid]; + if (cb < lo[i] || cb > hi[i]) break; + valid++; + } + + // If we matched all bytes, the sequence is structurally valid + // (shouldn't happen since we're called on an error), but cap + // to avoid skipping a valid sequence. + if (valid == seq_len) return valid; + + return valid; +} + +// Trim trailing bytes that form a valid-but-incomplete UTF-8 sequence. +// Only trims sequences whose bytes so far match Table 3-7 ranges (i.e., +// truly partial sequences that could be completed by future input). +// Invalid lead bytes (C0, C1, F5-FF) or mismatched continuations are NOT +// trimmed — they will be handled as errors by DecodeUTF8. +static size_t TrimValidPartialUTF8(const uint8_t* input, size_t len) { + if (len == 0) return 0; + + // Find the start of a potential trailing partial sequence by scanning + // backwards from the end. We look for a lead byte (C2-F4) that could + // start a multi-byte sequence, possibly followed by continuation bytes. + // + // We check up to the last 4 bytes (max UTF-8 sequence length). + size_t check_start = len > 4 ? len - 4 : 0; + for (size_t pos = len; pos > check_start; pos--) { + unsigned char b = input[pos - 1]; + + // Skip continuation bytes — they might belong to the partial sequence. + if ((b & 0xC0) == 0x80) continue; + + // Found a non-continuation byte. Only valid multi-byte leads (C2-F4) + // can start a partial sequence worth trimming. Anything else (ASCII, + // C0, C1, F5-FF) should be consumed by DecodeUTF8. + if (b < 0xC2 || b > 0xF4) return len; + + // Determine expected sequence length from the lead byte. + size_t expected; + if (b <= 0xDF) + expected = 2; + else if (b <= 0xEF) + expected = 3; + else + expected = 4; + + size_t seq_remaining = len - (pos - 1); + + // If we have all expected bytes, the sequence is complete (not partial). + if (seq_remaining >= expected) return len; + + // Check if the trailing bytes form a valid prefix using MaximalSubpart. + const unsigned char* seq_start = input + pos - 1; + size_t subpart = MaximalSubpart(seq_start, seq_remaining); + + // Only trim if ALL trailing bytes are part of the valid prefix + // (the sequence is valid-so-far but incomplete). + if (subpart == seq_remaining) { + return pos - 1; + } + + // The sequence is ill-formed, don't trim — let DecodeUTF8 handle it. + return len; + } + + return len; +} + // Decode the UTF-8 text in input into output. Returns the number of decoded // characters. This function assumes output is large enough. // // This function handles malformed UTF-8 sequences by inserting a -// replacement character (U+FFFD) and continuing to decode. This function -// will consume the entire input no matter what. +// replacement character (U+FFFD) following the W3C/Unicode "U+FFFD +// Substitution of Maximal Subparts" algorithm and continuing to decode. +// This function will consume the entire input no matter what. size_t DecodeUTF8(const uint8_t* HWY_RESTRICT input, size_t count, char32_t* output) { @@ -34,27 +164,38 @@ size_t DecodeUTF8(const uint8_t* HWY_RESTRICT input, return 0; } - // Assume no errors for fast path. - const size_t decoded = simdutf::convert_utf8_to_utf32( - reinterpret_cast<const char*>(input), count, output); - if (decoded > 0) { - return decoded; + // Decode UTF-8 to UTF-32, replacing invalid sequences with U+FFFD. + const char* in = reinterpret_cast<const char*>(input); + size_t remaining = count; + char32_t* out = output; + while (remaining > 0) { + auto r = simdutf::convert_utf8_to_utf32_with_errors(in, remaining, out); + + // If the decode was a full success then we're done! + if (r.error == simdutf::SUCCESS) { + out += r.count; + break; + } + + // On error, r.count is the input byte position of the error. + // The output buffer is already written up to that point, but + // we need count_utf8 to find how many char32_t that produced. + out += simdutf::count_utf8(in, r.count); + + // Compute the maximal subpart at the error position and emit + // a single U+FFFD for it. + const unsigned char* err_pos = + reinterpret_cast<const unsigned char*>(in + r.count); + size_t err_remaining = remaining - r.count; + size_t skip = r.count + MaximalSubpart(err_pos, err_remaining); + + *out++ = 0xFFFD; + + in += skip; + remaining -= skip; } - // Errors in the UTF input, take a slow path and do a decode with - // replacement (with U+FFFD). Note that simdutf doesn't have a - // decode with replacement API: - // https://github.com/simdutf/simdutf/issues/147 - // - // Because of this, we use a separate library with heap allocation - // that is much, much slower (the allocation is slower, the algorithm - // is slower, etc.) This is just so we have something that works. - // I want to replace this. - std::vector<char> replacement_result; - utf8::replace_invalid(input, input + count, - std::back_inserter(replacement_result), 0xFFFD); - return DecodeUTF8(reinterpret_cast<const uint8_t*>(replacement_result.data()), - replacement_result.size(), output); + return static_cast<size_t>(out - output); } /// Decode the UTF-8 text in input into output until an escape @@ -86,16 +227,16 @@ size_t DecodeUTF8UntilControlSeqImpl(D d, // If we don't have any escapes we keep going. We want to accumulate // the largest possible valid UTF-8 sequence before decoding. // TODO(mitchellh): benchmark this vs decoding every time - const auto esc_idx = IndexOfChunk(d, esc_vec, input_vec); - if (!esc_idx) { + const size_t esc_idx = IndexOfChunk(d, esc_vec, input_vec); + if (esc_idx == kNotFound) { continue; } // We have an ESC char, decode up to this point. We start by assuming // a valid UTF-8 sequence and slow-path into error handling if we find // an invalid sequence. - *output_count = DecodeUTF8(input, i + esc_idx.value(), output); - return i + esc_idx.value(); + *output_count = DecodeUTF8(input, i + esc_idx, output); + return i + esc_idx; } // If we have leftover input then we decode it one byte at a time (slow!) @@ -106,21 +247,27 @@ size_t DecodeUTF8UntilControlSeqImpl(D d, const hn::Vec<D1> esc1 = Set(d1, hn::GetLane(esc_vec)); for (; i < count; ++i) { const hn::Vec<D1> input_vec = hn::LoadU(d1, input + i); - const auto esc_idx = IndexOfChunk(d1, esc1, input_vec); - if (!esc_idx) { + const size_t esc_idx = IndexOfChunk(d1, esc1, input_vec); + if (esc_idx == kNotFound) { continue; } - *output_count = DecodeUTF8(input, i + esc_idx.value(), output); - return i + esc_idx.value(); + *output_count = DecodeUTF8(input, i + esc_idx, output); + return i + esc_idx; } } // If we reached this point, its possible for our input to have an // incomplete sequence because we're consuming the full input. We need // to trim any incomplete sequences from the end of the input. - const size_t trimmed_len = - simdutf::trim_partial_utf8(reinterpret_cast<const char*>(input), i); + // + // We use our own trim instead of simdutf::trim_partial_utf8 because + // we only want to trim sequences that are valid-so-far (true partial + // sequences that may be completed by future input). Invalid bytes + // like C0, C1, F5-FF should NOT be trimmed — they should be passed + // through to DecodeUTF8 which will replace them with U+FFFD per the + // maximal subpart algorithm. + const size_t trimmed_len = TrimValidPartialUTF8(input, i); *output_count = DecodeUTF8(input, trimmed_len, output); return trimmed_len; } |
