From 2b76b47c8a087b81be139b552d9a41bb2006dcd3 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 28 Jul 2026 12:28:27 -0300 Subject: fixing some crashes --- src/pardes.zig | 49 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/pardes.zig b/src/pardes.zig index 8a947065..4d821e60 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -399,13 +399,28 @@ pub const Surface = struct { /// Print UTF-8 text into a row, no wrap, clipped to [x, x+w). Returns the /// column after the last written cell. Wide glyphs take two cells. + /// + /// The text is NOT trusted to be valid UTF-8 — a file pane holds whatever + /// bytes are on disk (latin-1 source, an ELF opened by mistake), a path can + /// be any bytes at all, and a search row splices both. std's unchecked + /// iterator panics on a bad start byte, so decode by hand and paint one + /// U+FFFD per undecodable byte (what a terminal does). fn print(s: *Surface, x: u16, y: u16, w: u16, text: []const u8, style: CellStyle) u16 { var col = x; const end = x + w; - var it = std.unicode.Utf8View.initUnchecked(text).iterator(); - while (it.nextCodepointSlice()) |cp_slice| { + var i: usize = 0; + while (i < text.len) { if (col >= end) break; - const cp = std.unicode.utf8Decode(cp_slice) catch continue; + // n == 0: not a start byte at all. A short tail or a bad + // continuation decodes to null the same way — one U+FFFD, one byte. + const n = std.unicode.utf8ByteSequenceLength(text[i]) catch 0; + const decoded: ?u21 = if (n > 0 and i + n <= text.len) + (std.unicode.utf8Decode(text[i .. i + n]) catch null) + else + null; + const cp_slice = if (decoded == null) "\u{FFFD}" else text[i .. i + n]; + i += if (decoded == null) 1 else n; + const cp = decoded orelse 0xFFFD; if (cp == '\r') continue; const width: u16 = if (cp < 0x80) 1 else uucode.get(.width, cp); if (width == 0) continue; @@ -3292,7 +3307,10 @@ pub const Pardes = struct { const row0 = eb.row0; if (y[y.len - 1] == '\n') { const block_text = y[0 .. y.len - 1]; - const n = modal.lineCount(block_text); + // a yanked BLANK line is "\n": the block is empty and lineCount + // says 0 lines, but it still pastes as one (empty) line — without + // the floor every `n - 1` below underflows and panics. + const n = @max(1, modal.lineCount(block_text)); var out: []u8 = undefined; if (before) { const row: usize = @intCast(@max(0, b.lo_row - row0)); @@ -5461,7 +5479,8 @@ pub const Pardes = struct { for (0..p.ncol) |c| { const last = c + 1 == p.ncol; const fw = @as(f32, @floatFromInt(p.screen_w)) * p.col_weight[c] / wsum; - const cw: u16 = if (last) (p.screen_w -| x) else @max(1, @as(u16, @intFromFloat(@round(fw)))); + const wroom = p.screen_w -| x; // same clamp as the rows below + const cw: u16 = if (last) wroom else @min(wroom, @max(1, @as(u16, @intFromFloat(@round(fw))))); p.col_x[c] = x; p.col_w[c] = cw; @@ -5478,7 +5497,14 @@ pub const Pardes = struct { const pane = p.panes[id] orelse continue; const lastk = k + 1 == p.col_n[c]; const fh = @as(f32, @floatFromInt(avail_h)) * pane.vweight / vsum; - const ch: u16 = if (lastk) (p.screen_h -| y) else @max(1, @as(u16, @intFromFloat(@round(fh)))); + // every pane wants at least one row, so a column with more + // panes than the window has rows would walk `y` off the bottom + // and hand renderPane a rect outside the surface (assert, then + // panic — shrink a window with a few stacked panes). Clamp to + // what is left: the panes past the edge get h = 0 and render + // nothing until the window grows back. + const room = p.screen_h -| y; + const ch: u16 = if (lastk) room else @min(room, @max(1, @as(u16, @intFromFloat(@round(fh))))); p.rects[id] = .{ .x = x, .y = y, .w = cw, .h = ch }; y +|= ch; } @@ -5558,17 +5584,20 @@ pub const Pardes = struct { }; // resize-handle hint / drag previews: a dash overlay that keeps the - // underlying colors (border drags + hover), or the move indicator + // underlying colors (border drags + hover), or the move indicator. A + // drag holds the coordinates of the last mouse event, so a resize + // mid-drag (tiling WM, font-size change) can leave them off the new + // surface — every arm below checks before it draws. switch (p.drag) { - .border_v => |d| { + .border_v => |d| if (d.cur_x < s.cols) { var row: u16 = TOPBAR_H; while (row < s.rows) : (row += 1) s.overlayDash(d.cur_x, row, "╎"); }, - .border_h => |d| { + .border_h => |d| if (d.cur_y < s.rows) { var col = p.col_x[d.col]; while (col < p.col_x[d.col] + p.col_w[d.col]) : (col += 1) s.overlayDash(col, d.cur_y, "╌"); }, - .move => |d| { + .move => |d| if (d.cur_x < s.cols) { if (p.movePlacement(d.id, d.cur_x, d.cur_y)) |placement| { var col: u16 = p.col_x[placement.preview_col]; while (col < p.col_x[placement.preview_col] + p.col_w[placement.preview_col]) : (col += 1) { -- cgit v1.3