summaryrefslogtreecommitdiff
path: root/src/dump.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-16 15:49:12 -0300
committerGabriel Schneider <[email protected]>2026-08-18 23:44:42 -0300
commit1551e409c31992437cb2fa864f576d45c8433801 (patch)
treee2fae8451f87b735a1360c7c2e383fdc40165789 /src/dump.zig
parentbe2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff)
downloadpardes-1551e409c31992437cb2fa864f576d45c8433801.tar.gz
pardes-1551e409c31992437cb2fa864f576d45c8433801.zip
big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh
Diffstat (limited to 'src/dump.zig')
-rw-r--r--src/dump.zig152
1 files changed, 152 insertions, 0 deletions
diff --git a/src/dump.zig b/src/dump.zig
index 44f61cf4..566cf03c 100644
--- a/src/dump.zig
+++ b/src/dump.zig
@@ -40,6 +40,13 @@ pub const magic = "pardes-dump";
pub const version: u32 = 1;
pub const max_panes: usize = 16;
pub const max_cols: usize = 6;
+/// Bounds the only user-editable, schema-owned tag fragment. Keep this beside
+/// the dump limits so readers can reject data before copying it into a pane.
+pub const max_tag_tail: usize = 4096;
+/// Output arguments are typed in the same bounded one-line tag storage. Keep
+/// the schema limit named independently so a dump reader can validate it
+/// without importing the output-pane implementation.
+pub const max_origin_arg: usize = max_tag_tail;
pub const Size = struct {
cols: u16,
@@ -77,9 +84,19 @@ pub const File = struct {
origin_arg: []const u8 = "",
};
+pub const ImagePalette = enum {
+ commodore,
+ terminal,
+};
+
pub const Image = struct {
path: []const u8 = "",
bytes_b64: []const u8 = "",
+ // Defaults are the renderer's historical state, so version-1 dumps which
+ // predate these fields remain readable without a schema fork.
+ petscii: bool = false,
+ palette: ImagePalette = .commodore,
+ ascii: bool = true,
};
pub const Pane = struct {
@@ -90,6 +107,9 @@ pub const Pane = struct {
cols: u16 = 0,
rows: u16 = 0,
vweight: f32 = 1,
+ /// Exact editable tail when the user touched it. Null retains the live
+ /// default; a present empty slice deliberately restores an empty tail.
+ tag_tail: ?[]const u8 = null,
terminal: ?Terminal = null,
file: ?File = null,
image: ?Image = null,
@@ -118,12 +138,20 @@ pub fn validate(state: State) !void {
if (state.columns.len == 0 or state.columns.len > max_cols) return error.BadDumpColumns;
if (state.active >= state.panes.len) return error.BadDumpActive;
for (state.columns) |col| {
+ if (!std.math.isFinite(col.weight) or col.weight <= 0) return error.BadDumpColumns;
if (col.panes.len == 0 or col.panes.len > max_panes) return error.BadDumpColumns;
for (col.panes) |pane| {
if (pane >= state.panes.len) return error.BadDumpPaneRef;
}
}
for (state.panes) |pane| {
+ if (!std.math.isFinite(pane.vweight) or pane.vweight <= 0)
+ return error.BadDumpPaneWeight;
+ if (pane.scroll > std.math.maxInt(i32)) return error.BadDumpPaneScroll;
+ if (pane.tag_tail) |tail| if (tail.len > max_tag_tail)
+ return error.BadDumpTagTail;
+ if (pane.file) |file| if (file.origin_arg.len > max_origin_arg)
+ return error.BadDumpOriginArg;
switch (pane.kind) {
.terminal => if (pane.terminal == null) return error.BadDumpPaneKind,
.file => if (pane.file == null) return error.BadDumpPaneKind,
@@ -132,6 +160,37 @@ pub fn validate(state: State) !void {
}
}
+test "oversized recognized output argument is rejected before restore" {
+ var oversized: [max_origin_arg + 1]u8 = @splat('x');
+ const pane_ids = [_]usize{0};
+ const columns = [_]Column{.{ .panes = &pane_ids }};
+ const panes = [_]Pane{.{
+ .kind = .file,
+ .tag = "+Search",
+ .body = "",
+ .file = .{
+ .path = "/tmp/+Search",
+ .origin = "Find",
+ .origin_arg = &oversized,
+ },
+ }};
+ const state: State = .{
+ .screen = .{ .cols = 80, .rows = 24 },
+ .columns = &columns,
+ .panes = &panes,
+ };
+
+ // Serialize without writeFile: that writer correctly rejects the state
+ // too, while this exercises the hostile bytes a restore actually reads.
+ var encoded: std.Io.Writer.Allocating = .init(std.testing.allocator);
+ defer encoded.deinit();
+ try std.zon.stringify.serialize(state, .{}, &encoded.writer);
+ try std.testing.expectError(
+ error.BadDumpOriginArg,
+ readZon(std.testing.allocator, encoded.written(), "oversized-origin-arg"),
+ );
+}
+
pub fn writeFile(io: std.Io, gpa: std.mem.Allocator, path: []const u8, state: State) !void {
try validate(state);
var out: std.Io.Writer.Allocating = .init(gpa);
@@ -257,3 +316,96 @@ test "dump zon roundtrip" {
try std.testing.expectEqualStrings("alpha\nbeta\n", bytes);
}
}
+
+test "version-one image records default old fields and roundtrip new state" {
+ const gpa = std.testing.allocator;
+ const legacy =
+ \\.{
+ \\ .magic = "pardes-dump",
+ \\ .version = 1,
+ \\ .screen = .{ .cols = 80, .rows = 24 },
+ \\ .active = 0,
+ \\ .topbar = "",
+ \\ .theme = "dark",
+ \\ .columns = .{.{ .panes = .{0} }},
+ \\ .panes = .{.{
+ \\ .kind = .image,
+ \\ .tag = "img /tmp/old.ppm New Del",
+ \\ .body = "",
+ \\ .image = .{ .path = "/tmp/old.ppm", .bytes_b64 = "" },
+ \\ }},
+ \\}
+ ;
+ const old = try readZon(gpa, legacy, "legacy-image");
+ defer free(gpa, old);
+ const old_image = old.panes[0].image.?;
+ try std.testing.expect(!old_image.petscii);
+ try std.testing.expectEqual(ImagePalette.commodore, old_image.palette);
+ try std.testing.expect(old_image.ascii);
+ try std.testing.expect(old.panes[0].tag_tail == null);
+
+ const pane = Pane{
+ .kind = .image,
+ .tag = "img petscii:on palette:terminal ascii:off /tmp/new.ppm",
+ .body = "",
+ .tag_tail = "",
+ .image = .{
+ .path = "/tmp/new.ppm",
+ .petscii = true,
+ .palette = .terminal,
+ .ascii = false,
+ },
+ };
+ const ids = [_]usize{0};
+ const columns = [_]Column{.{ .panes = &ids }};
+ const panes = [_]Pane{pane};
+ const state = State{
+ .screen = .{ .cols = 80, .rows = 24 },
+ .columns = &columns,
+ .panes = &panes,
+ };
+ var out: std.Io.Writer.Allocating = .init(gpa);
+ defer out.deinit();
+ try std.zon.stringify.serialize(state, .{ .whitespace = true }, &out.writer);
+ const parsed = try readZon(gpa, out.written(), "new-image");
+ defer free(gpa, parsed);
+ const restored = parsed.panes[0].image.?;
+ try std.testing.expect(restored.petscii);
+ try std.testing.expectEqual(ImagePalette.terminal, restored.palette);
+ try std.testing.expect(!restored.ascii);
+ try std.testing.expect(parsed.panes[0].tag_tail != null);
+ try std.testing.expectEqualStrings("", parsed.panes[0].tag_tail.?);
+}
+
+test "validation bounds pane restore state" {
+ const ids = [_]usize{0};
+ const columns = [_]Column{.{ .panes = &ids }};
+ var panes = [_]Pane{.{
+ .kind = .terminal,
+ .tag = "term",
+ .body = "",
+ .terminal = .{},
+ }};
+ const state = State{
+ .screen = .{ .cols = 80, .rows = 24 },
+ .columns = &columns,
+ .panes = &panes,
+ };
+ try validate(state);
+
+ panes[0].vweight = 0;
+ try std.testing.expectError(error.BadDumpPaneWeight, validate(state));
+ panes[0].vweight = std.math.inf(f32);
+ try std.testing.expectError(error.BadDumpPaneWeight, validate(state));
+ panes[0].vweight = 1;
+
+ panes[0].scroll = @as(usize, @intCast(std.math.maxInt(i32))) + 1;
+ try std.testing.expectError(error.BadDumpPaneScroll, validate(state));
+ panes[0].scroll = 0;
+
+ var oversized_tail: [max_tag_tail + 1]u8 = @splat('x');
+ panes[0].tag_tail = &oversized_tail;
+ try std.testing.expectError(error.BadDumpTagTail, validate(state));
+ panes[0].tag_tail = oversized_tail[0..max_tag_tail];
+ try validate(state);
+}