summaryrefslogtreecommitdiff
path: root/src/dump.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/dump.zig')
-rw-r--r--src/dump.zig223
1 files changed, 170 insertions, 53 deletions
diff --git a/src/dump.zig b/src/dump.zig
index f8740cdb..bb692481 100644
--- a/src/dump.zig
+++ b/src/dump.zig
@@ -1,6 +1,6 @@
const std = @import("std");
const builtin = @import("builtin");
-const limits = @import("limits.zig");
+const limits = @import("memory.zig").limits;
// scoped, not bare std.log: main.zig's logFn drops the unscoped .default scope
// wholesale (ghostty and uucode log there too), and a corrupt dump's parse
@@ -74,13 +74,12 @@ pub const File = struct {
path: []const u8 = "",
content: []const u8 = "",
content_b64: []const u8 = "",
- /// non-empty = an output buffer (no file behind the name): the WORD of the
- /// command that opened it, plus that command's argument — see
- /// output_pane.Origin. A word rather than an integer for the reason every
- /// other command in here is a word: reordering builtins.zig stays free,
- /// and a dump stays something a person can read.
+ dirty: bool = false,
+ // Builtin names remain stable when enum ordinals change.
origin: []const u8 = "",
origin_arg: []const u8 = "",
+ mini_source: []const u8 = "",
+ mini_colors_b64: []const u8 = "",
};
pub const ImagePalette = enum {
@@ -119,6 +118,8 @@ pub const Column = struct {
panes: []const usize = &.{},
};
+pub const Mount = struct { name: []const u8, dial: []const u8 };
+
pub const State = struct {
magic: []const u8 = magic,
version: u32 = version,
@@ -128,6 +129,7 @@ pub const State = struct {
theme: []const u8 = "dark",
columns: []const Column = &.{},
panes: []const Pane = &.{},
+ mounts: []const Mount = &.{},
};
pub fn validate(state: State) !void {
@@ -136,6 +138,15 @@ pub fn validate(state: State) !void {
if (state.panes.len == 0 or state.panes.len > max_panes) return error.BadDumpPanes;
if (state.columns.len == 0 or state.columns.len > max_cols) return error.BadDumpColumns;
if (state.active >= state.panes.len) return error.BadDumpActive;
+ if (state.mounts.len > 8) return error.BadDumpMounts;
+ for (state.mounts, 0..) |mount, i| {
+ if (mount.name.len == 0 or mount.name.len > 255 or mount.dial.len == 0 or
+ std.mem.indexOfScalar(u8, mount.dial, 0) != null) return error.BadDumpMounts;
+ if (std.mem.eql(u8, mount.name, ".") or std.mem.eql(u8, mount.name, "..") or
+ std.mem.eql(u8, mount.name, "os") or std.mem.eql(u8, mount.name, "self")) return error.BadDumpMounts;
+ for (mount.name) |c| if (!std.ascii.isAlphanumeric(c) and c != '_' and c != '-' and c != '.') return error.BadDumpMounts;
+ for (state.mounts[0..i]) |previous| if (std.mem.eql(u8, mount.name, previous.name)) return error.BadDumpMounts;
+ }
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;
@@ -151,6 +162,11 @@ pub fn validate(state: State) !void {
return error.BadDumpTagTail;
if (pane.file) |file| if (file.origin_arg.len > max_origin_arg)
return error.BadDumpOriginArg;
+ if (pane.file) |file| {
+ if (file.mini_source.len > 4096 or std.mem.indexOfScalar(u8, file.mini_source, 0) != null or
+ (file.mini_source.len == 0 and file.mini_colors_b64.len != 0) or
+ (file.mini_source.len != 0 and !std.mem.eql(u8, file.origin, "Mini"))) return error.BadDumpMini;
+ }
switch (pane.kind) {
.terminal => if (pane.terminal == null) return error.BadDumpPaneKind,
.file => if (pane.file == null) return error.BadDumpPaneKind,
@@ -201,42 +217,37 @@ pub fn writeFile(io: std.Io, gpa: std.mem.Allocator, path: []const u8, state: St
try file.writeStreamingAll(io, out.written());
}
-pub fn readFile(io: std.Io, gpa: std.mem.Allocator, path: []const u8) !State {
- const bytes = try std.Io.Dir.cwd().readFileAlloc(io, path, gpa, .limited(64 * 1024 * 1024));
- defer gpa.free(bytes);
- return readZon(gpa, bytes, path);
-}
+pub const Parsed = struct {
+ value: State,
+ arena: std.heap.ArenaAllocator,
+
+ pub fn deinit(parsed: *Parsed) void {
+ parsed.arena.deinit();
+ }
+};
-pub fn readZon(gpa: std.mem.Allocator, bytes: []const u8, label: []const u8) !State {
+pub fn readZon(gpa: std.mem.Allocator, bytes: []const u8, label: []const u8) !Parsed {
+ var parsed: Parsed = .{ .value = undefined, .arena = .init(gpa) };
+ errdefer parsed.deinit();
+ const arena = parsed.arena.allocator();
const source = try gpa.dupeZ(u8, bytes);
defer gpa.free(source);
- return readZonZ(gpa, source, label);
-}
-pub fn readZonZ(gpa: std.mem.Allocator, source: [:0]const u8, label: []const u8) !State {
- if (builtin.os.tag == .emscripten or builtin.os.tag == .freestanding) {
- const state = std.zon.parse.fromSliceAlloc(State, gpa, source, null, .{}) catch |err| {
- log.err("parse dump {s}: {s}", .{ label, @errorName(err) });
+ parsed.value = if (builtin.os.tag == .emscripten or builtin.os.tag == .freestanding)
+ std.zon.parse.fromSliceAlloc(State, arena, source, null, .{ .free_on_error = false }) catch |err| {
+ if (err == error.ParseZon) log.err("parse dump {s}: {s}", .{ label, @errorName(err) });
+ return err;
+ }
+ else blk: {
+ var diag: std.zon.parse.Diagnostics = .{};
+ defer diag.deinit(arena);
+ break :blk std.zon.parse.fromSliceAlloc(State, arena, source, &diag, .{ .free_on_error = false }) catch |err| {
+ if (err == error.ParseZon) log.err("parse dump {s}: {f}", .{ label, diag });
return err;
};
- errdefer std.zon.parse.free(gpa, state);
- try validate(state);
- return state;
- }
-
- var diag: std.zon.parse.Diagnostics = .{};
- defer diag.deinit(gpa);
- const state = std.zon.parse.fromSliceAlloc(State, gpa, source, &diag, .{}) catch |err| {
- log.err("parse dump {s}: {f}", .{ label, diag });
- return err;
};
- errdefer std.zon.parse.free(gpa, state);
- try validate(state);
- return state;
-}
-
-pub fn free(gpa: std.mem.Allocator, state: State) void {
- std.zon.parse.free(gpa, state);
+ try validate(parsed.value);
+ return parsed;
}
pub fn encodeBytes(alloc: std.mem.Allocator, bytes: []const u8) ![]const u8 {
@@ -276,7 +287,7 @@ test "dump zon roundtrip" {
.body = " 1 alpha",
.cols = 20,
.rows = 5,
- .file = .{ .path = "/tmp/a.txt", .content = "alpha\nbeta\n", .content_b64 = file_b64 },
+ .file = .{ .path = "/tmp/a.txt", .content = "alpha\nbeta\n", .content_b64 = file_b64, .dirty = true },
},
};
const col_panes = [_]usize{ 0, 1 };
@@ -292,18 +303,14 @@ test "dump zon roundtrip" {
var out: std.Io.Writer.Allocating = .init(gpa);
defer out.deinit();
try std.zon.stringify.serialize(state, .{ .whitespace = true }, &out.writer);
- const source = try out.toOwnedSliceSentinel(0);
- defer gpa.free(source);
-
- var diag: std.zon.parse.Diagnostics = .{};
- defer diag.deinit(gpa);
- const parsed = try std.zon.parse.fromSliceAlloc(State, gpa, source, &diag, .{});
- defer std.zon.parse.free(gpa, parsed);
- try validate(parsed);
+ var result = try readZon(gpa, out.written(), "roundtrip");
+ defer result.deinit();
+ const parsed = result.value;
try std.testing.expectEqual(@as(usize, 2), parsed.panes.len);
try std.testing.expectEqualStrings("dark", parsed.theme);
try std.testing.expectEqualStrings("old\nhello\nworld", parsed.panes[0].terminal.?.stream);
try std.testing.expectEqualStrings("alpha\nbeta\n", parsed.panes[1].file.?.content);
+ try std.testing.expect(parsed.panes[1].file.?.dirty);
{
const bytes = try decodeBytes(gpa, parsed.panes[0].terminal.?.stream_b64);
defer gpa.free(bytes);
@@ -316,6 +323,116 @@ test "dump zon roundtrip" {
}
}
+test "omitted dump defaults roundtrip without borrowing input" {
+ const gpa = std.testing.allocator;
+ const fixture =
+ \\.{
+ \\ .screen = .{ .cols = 80, .rows = 24 },
+ \\ .columns = .{.{ .panes = .{0, 1, 2} }},
+ \\ .panes = .{
+ \\ .{ .kind = .terminal, .tag = "terminal", .body = "", .terminal = .{} },
+ \\ .{ .kind = .file, .tag = "file", .body = "", .file = .{ .path = "file.zig", .content = "const café = 1;" } },
+ \\ .{ .kind = .image, .tag = "image", .body = "", .image = .{} },
+ \\ },
+ \\}
+ ;
+ const input = try gpa.dupe(u8, fixture);
+ defer gpa.free(input);
+ var parsed = try readZon(gpa, input, "omitted-defaults");
+ defer parsed.deinit();
+ @memset(input, 'x');
+ const value = parsed.value;
+ try std.testing.expectEqualStrings(magic, value.magic);
+ try std.testing.expectEqualStrings("dark", value.theme);
+ try std.testing.expectEqualStrings("", value.topbar);
+ try std.testing.expectEqual(@as(usize, 0), value.mounts.len);
+ try std.testing.expectEqual(@as(usize, 3), value.panes.len);
+ try std.testing.expectEqualStrings("file.zig", value.panes[1].file.?.path);
+ try std.testing.expectEqualStrings("const café = 1;", value.panes[1].file.?.content);
+ try std.testing.expect(!value.panes[1].file.?.dirty);
+ try std.testing.expect(value.panes[2].image.?.ascii);
+
+ var encoded: std.Io.Writer.Allocating = .init(gpa);
+ defer encoded.deinit();
+ try std.zon.stringify.serialize(value, .{}, &encoded.writer);
+ var again = try readZon(gpa, encoded.written(), "default-roundtrip");
+ defer again.deinit();
+ try std.testing.expectEqualDeep(value, again.value);
+}
+
+test "dump parser releases its arena at every allocation failure" {
+ const Check = struct {
+ fn run(gpa: std.mem.Allocator) !void {
+ var parsed = try readZon(gpa,
+ \\.{
+ \\ .screen = .{ .cols = 80, .rows = 24 },
+ \\ .columns = .{.{ .panes = .{0} }},
+ \\ .panes = .{.{ .kind = .file, .tag = "file", .body = "", .file = .{ .content = "owned" } }},
+ \\}
+ , "allocation-cleanup");
+ defer parsed.deinit();
+ try std.testing.expectEqualStrings("owned", parsed.value.panes[0].file.?.content);
+ }
+ };
+ try std.testing.checkAllAllocationFailures(std.testing.allocator, Check.run, .{});
+}
+
+test "dump mount validation bounds names ownership inputs and duplicates" {
+ const panes = [_]Pane{.{ .kind = .file, .tag = "", .body = "", .file = .{} }};
+ const ids = [_]usize{0};
+ const columns = [_]Column{.{ .panes = &ids }};
+ var state: State = .{
+ .screen = .{ .cols = 80, .rows = 24 },
+ .columns = &columns,
+ .panes = &panes,
+ };
+ try validate(state);
+ for ([_]Mount{
+ .{ .name = "peer", .dial = "/tmp/peer.sock" },
+ .{ .name = "build-1.local", .dial = "tcp!127.0.0.1!5640" },
+ }) |mount| {
+ state.mounts = &.{mount};
+ try validate(state);
+ }
+ for ([_]Mount{
+ .{ .name = "", .dial = "/tmp/peer.sock" },
+ .{ .name = ".", .dial = "/tmp/peer.sock" },
+ .{ .name = "..", .dial = "/tmp/peer.sock" },
+ .{ .name = "os", .dial = "/tmp/peer.sock" },
+ .{ .name = "self", .dial = "/tmp/peer.sock" },
+ .{ .name = "two/parts", .dial = "/tmp/peer.sock" },
+ .{ .name = "two parts", .dial = "/tmp/peer.sock" },
+ .{ .name = "peer", .dial = "" },
+ .{ .name = "peer", .dial = "unix!/tmp/peer\x00.sock" },
+ }) |mount| {
+ state.mounts = &.{mount};
+ try std.testing.expectError(error.BadDumpMounts, validate(state));
+ }
+ const duplicate = Mount{ .name = "peer", .dial = "/tmp/peer.sock" };
+ state.mounts = &.{ duplicate, duplicate };
+ try std.testing.expectError(error.BadDumpMounts, validate(state));
+ var name: [256]u8 = @splat('x');
+ state.mounts = &.{.{ .name = name[0..255], .dial = "/tmp/peer.sock" }};
+ try validate(state);
+ state.mounts = &.{.{ .name = &name, .dial = "/tmp/peer.sock" }};
+ try std.testing.expectError(error.BadDumpMounts, validate(state));
+ const mounts = [_]Mount{
+ .{ .name = "a", .dial = "/tmp/a" },
+ .{ .name = "b", .dial = "/tmp/b" },
+ .{ .name = "c", .dial = "/tmp/c" },
+ .{ .name = "d", .dial = "/tmp/d" },
+ .{ .name = "e", .dial = "/tmp/e" },
+ .{ .name = "f", .dial = "/tmp/f" },
+ .{ .name = "g", .dial = "/tmp/g" },
+ .{ .name = "h", .dial = "/tmp/h" },
+ .{ .name = "i", .dial = "/tmp/i" },
+ };
+ state.mounts = mounts[0..8];
+ try validate(state);
+ state.mounts = &mounts;
+ try std.testing.expectError(error.BadDumpMounts, validate(state));
+}
+
test "version-one image records default old fields and roundtrip new state" {
const gpa = std.testing.allocator;
const legacy =
@@ -335,13 +452,13 @@ test "version-one image records default old fields and roundtrip new state" {
\\ }},
\\}
;
- const old = try readZon(gpa, legacy, "legacy-image");
- defer free(gpa, old);
- const old_image = old.panes[0].image.?;
+ var old = try readZon(gpa, legacy, "legacy-image");
+ defer old.deinit();
+ const old_image = old.value.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);
+ try std.testing.expect(old.value.panes[0].tag_tail == null);
const pane = Pane{
.kind = .image,
@@ -366,14 +483,14 @@ test "version-one image records default old fields and roundtrip new state" {
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.?;
+ var parsed = try readZon(gpa, out.written(), "new-image");
+ defer parsed.deinit();
+ const restored = parsed.value.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.?);
+ try std.testing.expect(parsed.value.panes[0].tag_tail != null);
+ try std.testing.expectEqualStrings("", parsed.value.panes[0].tag_tail.?);
}
test "validation bounds pane restore state" {