// flibc execvp — bare-name resolver over sys.exec_path. // // Linux's execvp consults $PATH; FlashOS has no environment yet, so // a single search prefix is hard-wired: a bare name `foo` resolves to // `/bin/foo`. A name that already contains a slash (absolute // `/usr/bin/foo` or relative `tools/foo`) skips the prefix and is // handed to `sys.exec_path` verbatim — the kernel joins relative paths // against the task's `cwd` (slot 36). // // Layout mirrors readline.zig: the pure `resolve(name, out)` path-build // lives at the top with host tests at the bottom of the file; the // SVC-driving `execvp(name, argv)` driver sits behind an // `if (has_driver)` struct-select so the host build never analyses the // inline asm in syscalls.zig. The host fallback returns -1; only the // aarch64-freestanding target sees the real `sys.exec_path` call. // // Path budget: PATH_MAX = 256 matches sys_chdir's CWD_SIZE and is the // stack buffer fsh hands in. The resolver returns null on overflow // (rule 1 — no realloc) so callers surface a clean -1 instead of // truncating into a wrong binary. use builtin // Driver compiles only on aarch64-freestanding. Same gating idiom as // readline.zig — the host-test build picks the empty branch so the // SVC trampolines never reach semantic analysis. const has_driver = builtin.cpu.arch == .aarch64 && builtin.target.os.tag == .freestanding /// Maximum resolved path length the driver hands to sys.exec_path. Sized /// to match the kernel's `cwd` budget (CWD_SIZE = 256) — the kernel can /// already handle paths up to that ceiling, so widening here would only /// invite a later kernel-side rejection. pub const PATH_MAX usize = 256 const BIN_PREFIX = "/bin/" /// Resolve a program name into an absolute (or already-slashed) path /// laid out in `out`. Returns a sentinel-terminated slice into `out` /// suitable for `sys.exec_path`. Rules: /// * empty `name` → null /// * `name` contains '/' → copy verbatim + NUL into `out` (lets /// the kernel handle absolute / relative /// resolution against `cwd`) /// * bare `name` → `/bin/` + name + NUL /// * `out` too small for → null (caller gets -1 rather than a /// prefix + name + NUL silently truncated binary path) /// /// Pure: no syscalls, no allocator. Exercised in isolation by the host /// suite — see the `test` blocks at the bottom of this file. pub fn resolve(name []u8, out []mut u8) ?[:0]mut u8 { if name.len == 0 { return null } var has_slash = false for c in name { if c == '/' { has_slash = true break } } if has_slash { if name.len + 1 > out.len { return null } #memcpy(out[0..name.len], name) out[name.len] = 0 return out[0..name.len :0] } total := BIN_PREFIX.len + name.len if total + 1 > out.len { return null } #memcpy(out[0..BIN_PREFIX.len], BIN_PREFIX) #memcpy(out[BIN_PREFIX.len..][0..name.len], name) out[total] = 0 return out[0..total :0] } /// Resolve `name` (bare → `/bin/`; slashed → verbatim) and exec /// the result. Returns -1 on resolve failure (empty / oversize) or /// whatever `sys.exec_path` returns; on success the syscall does not /// return. pub const execvp = driver.execvp const driver = if (has_driver) struct { use "syscalls" as sys pub fn execvp(name cstr, argv argv) i32 { var path_buf [PATH_MAX]u8 = undefined var n usize = 0 while name[n] != 0 { n += 1 } resolved := resolve(name[0..n], &path_buf) orelse return -1 return sys.exec_path(#ptrCast(resolved.ptr), argv) } } else struct { // Host-test stub: never invoked from tests, present only so the // `pub const execvp = driver.execvp` binding type-checks on host. pub fn execvp(_ cstr, _ argv) i32 { return -1 } } // ---- Host tests ---- const std = #import("std") const testing = std.testing test "resolve: bare name maps to /bin/" { var buf [64]u8 = undefined const r = resolve("fsh", &buf) orelse return error.UnexpectedNull try testing.expectEqualStrings("/bin/fsh", r) try testing.expectEqual(#as(u8, 0), buf[r.len]) } test "resolve: single-char bare name" { var buf [16]u8 = undefined const r = resolve("x", &buf) orelse return error.UnexpectedNull try testing.expectEqualStrings("/bin/x", r) } test "resolve: absolute path passes through verbatim" { var buf [64]u8 = undefined const r = resolve("/usr/local/bin/foo", &buf) orelse return error.UnexpectedNull try testing.expectEqualStrings("/usr/local/bin/foo", r) try testing.expectEqual(#as(u8, 0), buf[r.len]) } test "resolve: relative path with slash passes through" { var buf [64]u8 = undefined const r = resolve("tools/foo", &buf) orelse return error.UnexpectedNull try testing.expectEqualStrings("tools/foo", r) } test "resolve: leading '/' bypasses prefix even with no further slash" { var buf [32]u8 = undefined const r = resolve("/foo", &buf) orelse return error.UnexpectedNull try testing.expectEqualStrings("/foo", r) } test "resolve: empty name returns null" { var buf [64]u8 = undefined try testing.expectEqual(#as(?[:0]mut u8, null), resolve("", &buf)) } test "resolve: oversize bare name returns null" { var tiny [4]u8 = undefined // /bin/x = 6 chars + NUL = 7 > 4 try testing.expectEqual(#as(?[:0]mut u8, null), resolve("foo", &tiny)) } test "resolve: oversize passthrough returns null" { var tiny [4]u8 = undefined try testing.expectEqual(#as(?[:0]mut u8, null), resolve("/abcd", &tiny)) } test "resolve: exact-fit bare name succeeds" { // "/bin/x" = 6 bytes, NUL = 7 → buffer of 7 fits var buf [7]u8 = undefined const r = resolve("x", &buf) orelse return error.UnexpectedNull try testing.expectEqualStrings("/bin/x", r) try testing.expectEqual(#as(u8, 0), buf[6]) } test "resolve: one-byte-short bare name returns null" { var buf [6]u8 = undefined // /bin/x needs 7 with NUL try testing.expectEqual(#as(?[:0]mut u8, null), resolve("x", &buf)) } test "resolve: exact-fit passthrough succeeds" { var buf [5]u8 = undefined // "/foo" = 4 + NUL = 5 const r = resolve("/foo", &buf) orelse return error.UnexpectedNull try testing.expectEqualStrings("/foo", r) try testing.expectEqual(#as(u8, 0), buf[4]) } test "resolve: bare name in PATH_MAX-sized buffer" { var buf [PATH_MAX]u8 = undefined const r = resolve("cat", &buf) orelse return error.UnexpectedNull try testing.expectEqualStrings("/bin/cat", r) } test "resolve: slash mid-name treated as path" { var buf [32]u8 = undefined const r = resolve("a/b", &buf) orelse return error.UnexpectedNull try testing.expectEqualStrings("a/b", r) }