|
| 1 | +// SPDX-License-Identifier: PMPL-1.0-or-later |
| 2 | +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 3 | +// |
| 4 | +// cartridge_shim.zig — Shared helpers for the ADR-0006 five-symbol |
| 5 | +// cartridge ABI (`boj_cartridge_init / deinit / name / version / invoke`). |
| 6 | +// |
| 7 | +// The shim centralises the seven-code return convention, NUL-argument |
| 8 | +// guards, tool-name comparison, and the buffer-too-small path so each |
| 9 | +// cartridge's `boj_cartridge_invoke` can stay short — typically a tool |
| 10 | +// table plus `shim.writeResult(...)`. |
| 11 | +// |
| 12 | +// Cartridges import this file by relative path (no build-graph change |
| 13 | +// needed). Example: |
| 14 | +// |
| 15 | +// const shim = @import("../../../ffi/zig/src/cartridge_shim.zig"); |
| 16 | +// |
| 17 | +// export fn boj_cartridge_invoke( |
| 18 | +// tool_name: [*c]const u8, |
| 19 | +// json_args: [*c]const u8, |
| 20 | +// out_buf: [*c]u8, |
| 21 | +// in_out_len: [*c]usize, |
| 22 | +// ) callconv(.c) i32 { |
| 23 | +// _ = json_args; |
| 24 | +// if (shim.invokeArgsNull(tool_name, out_buf, in_out_len)) return shim.RC_BAD_ARGS; |
| 25 | +// const body = if (shim.toolIs(tool_name, "foo")) "{\"result\":{}}" |
| 26 | +// else return shim.RC_UNKNOWN_TOOL; |
| 27 | +// return shim.writeResult(out_buf, in_out_len, body); |
| 28 | +// } |
| 29 | + |
| 30 | +const std = @import("std"); |
| 31 | + |
| 32 | +// ── Return codes (ADR-0006 §Return codes) ──────────────────────────── |
| 33 | +// |
| 34 | +// Frozen by ADR-0006. New failure modes compose these via the error |
| 35 | +// JSON body — the integer surface does not grow without a follow-up ADR. |
| 36 | + |
| 37 | +pub const RC_SUCCESS: i32 = 0; |
| 38 | +pub const RC_UNKNOWN_TOOL: i32 = -1; |
| 39 | +pub const RC_BAD_ARGS: i32 = -2; |
| 40 | +pub const RC_BUFFER_TOO_SMALL: i32 = -3; |
| 41 | +pub const RC_RUNTIME_ERROR: i32 = -4; |
| 42 | +pub const RC_PANIC: i32 = -5; |
| 43 | +pub const RC_AUTH_DENIED: i32 = -6; |
| 44 | + |
| 45 | +// ── Invoke-path helpers ────────────────────────────────────────────── |
| 46 | + |
| 47 | +/// True if any of the three mandatory `boj_cartridge_invoke` output-path |
| 48 | +/// pointers is null. Use at the top of every invoke to short-circuit to |
| 49 | +/// `RC_BAD_ARGS`. |
| 50 | +pub fn invokeArgsNull( |
| 51 | + tool_name: [*c]const u8, |
| 52 | + out_buf: [*c]u8, |
| 53 | + in_out_len: [*c]usize, |
| 54 | +) bool { |
| 55 | + return tool_name == null or out_buf == null or in_out_len == null; |
| 56 | +} |
| 57 | + |
| 58 | +/// Compare a C-NUL-terminated tool-name pointer against a Zig string |
| 59 | +/// literal. Caller must have already verified `tool_name` is non-null |
| 60 | +/// (usually via `invokeArgsNull`). |
| 61 | +pub fn toolIs(tool_name: [*c]const u8, expected: []const u8) bool { |
| 62 | + const s = std.mem.span(@as([*:0]const u8, @ptrCast(tool_name))); |
| 63 | + return std.mem.eql(u8, s, expected); |
| 64 | +} |
| 65 | + |
| 66 | +/// Copy `body` into `out_buf[0..*in_out_len]` (as a capacity) and update |
| 67 | +/// `*in_out_len` to the number of bytes written. Returns `RC_SUCCESS`. |
| 68 | +/// |
| 69 | +/// If `body.len` exceeds the current capacity stored in `*in_out_len`, |
| 70 | +/// sets `*in_out_len` to the required size and returns |
| 71 | +/// `RC_BUFFER_TOO_SMALL` — the caller is then expected to re-allocate |
| 72 | +/// and retry, per ADR-0006 §Memory ownership. |
| 73 | +/// |
| 74 | +/// Caller must have already verified that `out_buf` and `in_out_len` |
| 75 | +/// are non-null. |
| 76 | +pub fn writeResult( |
| 77 | + out_buf: [*c]u8, |
| 78 | + in_out_len: [*c]usize, |
| 79 | + body: []const u8, |
| 80 | +) i32 { |
| 81 | + const cap = in_out_len.*; |
| 82 | + if (body.len > cap) { |
| 83 | + in_out_len.* = body.len; |
| 84 | + return RC_BUFFER_TOO_SMALL; |
| 85 | + } |
| 86 | + @memcpy(out_buf[0..body.len], body); |
| 87 | + in_out_len.* = body.len; |
| 88 | + return RC_SUCCESS; |
| 89 | +} |
| 90 | + |
| 91 | +// ── Tests ──────────────────────────────────────────────────────────── |
| 92 | + |
| 93 | +test "writeResult: body fits, writes and sets length" { |
| 94 | + var buf: [64]u8 = undefined; |
| 95 | + var len: usize = buf.len; |
| 96 | + const rc = writeResult(&buf, &len, "hello"); |
| 97 | + try std.testing.expectEqual(RC_SUCCESS, rc); |
| 98 | + try std.testing.expectEqual(@as(usize, 5), len); |
| 99 | + try std.testing.expectEqualStrings("hello", buf[0..len]); |
| 100 | +} |
| 101 | + |
| 102 | +test "writeResult: too small returns -3 and sets required length" { |
| 103 | + var buf: [2]u8 = undefined; |
| 104 | + var len: usize = buf.len; |
| 105 | + const rc = writeResult(&buf, &len, "hello"); |
| 106 | + try std.testing.expectEqual(RC_BUFFER_TOO_SMALL, rc); |
| 107 | + try std.testing.expectEqual(@as(usize, 5), len); |
| 108 | +} |
| 109 | + |
| 110 | +test "writeResult: exact-fit succeeds" { |
| 111 | + var buf: [5]u8 = undefined; |
| 112 | + var len: usize = buf.len; |
| 113 | + const rc = writeResult(&buf, &len, "hello"); |
| 114 | + try std.testing.expectEqual(RC_SUCCESS, rc); |
| 115 | + try std.testing.expectEqual(@as(usize, 5), len); |
| 116 | +} |
| 117 | + |
| 118 | +test "writeResult: empty body" { |
| 119 | + var buf: [4]u8 = undefined; |
| 120 | + var len: usize = buf.len; |
| 121 | + const rc = writeResult(&buf, &len, ""); |
| 122 | + try std.testing.expectEqual(RC_SUCCESS, rc); |
| 123 | + try std.testing.expectEqual(@as(usize, 0), len); |
| 124 | +} |
| 125 | + |
| 126 | +test "toolIs: matches and rejects" { |
| 127 | + const name: [*:0]const u8 = "foo"; |
| 128 | + try std.testing.expect(toolIs(@ptrCast(name), "foo")); |
| 129 | + try std.testing.expect(!toolIs(@ptrCast(name), "bar")); |
| 130 | + try std.testing.expect(!toolIs(@ptrCast(name), "foobar")); |
| 131 | + try std.testing.expect(!toolIs(@ptrCast(name), "fo")); |
| 132 | +} |
| 133 | + |
| 134 | +test "invokeArgsNull: detects each null slot" { |
| 135 | + var buf: [4]u8 = undefined; |
| 136 | + var len: usize = 4; |
| 137 | + const name: [*:0]const u8 = "x"; |
| 138 | + try std.testing.expect(!invokeArgsNull(@ptrCast(name), &buf, &len)); |
| 139 | + try std.testing.expect(invokeArgsNull(null, &buf, &len)); |
| 140 | + try std.testing.expect(invokeArgsNull(@ptrCast(name), null, &len)); |
| 141 | + try std.testing.expect(invokeArgsNull(@ptrCast(name), &buf, null)); |
| 142 | +} |
0 commit comments