fix(zig): never hand utf8Decode a malformed sequence when trimming titles

The backward scan in trimTitle passed whatever bytes followed a lead byte
to std.unicode.utf8Decode, which asserts a whole sequence; a title like
a%80%80%80%80 hit unreachable and aborted the server. Check the lead
byte's length matches before decoding; add regression tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev e5bb4d41f2c721bad2a390fb0d4f82e81aedf44c parent eaa1b44
1 file changed, +11 −0
  • apps/zig/src/app.zig +11 −0
diff --git a/apps/zig/src/app.zig b/apps/zig/src/app.zig
index 579194f..26eed99 100644
--- a/apps/zig/src/app.zig
+++ b/apps/zig/src/app.zig
@@ -215,6 +215,11 @@ fn trimTitle(s: []const u8) []const u8 {
while (end > start) {
var i = end - 1;
while (i > start and s[i] & 0xC0 == 0x80) i -= 1;
+ // utf8Decode asserts its input is one whole sequence, so check the
+ // lead byte claims exactly the bytes found; anything else is
+ // invalid UTF-8 and ends the trim.
+ const len = std.unicode.utf8ByteSequenceLength(s[i]) catch break;
+ if (len != end - i) break;
const cp = std.unicode.utf8Decode(s[i..end]) catch break;
if (!isSpace(cp)) break;
end = i;
@@ -340,6 +345,12 @@ test trimTitle {
try std.testing.expectEqualStrings("a\u{a0}b", trimTitle("a\u{a0}b"));
try std.testing.expectEqualStrings("日本語", trimTitle("日本語"));
try std.testing.expectEqualStrings("\xff x", trimTitle("\xff x"));
+ // Malformed UTF-8 is left alone, never decoded.
+ try std.testing.expectEqualStrings("a\x80\x80\x80\x80", trimTitle("a\x80\x80\x80\x80"));
+ try std.testing.expectEqualStrings("\x80\x80", trimTitle(" \x80\x80 "));
+ try std.testing.expectEqualStrings("x\xe2\x80", trimTitle("x\xe2\x80"));
+ try std.testing.expectEqualStrings("x\xc3", trimTitle("x\xc3"));
+ try std.testing.expectEqualStrings("\xf0\x80\x80\x80\x80", trimTitle("\xf0\x80\x80\x80\x80"));
}
test itemAction {