a73x

1b10f264

test: guard client core wasm compilation

a73x   2026-08-16 14:41

Commit message
test: guard client core wasm compilation

build.zig
Old New
@@ -393,13 +393,12 @@ fn shellGate(b: *std.Build, step: *std.Build.Step) void {
393 /// escape pins. mux and exe are executable roots but carry the argument 393 /// escape pins. mux and exe are executable roots but carry the argument
394 /// parsers — a test that is never built is not a test (decisions.md). 394 /// parsers — a test that is never built is not a test (decisions.md).
395 const test_order = [_][]const u8{ 395 const test_order = [_][]const u8{
396 "script", "protocol", "client_core", "engine", "pty", "delta", 396 "script", "protocol", "client_core", "engine", "pty", "delta",
397 "cmd", "shellint", "replica", "keymap", "webhub", 397 "cmd", "shellint", "replica", "keymap", "webhub", "sockpath",
398 "sockpath", "muxa", "server", "client", "proxy", 398 "muxa", "server", "client", "proxy", "mux", "quic",
399 "mux", "quic", "quic_server", "exe", "testtmp", 399 "quic_server", "exe", "testtmp", "quic_client", "predict", "rawmode",
400 "quic_client", "predict", "rawmode", "delaypipe", "xdg", 400 "delaypipe", "xdg", "spawn", "handoff", "paint", "render",
401 "spawn", "handoff", "paint", "render", "ptyclient", 401 "ptyclient", "webhub_main", "wsclient",
402 "webhub_main", "wsclient",
403 }; 402 };
404 403
405 comptime { 404 comptime {
@@ -589,6 +588,7 @@ pub fn build(b: *std.Build) void {
589 } 588 }
590 } 589 }
591 const engine_wasm_mod = wasm_mods[comptime idxOf("engine")].?; 590 const engine_wasm_mod = wasm_mods[comptime idxOf("engine")].?;
591 const client_core_wasm_mod = wasm_mods[comptime idxOf("client_core")].?;
592 if (ghostty_wasm_dep) |dep| { 592 if (ghostty_wasm_dep) |dep| {
593 engine_wasm_mod.addImport("ghostty-vt", dep.module("ghostty-vt")); 593 engine_wasm_mod.addImport("ghostty-vt", dep.module("ghostty-vt"));
594 } 594 }
@@ -598,6 +598,14 @@ pub fn build(b: *std.Build) void {
598 wasm_core_mod.addImport("client_core", wasm_mods[comptime idxOf("client_core")].?); 598 wasm_core_mod.addImport("client_core", wasm_mods[comptime idxOf("client_core")].?);
599 wasm_core_mod.addImport("replica", wasm_mods[comptime idxOf("replica")].?); 599 wasm_core_mod.addImport("replica", wasm_mods[comptime idxOf("replica")].?);
600 wasm_core_mod.addImport("keymap", wasm_mods[comptime idxOf("keymap")].?); 600 wasm_core_mod.addImport("keymap", wasm_mods[comptime idxOf("keymap")].?);
601 // Compile the semantic decoder as its own wasm object as well as making
602 // it available to wasm_core. The object is a build-only cleanliness
603 // guard: it forces this module's source and protocol import to undergo
604 // wasm semantic analysis without adding runtime exports or behavior.
605 const client_core_wasm_guard = b.addObject(.{
606 .name = "client_core_wasm_guard",
607 .root_module = client_core_wasm_mod,
608 });
601 const wasm_exe = b.addExecutable(.{ .name = "mux_core", .root_module = wasm_core_mod }); 609 const wasm_exe = b.addExecutable(.{ .name = "mux_core", .root_module = wasm_core_mod });
602 // A wasm reactor, not a command: no _start, and the exports must 610 // A wasm reactor, not a command: no _start, and the exports must
603 // survive the linker's dead-strip. Deliberately NOT use_llvm/use_lld — 611 // survive the linker's dead-strip. Deliberately NOT use_llvm/use_lld —
@@ -626,6 +634,7 @@ pub fn build(b: *std.Build) void {
626 b.installArtifact(webhub_exe); 634 b.installArtifact(webhub_exe);
627 635
628 const test_step = b.step("test", "Run unit tests"); 636 const test_step = b.step("test", "Run unit tests");
637 test_step.dependOn(&client_core_wasm_guard.step);
629 for (test_order) |name| { 638 for (test_order) |name| {
630 const i = idx.of(name); 639 const i = idx.of(name);
631 const t = b.addTest(.{ .root_module = test_mods[i] }); 640 const t = b.addTest(.{ .root_module = test_mods[i] });
src/client_core.zig
Old New
@@ -62,6 +62,8 @@ fn receiveEvent(payload: []const u8) Result {
62 fn validClipboard(target: u8, base64: []const u8) bool { 62 fn validClipboard(target: u8, base64: []const u8) bool {
63 if (!validTarget(target) or base64.len == 0 or base64.len > proto.clipboard_base64_max) 63 if (!validTarget(target) or base64.len == 0 or base64.len > proto.clipboard_base64_max)
64 return false; 64 return false;
65 // Enforce only the injection-safe alphabet; canonical base64 length and
66 // padding rules stay terminal-compatible and are intentionally not added.
65 for (base64) |byte| { 67 for (base64) |byte| {
66 if (!((byte >= 'A' and byte <= 'Z') or 68 if (!((byte >= 'A' and byte <= 'Z') or
67 (byte >= 'a' and byte <= 'z') or 69 (byte >= 'a' and byte <= 'z') or
@@ -156,9 +158,9 @@ test "client core refuses invalid clipboard targets" {
156 } 158 }
157 } 159 }
158 160
159 test "client core refuses invalid clipboard alphabet including BEL" { 161 test "client core refuses invalid clipboard alphabet" {
160 var core = ClientCore{}; 162 var core = ClientCore{};
161 const payload = [_]u8{ 0, 'c', 'A', 'G', 'k', '!', 0x07 }; 163 const payload = [_]u8{ 0, 'c', 'A', 'G', 'k', '!' };
162 try expectIgnored(core.receive(.term_event, &payload)); 164 try expectIgnored(core.receive(.term_event, &payload));
163 } 165 }
164 166
@@ -168,6 +170,19 @@ test "client core refuses BEL as the only invalid clipboard alphabet byte" {
168 try expectIgnored(core.receive(.term_event, &payload)); 170 try expectIgnored(core.receive(.term_event, &payload));
169 } 171 }
170 172
173 test "client core accepts digits plus slash and plus in clipboard alphabet" {
174 var core = ClientCore{};
175 const payload = [_]u8{ 0, 'c', 'A', 'B', '0', '1', '2', '+', '/', '=' };
176
177 switch (core.receive(.term_event, &payload)) {
178 .effect => |effect| switch (effect) {
179 .clipboard_set => |set| try std.testing.expectEqualStrings("AB012+/=", set.base64),
180 else => return error.ExpectedClipboard,
181 },
182 else => return error.ExpectedClipboard,
183 }
184 }
185
171 test "client core refuses empty clipboard" { 186 test "client core refuses empty clipboard" {
172 var core = ClientCore{}; 187 var core = ClientCore{};
173 const payload = [_]u8{ 0, 'c' }; 188 const payload = [_]u8{ 0, 'c' };