Flash 191 lines
// 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/<name>`; 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/<name>" {
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)
}