diff options
Diffstat (limited to 'src/look.zig')
| -rw-r--r-- | src/look.zig | 60 |
1 files changed, 57 insertions, 3 deletions
diff --git a/src/look.zig b/src/look.zig index 1730a456..f5d1488c 100644 --- a/src/look.zig +++ b/src/look.zig @@ -76,7 +76,17 @@ pub const Spot = struct { fn num(tok: []const u8, i: usize) struct { v: usize, end: usize } { var v: usize = 0; var j = i; - while (j < tok.len and std.ascii.isDigit(tok[j])) : (j += 1) v = v * 10 + (tok[j] - '0'); + // SATURATING, and this is not defensive programming — it is the fix for a + // crash on an ordinary keystroke. The digits come off whatever word is + // under the pointer, so `*` and `+` here run on text the user never wrote + // and cannot control: a right-click, an Enter, or an `n` on anything shaped + // `foo:99999999999999999999` — a hash in a log, a column of a CSV, the + // output of any program — overflowed a `usize` and took the editor down + // with "integer overflow". A number too big to be a line is not a line, and + // `maxInt` is refused by every consumer for free: `file_pane.open` asks + // `line <= total` and `focusPaneLine` asks `id < MAX_PANES`. Same shape + // acmefs.zig's address parser already uses. + while (j < tok.len and std.ascii.isDigit(tok[j])) : (j += 1) v = v *| 10 +| (tok[j] - '0'); return .{ .v = v, .end = j }; } @@ -175,6 +185,35 @@ test "parsePathLine: spots, ranges, and the paths that merely look like them" { } } +test "a number too big to be a line saturates instead of taking the editor down" { + // These are keystrokes, not arguments. `parsePathLine` runs on whatever + // word is under the pointer on a right-click, an Enter or an `n` — so the + // digits come out of a hash in a log, a CSV column, or any program's + // output, and an unchecked `v * 10` there is a panic on ordinary use. Every + // number in the token comes through the same scan, so all four are tried. + const huge = "99999999999999999999999999"; + const cases = [_][]const u8{ + "f.zig:" ++ huge, + "f.zig:" ++ huge ++ ":" ++ huge, + "f.zig:1-" ++ huge, + "f.zig:1:2-" ++ huge ++ ":" ++ huge, + }; + for (cases) |tok| { + const got = parsePathLine(tok); + try std.testing.expectEqualStrings("f.zig", got.path); + // Saturated rather than wrapped: a wrap would address a REAL line, and + // silently jumping somewhere is worse than not jumping. + try std.testing.expect(got.at.line >= 1); + } + + // ...and the pane address, which has its own scan. `focusPaneLine` refuses + // anything past MAX_PANES, so this resolves to a pane that cannot exist. + var realbuf: [4096]u8 = undefined; + const target = resolve("@p" ++ huge, "/tmp", &realbuf); + try std.testing.expect(target == .pane); + try std.testing.expect(target.pane.id >= 16); +} + test "parsePathLine: `end` separates a whole-token target from a lenient read" { // the whole token IS the target: every spelling the doc above lists for ([_][]const u8{ @@ -469,7 +508,10 @@ pub fn resolve(word_raw: []const u8, cwd: []const u8, realbuf: *[4096]u8) Target var id: usize = 0; for (word[config.pane_addr.len..]) |c| { if (!std.ascii.isDigit(c)) break; - id = id * 10 + (c - '0'); + // Saturating for the same reason `num` above is: this scan also + // runs on a word somebody merely clicked. `focusPaneLine` refuses + // anything past `MAX_PANES`, so a saturated id addresses nothing. + id = id *| 10 +| (c - '0'); } else return .{ .pane = .{ .id = id, .at = pl.at } }; } @@ -816,7 +858,19 @@ pub fn readFile(gpa: std.mem.Allocator, path: []const u8) ![]u8 { var pathbuf: [4096]u8 = undefined; const path_z = std.fmt.bufPrintSentinel(&pathbuf, "{s}", .{path}, 0) catch return error.PathTooLong; const fd = libc.open(path_z, .{ .ACCMODE = .RDONLY }); - if (fd < 0) return error.OpenFailed; + // WHY it would not open, not just that it would not. Every one of these is + // an ordinary thing to do by accident — `pardes /root`, a file left at mode + // 000, a name that was deleted between resolving and reading — and a caller + // that can only say "OpenFailed" has to show the human a word that means + // nothing to them. `errno` is libc's here, which is the only reason it can + // be read off a `-1`: see the raw-syscall note in `termCwd`. + if (fd < 0) return switch (libc.errno(fd)) { + .ACCES, .PERM => error.PermissionDenied, + .NOENT => error.FileNotFound, + .ISDIR => error.IsDirectory, + .NAMETOOLONG => error.PathTooLong, + else => error.OpenFailed, + }; defer _ = libc.close(fd); const end = libc.lseek(fd, 0, libc.SEEK.END); const size: usize = if (end < 0) 0 else @intCast(end); |
