diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 17:56:54 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | b6f07ba4e84e6d8474a118bee6e69e4b554417f1 (patch) | |
| tree | 3e9e1b2766e2fcc7f44afd3edd952d62877b2c89 /src | |
| parent | 031989d927b07d38f0874a0b78a52f54ad285efb (diff) | |
| download | pardes-b6f07ba4e84e6d8474a118bee6e69e4b554417f1.tar.gz pardes-b6f07ba4e84e6d8474a118bee6e69e4b554417f1.zip | |
An invalid byte in a grapheme is one cell, and never reaches vaxis's width
uucode joins a stray lead byte (as U+FFFD) to a following e and combining mark into one grapheme; vaxis's gwidth then counts back the replacement rune's three bytes over the one byte it stood for and overflows (gwidth.zig:62), panicking the next frame after a write of \xee e \xcc\x81 to xdata (the 9P monkey's crash-b2417c49). graphemeDisplayWidth now gives each invalid byte one cell, as surface.zig draws it, and measures the valid runs between alone.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/File.zig | 31 |
1 files changed, 30 insertions, 1 deletions
diff --git a/src/File.zig b/src/File.zig index 1d838c4f..99fccea8 100644 --- a/src/File.zig +++ b/src/File.zig @@ -266,7 +266,36 @@ test "stacked location metadata requires matching adjacent preview ownership" { pub fn graphemeDisplayWidth(grapheme: []const u8) usize { if (std.mem.eql(u8, grapheme, "\t")) return config.tab_width; if (grapheme.len == 1 and grapheme[0] >= 0x20 and grapheme[0] < 0x7f) return 1; - return @max(1, @as(usize, vaxis.gwidth.gwidth(grapheme, .unicode))); + if (std.unicode.utf8ValidateSlice(grapheme)) return @max(1, @as(usize, vaxis.gwidth.gwidth(grapheme, .unicode))); + // vaxis's gwidth counts back a replacement rune's 3 bytes over the one + // invalid byte it stood for (`\xee` before `e\u{301}` overflowed it): + // each invalid byte is a cell, as surface.zig draws it, and each valid + // run between is measured alone. + var width: usize = 0; + var run: usize = 0; + var i: usize = 0; + while (i < grapheme.len) { + const n = std.unicode.utf8ByteSequenceLength(grapheme[i]) catch 0; + if (n > 0 and i + n <= grapheme.len and std.unicode.utf8ValidateSlice(grapheme[i .. i + n])) { + i += n; + continue; + } + if (run < i) width += vaxis.gwidth.gwidth(grapheme[run..i], .unicode); + width += 1; + i += 1; + run = i; + } + if (run < grapheme.len) width += vaxis.gwidth.gwidth(grapheme[run..], .unicode); + return @max(1, width); +} + +test "an invalid byte in a grapheme is one cell, and never reaches vaxis's width" { + try std.testing.expectEqual(@as(usize, 2), graphemeDisplayWidth("\xeee\xcc\x81")); + try std.testing.expectEqual(@as(usize, 3), graphemeDisplayWidth("\xee\xffe")); + try std.testing.expectEqual(@as(usize, 3), graphemeDisplayWidth("\u{4e16}\x80")); + // The whole path the 9P fuzzer took: a body holding it, fitted to a width. + const text = "\xeee\xcc\x81"; + try std.testing.expectEqual(@as(usize, 4), fitEnd(text, 0, 10)); } pub fn byteDisplayWidth(byte: u8) usize { |
