diff options
Diffstat (limited to '9proc/src/vars.zig')
| -rw-r--r-- | 9proc/src/vars.zig | 824 |
1 files changed, 824 insertions, 0 deletions
diff --git a/9proc/src/vars.zig b/9proc/src/vars.zig new file mode 100644 index 0000000..20cadfc --- /dev/null +++ b/9proc/src/vars.zig @@ -0,0 +1,824 @@ +//! Comptime value renderers for /vars. For a type T, `vtableFor(T)` builds (at +//! comptime) a flat table of the files and directories that describe a value of +//! that type: +//! +//! <var>/value rendered text <var>/type @typeName +//! <var>/size @sizeOf <var>/addr 0x… +//! <var>/raw the bytes <var>/f/<field>/... recursively (depth <= max_depth) +//! +//! The core serves a variable by walking this table; a node index is the whole +//! state it needs. Rendering writes into a `*std.Io.Writer` and never allocates. +//! Writes to scalar `value` files are plain stores (not atomic). +const std = @import("std"); +const Writer = std.Io.Writer; + +/// Deepest `f/` nesting: /vars/x/f/a/f/b/f/c/f/d/value is depth 4. +pub const max_depth: u8 = 4; +/// Longest string rendered from a `[]const u8` / `[*:0]const u8` before "…". +pub const max_string: usize = 256; +/// Most array/slice elements rendered before "…". +pub const max_elems: usize = 64; + +pub const Kind = enum(u8) { + /// The directory of a value: value, type, size, addr, raw, [f]. + dir, + /// Rendered text; writable when `set` is non-null. + value, + /// @typeName, static content. + type_name, + /// @sizeOf, static content. + size, + /// "0x…" of the value's address. + addr, + /// The bytes of the value, length = size. + raw, + /// The `f` directory: one `dir` per struct field. + fields, +}; + +pub const RenderFn = *const fn (base: [*]const u8, w: *Writer) Writer.Error!void; +pub const SetFn = *const fn (base: [*]u8, text: []const u8) SetError!void; +pub const SetError = error{ Invalid, Unsupported }; + +/// One node of a type's tree. Children occupy `first..first+count`. +pub const Node = struct { + name: []const u8, + kind: Kind, + parent: u32, + first: u32 = 0, + count: u32 = 0, + /// Byte offset of the described value from the variable's base address. + offset: usize, + /// @sizeOf the described value. + size: usize, + /// Static text for `type_name` and `size` leaves. + content: []const u8 = "", + render: ?RenderFn = null, + set: ?SetFn = null, + + pub fn isDir(n: Node) bool { + return n.kind == .dir or n.kind == .fields; + } + + pub fn writable(n: Node) bool { + return n.kind == .value and n.set != null; + } +}; + +pub const VTable = struct { + nodes: []const Node, + type_name: []const u8, + size: usize, + + /// The child of `dir` named `name`, if any. + pub fn child(vt: *const VTable, dir: u32, name: []const u8) ?u32 { + const d = vt.nodes[dir]; + for (d.first..d.first + d.count) |i| { + if (std.mem.eql(u8, vt.nodes[i].name, name)) return @intCast(i); + } + return null; + } +}; + +/// The comptime-generated table for `T`; the same pointer for the same `T`. +pub fn vtableFor(comptime T: type) *const VTable { + const S = struct { + const nodes = buildTable(T); + const vt: VTable = .{ .nodes = &nodes, .type_name = @typeName(T), .size = @sizeOf(T) }; + }; + return &S.vt; +} + +/// Whether a struct's fields get an `f/` directory at this depth. +fn hasFields(comptime T: type, depth: u8) bool { + if (depth >= max_depth) return false; + return switch (@typeInfo(T)) { + .@"struct" => |s| s.layout != .@"packed" and fieldCount(T) > 0, + else => false, + }; +} + +fn fieldCount(comptime T: type) usize { + var n: usize = 0; + for (@typeInfo(T).@"struct".fields) |f| { + if (!f.is_comptime and @sizeOf(f.type) != 0) n += 1; + } + return n; +} + +fn countNodes(comptime T: type, depth: u8) usize { + var n: usize = 6; // dir + value, type, size, addr, raw + if (hasFields(T, depth)) { + n += 1; // f + for (@typeInfo(T).@"struct".fields) |f| { + if (f.is_comptime or @sizeOf(f.type) == 0) continue; + n += countNodes(f.type, depth + 1); + } + } + return n; +} + +fn fill(nodes: []Node, next: *usize, idx: usize, comptime T: type, name: []const u8, offset: usize, depth: u8, parent: u32) void { + const with_fields = hasFields(T, depth); + const count: u32 = if (with_fields) 6 else 5; + const first = next.*; + next.* += count; + nodes[idx] = .{ .name = name, .kind = .dir, .parent = parent, .first = @intCast(first), .count = count, .offset = offset, .size = @sizeOf(T) }; + const me: u32 = @intCast(idx); + nodes[first + 0] = .{ .name = "value", .kind = .value, .parent = me, .offset = offset, .size = @sizeOf(T), .render = renderFor(T), .set = setFor(T) }; + nodes[first + 1] = .{ .name = "type", .kind = .type_name, .parent = me, .offset = offset, .size = @sizeOf(T), .content = @typeName(T) }; + nodes[first + 2] = .{ .name = "size", .kind = .size, .parent = me, .offset = offset, .size = @sizeOf(T), .content = std.fmt.comptimePrint("{d}", .{@sizeOf(T)}) }; + nodes[first + 3] = .{ .name = "addr", .kind = .addr, .parent = me, .offset = offset, .size = @sizeOf(T) }; + nodes[first + 4] = .{ .name = "raw", .kind = .raw, .parent = me, .offset = offset, .size = @sizeOf(T) }; + if (with_fields) { + const fdir: u32 = @intCast(first + 5); + const nf = fieldCount(T); + const ffirst = next.*; + next.* += nf; + nodes[fdir] = .{ .name = "f", .kind = .fields, .parent = me, .first = @intCast(ffirst), .count = @intCast(nf), .offset = offset, .size = @sizeOf(T) }; + var i: usize = 0; + for (@typeInfo(T).@"struct".fields) |f| { + if (f.is_comptime or @sizeOf(f.type) == 0) continue; + fill(nodes, next, ffirst + i, f.type, f.name, offset + @offsetOf(T, f.name), depth + 1, fdir); + i += 1; + } + } +} + +fn buildTable(comptime T: type) [countNodes(T, 0)]Node { + @setEvalBranchQuota(1_000_000); + var nodes: [countNodes(T, 0)]Node = undefined; + var next: usize = 1; + fill(&nodes, &next, 0, T, "", 0, 0, 0); + std.debug.assert(next == nodes.len); + return nodes; +} + +fn renderFor(comptime T: type) RenderFn { + return &struct { + fn f(base: [*]const u8, w: *Writer) Writer.Error!void { + const p: *const T = @ptrCast(@alignCast(base)); + try render(p, w, max_depth); + } + }.f; +} + +fn setFor(comptime T: type) ?SetFn { + if (!settable(T)) return null; + return &struct { + fn f(base: [*]u8, text: []const u8) SetError!void { + const p: *T = @ptrCast(@alignCast(base)); + try set(p, text); + } + }.f; +} + +fn settable(comptime T: type) bool { + return switch (@typeInfo(T)) { + .int, .float, .bool, .@"enum" => true, + else => false, + }; +} + +// --------------------------------------------------------------------------- +// Rendering +// --------------------------------------------------------------------------- + +/// Renders `ptr.*`. Structs become "field: value" lines (nested structs +/// indented); everything else is a single line without a trailing newline. +/// `depth` bounds struct/union/optional nesting; deeper values print as "…". +pub fn render(ptr: anytype, w: *Writer, depth: usize) Writer.Error!void { + const T = @TypeOf(ptr.*); + if (comptime isPlainStruct(T)) { + try renderStruct(T, ptr, w, depth, 0); + } else { + try renderValue(T, ptr, w, depth); + } +} + +fn isPlainStruct(comptime T: type) bool { + return switch (@typeInfo(T)) { + .@"struct" => |s| !s.is_tuple and s.fields.len > 0, + else => false, + }; +} + +/// The multi-line form: each field on its own line, nested structs indented. +fn renderStruct(comptime T: type, ptr: *const T, w: *Writer, depth: usize, indent: usize) Writer.Error!void { + if (depth == 0) { + try w.splatByteAll(' ', indent); + try w.writeAll("…\n"); + return; + } + const packed_layout = @typeInfo(T).@"struct".layout == .@"packed"; + inline for (@typeInfo(T).@"struct".fields) |f| { + try w.splatByteAll(' ', indent); + try w.writeAll(f.name); + try w.writeByte(':'); + if (comptime f.is_comptime) { + try w.writeAll(" (comptime)\n"); + } else if (comptime packed_layout) { + // Fields of a packed struct have no byte address: render a copy. + const v = @field(ptr.*, f.name); + try w.writeByte(' '); + try renderValue(f.type, &v, w, depth - 1); + try w.writeByte('\n'); + } else if (comptime isPlainStruct(f.type)) { + try w.writeByte('\n'); + try renderStruct(f.type, &@field(ptr.*, f.name), w, depth - 1, indent + 2); + } else { + try w.writeByte(' '); + try renderValue(f.type, &@field(ptr.*, f.name), w, depth - 1); + try w.writeByte('\n'); + } + } +} + +/// The single-line form of any value. +fn renderValue(comptime T: type, ptr: *const T, w: *Writer, depth: usize) Writer.Error!void { + switch (@typeInfo(T)) { + .int, .comptime_int => try w.print("{d}", .{ptr.*}), + .float, .comptime_float => try w.print("{d}", .{ptr.*}), + .bool => { + // The variable is live memory that anything (a debugger's /mem write, + // a torn update) may have corrupted: judge the byte, not the bool. + const b = @as(*const u8, @ptrCast(ptr)).*; + switch (b) { + 0 => try w.writeAll("false"), + 1 => try w.writeAll("true"), + else => try w.print("{d}", .{b}), + } + }, + .void => try w.writeAll("{}"), + .@"enum" => |e| { + // Read the storage bytes as one integer: @tagName/switch on a corrupt + // value is a safety panic, and the value is caller memory we do not + // control. A load through the tag type would truncate to its bit + // width (a u2 tag in a byte), so the full storage width is read. + if (@sizeOf(T) == 0) return w.writeAll(e.fields[0].name); + const Raw = std.meta.Int(.unsigned, @sizeOf(T) * 8); + const raw = @as(*align(@alignOf(T)) const Raw, @ptrCast(ptr)).*; + const TagU = std.meta.Int(.unsigned, @bitSizeOf(e.tag_type)); + const padding: Raw = if (@bitSizeOf(TagU) == @bitSizeOf(Raw)) 0 else ~@as(Raw, std.math.maxInt(TagU)); + if (raw & padding == 0) { + const low: TagU = @truncate(raw); + inline for (e.fields) |f| { + if (low == @as(TagU, @bitCast(@as(e.tag_type, f.value)))) return w.writeAll(f.name); + } + } + try w.print("{d}", .{raw}); + }, + .error_set => try w.print("error.{s}", .{@errorName(ptr.*)}), + .error_union => |eu| if (ptr.*) |v| { + try renderValue(eu.payload, &v, w, depth); + } else |e| { + try w.print("error.{s}", .{@errorName(e)}); + }, + .optional => |o| if (ptr.*) |v| { + try renderValue(o.child, &v, w, depth); + } else { + try w.writeAll("null"); + }, + .pointer => |p| switch (p.size) { + .slice => if (p.child == u8) { + try renderString(ptr.*, w); + } else { + try renderElems(p.child, ptr.*, w, depth); + }, + .many => if (p.child == u8 and p.sentinel() == 0) { + try renderCString(ptr.*, w); + } else { + try w.print("0x{x}", .{@intFromPtr(ptr.*)}); + }, + .one, .c => try w.print("0x{x}", .{@intFromPtr(ptr.*)}), + }, + .array => |a| if (a.child == u8) { + try renderString(ptr.*[0..], w); + } else { + try renderElems(a.child, ptr.*[0..], w, depth); + }, + .vector => |v| { + const arr: [v.len]v.child = ptr.*; + try renderElems(v.child, &arr, w, depth); + }, + .@"struct" => |s| { + if (depth == 0) { + try w.writeAll("…"); + return; + } + if (s.fields.len == 0) { + try w.writeAll("{}"); + return; + } + try w.writeAll("{ "); + inline for (s.fields, 0..) |f, i| { + if (i != 0) try w.writeAll(", "); + if (!s.is_tuple) { + try w.writeAll(f.name); + try w.writeAll(": "); + } + if (comptime f.is_comptime) { + try w.writeAll("(comptime)"); + } else if (comptime s.layout == .@"packed") { + const v = @field(ptr.*, f.name); + try renderValue(f.type, &v, w, depth - 1); + } else { + try renderValue(f.type, &@field(ptr.*, f.name), w, depth - 1); + } + } + try w.writeAll(" }"); + }, + .@"union" => |u| { + const Tag = u.tag_type orelse { + try w.print("(untagged union, {d} bytes)", .{@sizeOf(T)}); + return; + }; + if (depth == 0) { + try w.writeAll("…"); + return; + } + // A switch on a corrupt tag is a safety panic: match the integer first. + const raw = @intFromEnum(@as(Tag, ptr.*)); + inline for (u.fields) |f| { + if (raw == @intFromEnum(@field(Tag, f.name))) { + try w.writeAll(f.name); + if (f.type != void) { + try w.writeAll(": "); + try renderValue(f.type, &@field(ptr.*, f.name), w, depth - 1); + } + return; + } + } + try w.print("(invalid tag {d})", .{raw}); + }, + .@"fn" => try w.print("0x{x}", .{@intFromPtr(ptr)}), + else => try w.print("<{s}>", .{@typeName(T)}), + } +} + +fn renderElems(comptime E: type, items: []const E, w: *Writer, depth: usize) Writer.Error!void { + try w.writeByte('['); + for (items, 0..) |*item, i| { + if (i == max_elems) { + try w.writeAll(", …"); + break; + } + if (i != 0) try w.writeAll(", "); + try renderValue(E, item, w, depth); + } + try w.writeByte(']'); +} + +/// A NUL-terminated string, scanning at most `max_string` + 1 bytes for the +/// terminator so that a missing one cannot walk off the end of the mapping. +fn renderCString(s: [*:0]const u8, w: *Writer) Writer.Error!void { + var n: usize = 0; + while (n <= max_string and s[n] != 0) n += 1; + try renderString(s[0..n], w); +} + +/// A double-quoted string with C-style escapes, truncated to `max_string` bytes. +fn renderString(s: []const u8, w: *Writer) Writer.Error!void { + try w.writeByte('"'); + for (s[0..@min(s.len, max_string)]) |b| switch (b) { + '\n' => try w.writeAll("\\n"), + '\r' => try w.writeAll("\\r"), + '\t' => try w.writeAll("\\t"), + '\\' => try w.writeAll("\\\\"), + '"' => try w.writeAll("\\\""), + ' '...'!', '#'...'[', ']'...'~' => try w.writeByte(b), + else => { + const hex = "0123456789abcdef"; + try w.writeAll("\\x"); + try w.writeByte(hex[b >> 4]); + try w.writeByte(hex[b & 15]); + }, + }; + try w.writeByte('"'); + if (s.len > max_string) try w.writeAll("…"); +} + +// --------------------------------------------------------------------------- +// Setting +// --------------------------------------------------------------------------- + +/// Parses `text` and stores it into `ptr.*`: ints in decimal or 0x/0o/0b, +/// floats, bools (true/false/1/0), enums by tag name (or by integer value for +/// non-exhaustive enums). Other types are `error.Unsupported`. +pub fn set(ptr: anytype, text: []const u8) SetError!void { + const T = @TypeOf(ptr.*); + const s = std.mem.trim(u8, text, " \t\r\n\x00"); + switch (@typeInfo(T)) { + .int => ptr.* = std.fmt.parseInt(T, s, 0) catch return error.Invalid, + .float => ptr.* = std.fmt.parseFloat(T, s) catch return error.Invalid, + .bool => { + if (std.mem.eql(u8, s, "true") or std.mem.eql(u8, s, "1")) { + ptr.* = true; + } else if (std.mem.eql(u8, s, "false") or std.mem.eql(u8, s, "0")) { + ptr.* = false; + } else return error.Invalid; + }, + .@"enum" => |e| { + if (std.meta.stringToEnum(T, s)) |v| { + ptr.* = v; + } else if (!e.is_exhaustive) { + const raw = std.fmt.parseInt(e.tag_type, s, 0) catch return error.Invalid; + ptr.* = @enumFromInt(raw); + } else return error.Invalid; + }, + else => return error.Unsupported, + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +const testing = std.testing; + +fn renderToBuf(buf: []u8, ptr: anytype) ![]const u8 { + var w: Writer = .fixed(buf); + try render(ptr, &w, max_depth); + return w.buffered(); +} + +test "render scalars, strings, pointers, optionals, enums, arrays" { + var buf: [512]u8 = undefined; + const i: i32 = -42; + try testing.expectEqualStrings("-42", try renderToBuf(&buf, &i)); + const f: f32 = 1.5; + try testing.expectEqualStrings("1.5", try renderToBuf(&buf, &f)); + const b: bool = true; + try testing.expectEqualStrings("true", try renderToBuf(&buf, &b)); + const s: []const u8 = "hi \"there\"\n"; + try testing.expectEqualStrings("\"hi \\\"there\\\"\\n\"", try renderToBuf(&buf, &s)); + const z: [*:0]const u8 = "zed"; + try testing.expectEqualStrings("\"zed\"", try renderToBuf(&buf, &z)); + const p: *const i32 = &i; + var expect_buf: [32]u8 = undefined; + const expect = try std.fmt.bufPrint(&expect_buf, "0x{x}", .{@intFromPtr(&i)}); + try testing.expectEqualStrings(expect, try renderToBuf(&buf, &p)); + const o: ?u8 = null; + try testing.expectEqualStrings("null", try renderToBuf(&buf, &o)); + const o2: ?u8 = 7; + try testing.expectEqualStrings("7", try renderToBuf(&buf, &o2)); + const E = enum { red, green }; + const e: E = .green; + try testing.expectEqualStrings("green", try renderToBuf(&buf, &e)); + const NE = enum(u8) { a, _ }; + const ne: NE = @enumFromInt(9); + try testing.expectEqualStrings("9", try renderToBuf(&buf, &ne)); + const arr = [_]u16{ 1, 2, 3 }; + try testing.expectEqualStrings("[1, 2, 3]", try renderToBuf(&buf, &arr)); + const bytes = [_]u8{ 0, 'a', 0xff }; + try testing.expectEqualStrings("\"\\x00a\\xff\"", try renderToBuf(&buf, &bytes)); + const U = union(enum) { none, some: u32 }; + const u: U = .{ .some = 5 }; + try testing.expectEqualStrings("some: 5", try renderToBuf(&buf, &u)); + const un: U = .none; + try testing.expectEqualStrings("none", try renderToBuf(&buf, &un)); +} + +test "render structs multi-line with nested indentation and depth limit" { + const Inner = struct { x: f32, flags: [2]bool }; + const Outer = struct { a: u32, b: bool, name: []const u8, inner: Inner, items: []const Inner }; + const v: Outer = .{ .a = 1, .b = false, .name = "n", .inner = .{ .x = 2.5, .flags = .{ true, false } }, .items = &.{.{ .x = 0, .flags = .{ false, false } }} }; + var buf: [512]u8 = undefined; + try testing.expectEqualStrings( + \\a: 1 + \\b: false + \\name: "n" + \\inner: + \\ x: 2.5 + \\ flags: [true, false] + \\items: [{ x: 0, flags: [false, false] }] + \\ + , try renderToBuf(&buf, &v)); + var w: Writer = .fixed(&buf); + try render(&v, &w, 1); + try testing.expectEqualStrings( + \\a: 1 + \\b: false + \\name: "n" + \\inner: + \\ … + \\items: […] + \\ + , w.buffered()); +} + +test "long strings and arrays are truncated" { + const long = [_]u8{'x'} ** 300; + var buf: [1024]u8 = undefined; + const s: []const u8 = &long; + const out = try renderToBuf(&buf, &s); + try testing.expectEqual(@as(usize, 1 + max_string + 1 + "…".len), out.len); + try testing.expect(std.mem.endsWith(u8, out, "\"…")); + const nums: [100]u32 = @splat(1); + const out2 = try renderToBuf(&buf, &nums); + try testing.expect(std.mem.endsWith(u8, out2, ", …]")); + try testing.expectEqual(@as(usize, max_elems), std.mem.count(u8, out2, "1")); +} + +test "set parses ints, floats, bools and enums" { + var i: u32 = 0; + try set(&i, "42\n"); + try testing.expectEqual(@as(u32, 42), i); + try set(&i, "0x10"); + try testing.expectEqual(@as(u32, 16), i); + try testing.expectError(error.Invalid, set(&i, "-1")); + try testing.expectError(error.Invalid, set(&i, "abc")); + var si: i8 = 0; + try set(&si, " -7 "); + try testing.expectEqual(@as(i8, -7), si); + try testing.expectError(error.Invalid, set(&si, "200")); + var f: f64 = 0; + try set(&f, "2.25"); + try testing.expectEqual(@as(f64, 2.25), f); + var b: bool = false; + try set(&b, "true"); + try testing.expect(b); + try set(&b, "0"); + try testing.expect(!b); + try testing.expectError(error.Invalid, set(&b, "maybe")); + const E = enum { off, on }; + var e: E = .off; + try set(&e, "on"); + try testing.expectEqual(E.on, e); + try testing.expectError(error.Invalid, set(&e, "blue")); + var s: []const u8 = "x"; + try testing.expectError(error.Unsupported, set(&s, "y")); +} + +test "vtable table layout for a nested struct" { + const Inner = struct { x: f32 }; + const T = struct { a: u32, b: bool, name: []const u8, inner: Inner }; + const vt = vtableFor(T); + try testing.expectEqual(vt, vtableFor(T)); + try testing.expectEqualStrings(@typeName(T), vt.type_name); + const root = vt.nodes[0]; + try testing.expect(root.isDir()); + try testing.expectEqual(@as(u32, 6), root.count); + const value = vt.child(0, "value").?; + try testing.expect(!vt.nodes[value].writable()); // a struct is not settable + try testing.expectEqualStrings(std.fmt.comptimePrint("{d}", .{@sizeOf(T)}), vt.nodes[vt.child(0, "size").?].content); + const f = vt.child(0, "f").?; + try testing.expectEqual(Kind.fields, vt.nodes[f].kind); + try testing.expectEqual(@as(u32, 4), vt.nodes[f].count); + const a = vt.child(f, "a").?; + try testing.expectEqual(@offsetOf(T, "a"), vt.nodes[a].offset); + const a_value = vt.child(a, "value").?; + try testing.expect(vt.nodes[a_value].writable()); + try testing.expectEqual(@as(usize, 4), vt.nodes[a_value].size); + try testing.expectEqual(a, vt.nodes[a_value].parent); + const inner = vt.child(f, "inner").?; + const inner_f = vt.child(inner, "f").?; + const x = vt.child(inner_f, "x").?; + try testing.expectEqual(@offsetOf(T, "inner") + @offsetOf(Inner, "x"), vt.nodes[x].offset); + try testing.expectEqualStrings("f32", vt.nodes[vt.child(x, "type").?].content); + try testing.expect(vt.child(f, "nope") == null); + // rendering and setting through the table + var v: T = .{ .a = 1, .b = true, .name = "n", .inner = .{ .x = 0.5 } }; + const base: [*]u8 = @ptrCast(&v); + var buf: [256]u8 = undefined; + var w: Writer = .fixed(&buf); + const x_value = vt.child(x, "value").?; + try vt.nodes[x_value].render.?(base + vt.nodes[x_value].offset, &w); + try testing.expectEqualStrings("0.5", w.buffered()); + try vt.nodes[a_value].set.?(base + vt.nodes[a_value].offset, "42"); + try testing.expectEqual(@as(u32, 42), v.a); + w = .fixed(&buf); + try vt.nodes[value].render.?(base, &w); + try testing.expect(std.mem.startsWith(u8, w.buffered(), "a: 42\nb: true\n")); +} + +test "depth limit stops the f/ tree at max_depth" { + const L4 = struct { v: u8 }; + const L3 = struct { l4: L4 }; + const L2 = struct { l3: L3 }; + const L1 = struct { l2: L2 }; + const L0 = struct { l1: L1 }; + const vt = vtableFor(L0); + var node: u32 = 0; + var depth: usize = 0; + while (vt.child(node, "f")) |f| : (depth += 1) { + node = vt.nodes[f].first; // the single field + } + try testing.expectEqual(@as(usize, max_depth), depth); + try testing.expect(vt.child(node, "value") != null); +} + +test "every @typeInfo category renders without dereferencing anything unbounded" { + var buf: [2048]u8 = undefined; + // packed and extern structs (packed fields have no address: rendered by copy) + const Packed = packed struct { a: u3, b: bool, c: u12, e: enum(u2) { p, q, r } }; + const pk: Packed = .{ .a = 5, .b = true, .c = 300, .e = .r }; + try testing.expectEqualStrings("a: 5\nb: true\nc: 300\ne: r\n", try renderToBuf(&buf, &pk)); + const Ext = extern struct { x: u16, y: f32, inner: extern struct { z: u8 } }; + const ex: Ext = .{ .x = 1, .y = 0.5, .inner = .{ .z = 9 } }; + try testing.expectEqualStrings("x: 1\ny: 0.5\ninner:\n z: 9\n", try renderToBuf(&buf, &ex)); + const Holder = struct { p: Packed, list: [2]Packed }; + const ho: Holder = .{ .p = pk, .list = .{ pk, pk } }; + try testing.expect(std.mem.startsWith(u8, try renderToBuf(&buf, &ho), "p:\n a: 5\n")); + // the f/ tree has no entries for a packed struct and works through a table + const vt = vtableFor(Packed); + try testing.expect(vt.child(0, "f") == null); + var w: Writer = .fixed(&buf); + try vt.nodes[vt.child(0, "value").?].render.?(@ptrCast(&pk), &w); + try testing.expect(std.mem.startsWith(u8, w.buffered(), "a: 5\n")); + // optionals of pointers are printed, never followed + var target: u32 = 7; + const op: ?*u32 = ⌖ + var expect_buf: [32]u8 = undefined; + try testing.expectEqualStrings(try std.fmt.bufPrint(&expect_buf, "0x{x}", .{@intFromPtr(&target)}), try renderToBuf(&buf, &op)); + const np: ?*u32 = null; + try testing.expectEqualStrings("null", try renderToBuf(&buf, &np)); + const dangling: *const u32 = @ptrFromInt(0x1000); + try testing.expectEqualStrings("0x1000", try renderToBuf(&buf, &dangling)); + const cptr: [*c]const u8 = @ptrFromInt(0x2000); + try testing.expectEqualStrings("0x2000", try renderToBuf(&buf, &cptr)); + const manyp: [*]const u32 = @ptrFromInt(0x3000); + try testing.expectEqualStrings("0x3000", try renderToBuf(&buf, &manyp)); + // untagged and tagged unions, error unions, error sets + const Untagged = union { a: u32, b: f32 }; + const un: Untagged = .{ .a = 1 }; + try testing.expectEqualStrings(std.fmt.comptimePrint("(untagged union, {d} bytes)", .{@sizeOf(Untagged)}), try renderToBuf(&buf, &un)); + const Tagged = union(enum(u8)) { none, some: u32, pair: struct { l: u8, r: u8 } }; + const tg: Tagged = .{ .pair = .{ .l = 1, .r = 2 } }; + try testing.expectEqualStrings("pair: { l: 1, r: 2 }", try renderToBuf(&buf, &tg)); + const eu: anyerror!u8 = error.Boom; + try testing.expectEqualStrings("error.Boom", try renderToBuf(&buf, &eu)); + const eu2: error{X}!u8 = 4; + try testing.expectEqualStrings("4", try renderToBuf(&buf, &eu2)); + const es: anyerror = error.Zap; + try testing.expectEqualStrings("error.Zap", try renderToBuf(&buf, &es)); + // wide ints and floats, vectors, sentinel arrays, slices of slices, void, comptime fields + const big: u128 = std.math.maxInt(u128); + try testing.expectEqualStrings("340282366920938463463374607431768211455", try renderToBuf(&buf, &big)); + const neg: i128 = std.math.minInt(i128); + try testing.expectEqualStrings("-170141183460469231731687303715884105728", try renderToBuf(&buf, &neg)); + const h: f16 = 1.5; + try testing.expectEqualStrings("1.5", try renderToBuf(&buf, &h)); + const ld: f80 = 2.25; + try testing.expectEqualStrings("2.25", try renderToBuf(&buf, &ld)); + const quad: f128 = 3.125; + try testing.expectEqualStrings("3.125", try renderToBuf(&buf, &quad)); + const vec: @Vector(4, i16) = .{ 1, -2, 3, -4 }; + try testing.expectEqualStrings("[1, -2, 3, -4]", try renderToBuf(&buf, &vec)); + const sarr: [3:0]u8 = .{ 'a', 'b', 'c' }; + try testing.expectEqualStrings("\"abc\"", try renderToBuf(&buf, &sarr)); + const rows: []const []const u8 = &.{ "ab", "cd" }; + try testing.expectEqualStrings("[\"ab\", \"cd\"]", try renderToBuf(&buf, &rows)); + const Odd = struct { v: void, comptime k: u8 = 3, n: u8 }; + const odd: Odd = .{ .v = {}, .n = 1 }; + try testing.expectEqualStrings("v: {}\nk: (comptime)\nn: 1\n", try renderToBuf(&buf, &odd)); + try testing.expectEqual(@as(u32, 1), vtableFor(Odd).nodes[vtableFor(Odd).child(0, "f").?].count); + // self-referential through a pointer: rendered as an address, table stays finite + const Link = struct { next: ?*const @This(), v: u8 }; + var a: Link = .{ .next = null, .v = 1 }; + const b: Link = .{ .next = &a, .v = 2 }; + a.next = &b; + try testing.expectEqualStrings(try std.fmt.bufPrint(&expect_buf, "next: 0x{x}\nv: 2\n", .{@intFromPtr(&a)}), try renderToBuf(&buf, &b)); + try testing.expect(vtableFor(Link).nodes.len < 32); + // tuples + const tup: struct { u8, []const u8 } = .{ 1, "x" }; + try testing.expectEqualStrings("{ 1, \"x\" }", try renderToBuf(&buf, &tup)); +} + +test "corrupt live memory renders instead of trapping: enums, unions, bools" { + var buf: [128]u8 = undefined; + const E = enum(u8) { a, b }; + var raw_e: u8 = 7; + try testing.expectEqualStrings("7", try renderToBuf(&buf, @as(*const E, @ptrCast(&raw_e)))); + raw_e = 1; + try testing.expectEqualStrings("b", try renderToBuf(&buf, @as(*const E, @ptrCast(&raw_e)))); + // a u2 tag in a byte: the whole byte is judged, not the truncated tag (ReleaseSafe would say "c") + const E3 = enum { a, b, c }; + var raw3: u8 = 0xEE; + try testing.expectEqualStrings("238", try renderToBuf(&buf, @as(*const E3, @ptrCast(&raw3)))); + raw3 = 3; + try testing.expectEqualStrings("3", try renderToBuf(&buf, @as(*const E3, @ptrCast(&raw3)))); + raw3 = 2; + try testing.expectEqualStrings("c", try renderToBuf(&buf, @as(*const E3, @ptrCast(&raw3)))); + const E12 = enum(u12) { p = 5, q = 4095 }; + var raw12: u16 = 0xF005; + try testing.expectEqualStrings("61445", try renderToBuf(&buf, @as(*const E12, @ptrCast(&raw12)))); + raw12 = 4095; + try testing.expectEqualStrings("q", try renderToBuf(&buf, @as(*const E12, @ptrCast(&raw12)))); + const ES = enum(i8) { neg = -3, pos = 7 }; + var raws: u8 = 0xFD; + try testing.expectEqualStrings("neg", try renderToBuf(&buf, @as(*const ES, @ptrCast(&raws)))); + raws = 0x80; + try testing.expectEqualStrings("128", try renderToBuf(&buf, @as(*const ES, @ptrCast(&raws)))); + const E1 = enum { only }; + const e1: E1 = .only; + try testing.expectEqualStrings("only", try renderToBuf(&buf, &e1)); + const NE = enum(u16) { x = 5, _ }; + var raw_ne: u16 = 5; + try testing.expectEqualStrings("x", try renderToBuf(&buf, @as(*const NE, @ptrCast(&raw_ne)))); + raw_ne = 6; + try testing.expectEqualStrings("6", try renderToBuf(&buf, @as(*const NE, @ptrCast(&raw_ne)))); + var raw_b: u8 = 2; + try testing.expectEqualStrings("2", try renderToBuf(&buf, @as(*const bool, @ptrCast(&raw_b)))); + const U = union(enum(u8)) { x: u32, y: bool }; + var raw_u: [@sizeOf(U)]u8 align(@alignOf(U)) = @splat(0x55); + const out = try renderToBuf(&buf, @as(*const U, @ptrCast(&raw_u))); + try testing.expectEqualStrings("(invalid tag 85)", out); + const S = struct { e: E, u: U, b: bool }; + var raw_s: [@sizeOf(S)]u8 align(@alignOf(S)) = @splat(0xEE); + const ps: *const S = @ptrCast(&raw_s); + _ = try renderToBuf(&buf, ps); // no trap + try testing.expect(std.mem.indexOf(u8, try renderToBuf(&buf, ps), "238") != null); +} + +test "a [*:0]const u8 without a terminator is read at most max_string + 1 bytes" { + // Only the first max_string + 1 bytes exist; anything beyond is the + // testing allocator's guard, which a wider scan would touch. + const mem = try testing.allocator.alloc(u8, max_string + 1); + defer testing.allocator.free(mem); + @memset(mem, 'x'); + const z: [*:0]const u8 = @ptrCast(mem.ptr); + var buf: [1024]u8 = undefined; + const out = try renderToBuf(&buf, &z); + try testing.expectEqual(@as(usize, 1 + max_string + 1 + "…".len), out.len); + try testing.expect(std.mem.endsWith(u8, out, "\"…")); + // exactly max_string bytes then NUL: no ellipsis + const mem2 = try testing.allocator.alloc(u8, max_string + 1); + defer testing.allocator.free(mem2); + @memset(mem2, 'y'); + mem2[max_string] = 0; + const z2: [*:0]const u8 = @ptrCast(mem2.ptr); + const out2 = try renderToBuf(&buf, &z2); + try testing.expectEqual(@as(usize, 1 + max_string + 1), out2.len); + // a garbage-length []const u8 still reads at most max_string bytes + const garbage: []const u8 = mem[0..max_string]; + _ = try renderToBuf(&buf, &garbage); +} + +test "set rejects hostile input without partial writes" { + var u: u8 = 200; + for ([_][]const u8{ "-1", "256", "1e3", "0x", "", " ", "1.5", "+", "0b2", "١", "12abc", "0x100", "\x00", "1 2" }) |bad| { + try testing.expectError(error.Invalid, set(&u, bad)); + try testing.expectEqual(@as(u8, 200), u); + } + try set(&u, "0b1111_1111"); + try testing.expectEqual(@as(u8, 255), u); + try set(&u, "+0o17"); + try testing.expectEqual(@as(u8, 15), u); + var i: i64 = 1; + try set(&i, "-9223372036854775808"); + try testing.expectEqual(std.math.minInt(i64), i); + try testing.expectError(error.Invalid, set(&i, "9223372036854775808")); + var w: u128 = 0; + try set(&w, "340282366920938463463374607431768211455"); + try testing.expectEqual(std.math.maxInt(u128), w); + try testing.expectError(error.Invalid, set(&w, "340282366920938463463374607431768211456")); + // floats: exponents, hex floats, inf/nan spellings, and junk + var f: f32 = 1; + try set(&f, "1.5e3"); + try testing.expectEqual(@as(f32, 1500), f); + try set(&f, "-0x1p-2"); + try testing.expectEqual(@as(f32, -0.25), f); + try set(&f, "1e999"); + try testing.expect(std.math.isInf(f)); + try testing.expectError(error.Invalid, set(&f, "1.5.5")); + try testing.expectError(error.Invalid, set(&f, "e5")); + try testing.expectError(error.Invalid, set(&f, "")); + var h: f16 = 0; + try set(&h, "65504"); + try testing.expectEqual(@as(f16, 65504), h); + var q: f128 = 0; + try set(&q, "2.5"); + try testing.expectEqual(@as(f128, 2.5), q); + // enums: NULs inside the tag, case, trailing junk; non-exhaustive by integer only when out of names + const E = enum(u8) { off, on }; + var e: E = .off; + for ([_][]const u8{ "on\x00x", "On", "on x", "1", "0x1", "" }) |bad| { + try testing.expectError(error.Invalid, set(&e, bad)); + try testing.expectEqual(E.off, e); + } + try set(&e, "\x00on\n"); + try testing.expectEqual(E.on, e); + const NE = enum(u8) { a, _ }; + var ne: NE = .a; + try set(&ne, "200"); + try testing.expectEqual(@as(u8, 200), @intFromEnum(ne)); + try testing.expectError(error.Invalid, set(&ne, "256")); + try testing.expectError(error.Invalid, set(&ne, "-1")); + try set(&ne, "a"); + try testing.expectEqual(NE.a, ne); + // bools + var b: bool = true; + for ([_][]const u8{ "yes", "TRUE", "2", "", "01" }) |bad| { + try testing.expectError(error.Invalid, set(&b, bad)); + try testing.expect(b); + } + // unsupported types are refused without touching memory + var opt: ?u8 = 3; + try testing.expectError(error.Unsupported, set(&opt, "4")); + try testing.expectEqual(@as(?u8, 3), opt); + var arr: [2]u8 = .{ 1, 2 }; + try testing.expectError(error.Unsupported, set(&arr, "x")); + var un: union(enum) { a: u8 } = .{ .a = 1 }; + try testing.expectError(error.Unsupported, set(&un, "a")); +} |
