302 lines
11 KiB
Zig
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;
|
|
}
|