Files
2026-07-13 12:29:49 +08:00

302 lines
11 KiB
Zig

//! Decoder over the app-core Cmd/Sub wire format (rt.zig, cmd_format_version
//! 2), shared by the ts-track behavioral harnesses. The graders copy this
//! file next to each case's harness so assertions read decoded ops — "a
//! fetch with key `feed` targeting this URL", "the delay re-armed" — instead
//! of hand-built byte strings, which keeps harnesses lenient about the parts
//! a case does not pin (timeouts, header order, batching).
//!
//! A Cmd value is a flat concatenation of op records; iterate them with
//! `CmdIter`. Sub values share the encoding (`SubIter`).
const std = @import("std");
pub const Op = union(enum) {
persist,
now: struct { msg_tag: u8 },
host: Host,
host_bytes: struct { name: []const u8, payload: []const u8 },
request: struct { name: []const u8, key: []const u8, ok_tag: u8, err_tag: u8, payload: []const u8 },
cancel: struct { key: []const u8 },
read_file: struct { key: []const u8, ok_tag: u8, err_tag: u8, path: []const u8 },
write_file: struct { key: []const u8, ok_tag: u8, err_tag: u8, path: []const u8, bytes: []const u8 },
fetch: Fetch,
clip_write: struct { bytes: []const u8 },
clip_read: struct { key: []const u8, ok_tag: u8, err_tag: u8 },
delay: struct { key: []const u8, after_ms: f64, msg_tag: u8 },
spawn: Spawn,
audio_play: struct { key: []const u8, event_tag: u8, path: []const u8, url: []const u8, cache_path: []const u8, expected_bytes: f64 },
audio_ctl: struct { key: []const u8, verb: u8, value: f64 },
pub const Host = struct {
name: []const u8,
/// f64 args, little-endian, 8 bytes each.
arg_bytes: []const u8,
pub fn argCount(self: Host) usize {
return self.arg_bytes.len / 8;
}
pub fn arg(self: Host, index: usize) f64 {
return @bitCast(std.mem.readInt(u64, self.arg_bytes[index * 8 ..][0..8], .little));
}
};
pub const Fetch = struct {
key: []const u8,
ok_tag: u8,
err_tag: u8,
/// rt.CmdFetchMethod declaration order: GET 0, POST 1, PUT 2,
/// DELETE 3, PATCH 4, HEAD 5.
method: u8,
timeout_ms: u32,
url: []const u8,
header_count: u8,
/// Raw header block: per header [name_len u8][name][value_len u32 LE][value].
header_bytes: []const u8,
body: []const u8,
};
pub const Spawn = struct {
key: []const u8,
/// 0xFF = no line routing (rt.spawn_no_line_tag).
line_tag: u8,
exit_tag: u8,
err_tag: u8,
/// rt.CmdSpawnMode: lines 0, collect 1.
mode: u8,
arg_count: u8,
/// Raw argv block: per element [len u32 LE][bytes].
argv_bytes: []const u8,
stdin: []const u8,
pub fn arg(self: Spawn, index: usize) []const u8 {
var off: usize = 0;
var i: usize = 0;
while (true) {
const len = std.mem.readInt(u32, self.argv_bytes[off..][0..4], .little);
if (i == index) return self.argv_bytes[off + 4 ..][0..len];
off += 4 + len;
i += 1;
}
}
};
};
pub const CmdIter = struct {
bytes: []const u8,
off: usize = 0,
pub fn init(bytes: []const u8) CmdIter {
return .{ .bytes = bytes };
}
pub fn next(self: *CmdIter) ?Op {
if (self.off >= self.bytes.len) return null;
const b = self.bytes;
var off = self.off;
const op = b[off];
off += 1;
const decoded: Op = switch (op) {
0x01 => .persist,
0x02 => blk: {
const tag = b[off];
off += 1;
break :blk .{ .now = .{ .msg_tag = tag } };
},
0x03 => blk: {
const name = shortBytes(b, &off);
const argc = b[off];
off += 1;
const args = b[off..][0 .. @as(usize, argc) * 8];
off += args.len;
break :blk .{ .host = .{ .name = name, .arg_bytes = args } };
},
0x04 => blk: {
const name = shortBytes(b, &off);
const payload = longBytes(b, &off);
break :blk .{ .host_bytes = .{ .name = name, .payload = payload } };
},
0x05 => blk: {
const name = shortBytes(b, &off);
const key = shortBytes(b, &off);
const ok = b[off];
const err = b[off + 1];
off += 2;
const payload = longBytes(b, &off);
break :blk .{ .request = .{ .name = name, .key = key, .ok_tag = ok, .err_tag = err, .payload = payload } };
},
0x06 => blk: {
const key = shortBytes(b, &off);
break :blk .{ .cancel = .{ .key = key } };
},
0x07 => blk: {
const head = routedHead(b, &off);
const path = longBytes(b, &off);
break :blk .{ .read_file = .{ .key = head.key, .ok_tag = head.ok, .err_tag = head.err, .path = path } };
},
0x08 => blk: {
const head = routedHead(b, &off);
const path = longBytes(b, &off);
const bytes = longBytes(b, &off);
break :blk .{ .write_file = .{ .key = head.key, .ok_tag = head.ok, .err_tag = head.err, .path = path, .bytes = bytes } };
},
0x09 => blk: {
const head = routedHead(b, &off);
const method = b[off];
off += 1;
const timeout = std.mem.readInt(u32, b[off..][0..4], .little);
off += 4;
const url = longBytes(b, &off);
const header_count = b[off];
off += 1;
const headers_start = off;
var h: usize = 0;
while (h < header_count) : (h += 1) {
_ = shortBytes(b, &off);
_ = longBytes(b, &off);
}
const header_bytes = b[headers_start..off];
const body = longBytes(b, &off);
break :blk .{ .fetch = .{ .key = head.key, .ok_tag = head.ok, .err_tag = head.err, .method = method, .timeout_ms = timeout, .url = url, .header_count = header_count, .header_bytes = header_bytes, .body = body } };
},
0x0A => blk: {
const bytes = longBytes(b, &off);
break :blk .{ .clip_write = .{ .bytes = bytes } };
},
0x0B => blk: {
const head = routedHead(b, &off);
break :blk .{ .clip_read = .{ .key = head.key, .ok_tag = head.ok, .err_tag = head.err } };
},
0x0C => blk: {
const key = shortBytes(b, &off);
const after: f64 = @bitCast(std.mem.readInt(u64, b[off..][0..8], .little));
off += 8;
const tag = b[off];
off += 1;
break :blk .{ .delay = .{ .key = key, .after_ms = after, .msg_tag = tag } };
},
0x0D => blk: {
const key = shortBytes(b, &off);
const line_tag = b[off];
const exit_tag = b[off + 1];
const err_tag = b[off + 2];
const mode = b[off + 3];
const argc = b[off + 4];
off += 5;
const argv_start = off;
var i: usize = 0;
while (i < argc) : (i += 1) _ = longBytes(b, &off);
const argv_bytes = b[argv_start..off];
const stdin = longBytes(b, &off);
break :blk .{ .spawn = .{ .key = key, .line_tag = line_tag, .exit_tag = exit_tag, .err_tag = err_tag, .mode = mode, .arg_count = argc, .argv_bytes = argv_bytes, .stdin = stdin } };
},
0x0E => blk: {
const key = shortBytes(b, &off);
const event_tag = b[off];
off += 1;
const path = longBytes(b, &off);
const url = longBytes(b, &off);
const cache = longBytes(b, &off);
const expected: f64 = @bitCast(std.mem.readInt(u64, b[off..][0..8], .little));
off += 8;
break :blk .{ .audio_play = .{ .key = key, .event_tag = event_tag, .path = path, .url = url, .cache_path = cache, .expected_bytes = expected } };
},
0x0F => blk: {
const key = shortBytes(b, &off);
const verb = b[off];
off += 1;
const value: f64 = @bitCast(std.mem.readInt(u64, b[off..][0..8], .little));
off += 8;
break :blk .{ .audio_ctl = .{ .key = key, .verb = verb, .value = value } };
},
else => std.debug.panic("cmdview: unknown op byte 0x{X:0>2} at offset {d}", .{ op, self.off }),
};
self.off = off;
return decoded;
}
};
/// Sub records share the framing; today's only op is the repeating timer.
pub const SubOp = union(enum) {
timer: struct { key: []const u8, every_ms: f64, msg_tag: u8 },
};
pub const SubIter = struct {
bytes: []const u8,
off: usize = 0,
pub fn init(bytes: []const u8) SubIter {
return .{ .bytes = bytes };
}
pub fn next(self: *SubIter) ?SubOp {
if (self.off >= self.bytes.len) return null;
const b = self.bytes;
var off = self.off;
const op = b[off];
off += 1;
if (op != 0x01) std.debug.panic("cmdview: unknown sub op byte 0x{X:0>2}", .{op});
const key = shortBytes(b, &off);
const every: f64 = @bitCast(std.mem.readInt(u64, b[off..][0..8], .little));
off += 8;
const tag = b[off];
off += 1;
self.off = off;
return .{ .timer = .{ .key = key, .every_ms = every, .msg_tag = tag } };
}
};
const RoutedHead = struct { key: []const u8, ok: u8, err: u8 };
fn routedHead(b: []const u8, off: *usize) RoutedHead {
const key = shortBytes(b, off);
const ok = b[off.*];
const err = b[off.* + 1];
off.* += 2;
return .{ .key = key, .ok = ok, .err = err };
}
fn shortBytes(b: []const u8, off: *usize) []const u8 {
const len = b[off.*];
const out = b[off.* + 1 ..][0..len];
off.* += 1 + len;
return out;
}
fn longBytes(b: []const u8, off: *usize) []const u8 {
const len = std.mem.readInt(u32, b[off.*..][0..4], .little);
const out = b[off.* + 4 ..][0..len];
off.* += 4 + len;
return out;
}
// ------------------------------------------------------------------ helpers
/// First decoded op of the given kind in a cmd buffer, or null.
pub fn findOp(bytes: []const u8, comptime kind: std.meta.Tag(Op)) ?@FieldType(Op, @tagName(kind)) {
var iter = CmdIter.init(bytes);
while (iter.next()) |op| {
if (op == kind) return @field(op, @tagName(kind));
}
return null;
}
/// Count of decoded ops of the given kind in a cmd buffer.
pub fn countOps(bytes: []const u8, comptime kind: std.meta.Tag(Op)) usize {
var iter = CmdIter.init(bytes);
var n: usize = 0;
while (iter.next()) |op| {
if (op == kind) n += 1;
}
return n;
}
/// First timer descriptor in a Sub buffer, or null.
pub fn findTimer(bytes: []const u8) ?@FieldType(SubOp, "timer") {
var iter = SubIter.init(bytes);
while (iter.next()) |op| {
return op.timer;
}
return null;
}