1b10f264
test: guard client core wasm compilation
a73x 2026-08-16 14:41
Commit message
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' }; |