//! 9ns: mount a 9P2000 tree into a fresh user+mount namespace via FUSE //! and run a program inside it. //! //! Exit codes: the child's status (128+sig if signalled); 125 for 9ns's //! own failures (usage, connect, attach, namespace/mount); 126/127 for exec //! failures. const std = @import("std"); const linux = std.os.linux; const cloud9 = @import("cloud9"); const ns = @import("ns.zig"); const nine = @import("nine.zig"); const bridge = @import("bridge.zig"); const version_string = "9ns 0.1.0"; const usage_text = \\Usage: 9ns [options] -- PROGRAM [ARGS...] \\Transport (exactly one): \\ --unix PATH Unix stream socket \\ --tcp IP:PORT TCP (IPv4/IPv6 literal) \\ --fd N already-connected inherited descriptor \\ --spawn CMD run CMD (via /bin/sh -c) with a socketpair on its stdin/stdout \\ --mntgen mount the posted-9P registry ($XDG_RUNTIME_DIR/9p): one \\ mount whose root lists the posted names; walking into a \\ name dials that server (mutually exclusive with the rest) \\Options: \\ --name NAME mount name: the tree appears at /mnt/9p/NAME (one path \\ component; default derived from the transport, see below; \\ not with --mntgen) \\ --mount PATH mountpoint inside the new namespace (overrides --name; \\ with --mntgen the mount is the registry view itself, \\ default /mnt/9p) \\ --uname NAME 9P user name (default $USER, else "none") \\ --aname NAME 9P tree to attach (default "") \\ --msize BYTES maximum 9P message size to request (default 131072) \\ --cache SECONDS attr/entry cache validity, may be fractional (default 1) \\ --no-direct-io let the kernel cache file pages (trusts stat length) \\ --debug trace FUSE and 9P operations on stderr \\ --help, --version \\PROGRAM defaults to $SHELL (else /bin/sh). The mountpoint is exported as $NINE_MOUNT. \\Default name: --unix PATH -> basename of PATH without .sock/.9p/.socket; \\--tcp IP:PORT -> tcp-IP-PORT (':' becomes '-'); --spawn CMD -> basename of its \\first word; --fd N -> fdN; 9p when nothing usable comes out of that. \\--mntgen: no per-server name; the registry mount goes to --mount (default /mnt/9p). ; /// Where `--name NAME` mounts: `mount_root/NAME`. const mount_root = "/mnt/9p"; /// Name used when nothing usable can be derived from the transport. const fallback_name = "9p"; const own_failure: u8 = 125; /// Largest 9P message size we agree to request: the session allocates two /// buffers of this size up front, before the server negotiates it down. const max_msize: u32 = 16 * 1024 * 1024; /// Write `text` to stdout (informational output such as --help); errors are /// ignored, there is nowhere better to report them. fn printStdout(text: []const u8) void { var off: usize = 0; while (off < text.len) { const rc = linux.write(1, text[off..].ptr, text.len - off); switch (linux.errno(rc)) { .SUCCESS => off += rc, .INTR => continue, else => return, } } } const Config = struct { address: ?nine.Address = null, spawn_cmd: ?[]const u8 = null, /// `--mntgen`: the mount lists the posted-9P registry and dials servers /// lazily (see `runMntgen`); mutually exclusive with the transports. mntgen: bool = false, /// `--mount`: wins over `name` when set. mount: ?[]const u8 = null, /// `--name`: null means "derive from the transport" (see `defaultName`). name: ?[]const u8 = null, uname: ?[]const u8 = null, aname: []const u8 = "", msize: u32 = 131072, cache_ns: u64 = 1_000_000_000, direct_io: bool = true, debug: bool = false, /// Empty means "default program". program: []const []const u8 = &.{}, }; const ParseResult = union(enum) { run: Config, /// Usage error, already reported on stderr; exit with this status. exit: u8, /// --help/--version: text for stdout, then exit 0. Printing is left to /// `main` so that no test path writes to fd 1 (under `zig build test` /// that is the test runner's protocol pipe). info: []const u8, }; fn usageError(comptime fmt: []const u8, args: anytype) ParseResult { std.debug.print("9ns: " ++ fmt ++ "\n(try 9ns --help)\n", args); return .{ .exit = own_failure }; } fn parseArgs(arena: std.mem.Allocator, args: []const [:0]const u8) !ParseResult { var cfg = Config{}; var transports: usize = 0; var i: usize = 1; var program_start: ?usize = null; while (i < args.len) : (i += 1) { const arg: []const u8 = args[i]; if (std.mem.eql(u8, arg, "--")) { program_start = i + 1; break; } if (!std.mem.startsWith(u8, arg, "--")) { // A single-dash word is a typo for an option, not a program. if (arg.len > 1 and arg[0] == '-') return usageError("unknown option {s} (options start with --)", .{arg}); // A bare word starts PROGRAM, as if "--" were given. program_start = i; break; } // Split "--opt=value". var name = arg; var inline_value: ?[]const u8 = null; if (std.mem.indexOfScalar(u8, arg, '=')) |eq| { name = arg[0..eq]; inline_value = arg[eq + 1 ..]; } const Opt = enum { unix, tcp, fd, spawn, mntgen, name, mount, uname, aname, msize, cache, @"no-direct-io", debug, help, version, unknown }; const opt = std.meta.stringToEnum(Opt, name[2..]) orelse .unknown; switch (opt) { .mntgen, .@"no-direct-io", .debug, .help, .version => if (inline_value != null) return usageError("{s} takes no value", .{name}), .unknown => return usageError("unknown option {s}", .{name}), else => {}, } const value: []const u8 = switch (opt) { .mntgen, .@"no-direct-io", .debug, .help, .version, .unknown => "", else => inline_value orelse blk: { i += 1; if (i >= args.len) return usageError("{s} needs a value", .{name}); break :blk args[i]; }, }; switch (opt) { .unix => { if (value.len == 0) return usageError("--unix wants a socket path", .{}); cfg.address = .{ .unix = value }; transports += 1; }, .tcp => { cfg.address = parseTcp(value) orelse return usageError("--tcp wants IP:PORT (IPv6 as [ADDR]:PORT), got '{s}'", .{value}); transports += 1; }, .fd => { const n = std.fmt.parseInt(i32, value, 10) catch return usageError("--fd wants a number, got '{s}'", .{value}); if (n < 0) return usageError("--fd wants a non-negative number", .{}); cfg.address = .{ .fd = n }; transports += 1; }, .spawn => { if (value.len == 0) return usageError("--spawn wants a command", .{}); cfg.spawn_cmd = value; transports += 1; }, .mntgen => { cfg.mntgen = true; transports += 1; }, .name => { if (!validName(value)) return usageError("--name wants a single path component (not empty, no '/', not . or ..), got '{s}'", .{value}); cfg.name = value; }, .mount => { if (value.len == 0) return usageError("--mount wants a path", .{}); cfg.mount = value; }, .uname => cfg.uname = value, .aname => cfg.aname = value, .msize => { cfg.msize = std.fmt.parseInt(u32, value, 10) catch return usageError("--msize wants a number, got '{s}'", .{value}); if (cfg.msize < 4096 or cfg.msize > max_msize) return usageError("--msize must be between 4096 and {d}", .{max_msize}); }, .cache => { const secs = std.fmt.parseFloat(f64, value) catch return usageError("--cache wants seconds, got '{s}'", .{value}); if (!(secs >= 0) or secs > 1e9) return usageError("--cache out of range", .{}); cfg.cache_ns = @intFromFloat(secs * 1e9); }, .@"no-direct-io" => cfg.direct_io = false, .debug => cfg.debug = true, .help => return .{ .info = usage_text }, .version => return .{ .info = version_string ++ "\n" }, .unknown => unreachable, } } if (cfg.mntgen and cfg.name != null) return usageError("--name is not meaningful with --mntgen (the registry mount is --mount, default {s})", .{mount_root}); if (transports == 0) return usageError("one transport is required (--unix, --tcp, --fd, --spawn or --mntgen)", .{}); if (transports > 1) return usageError("exactly one transport is allowed", .{}); if (program_start) |start| { const prog = try arena.alloc([]const u8, args.len - start); for (args[start..], 0..) |a, j| prog[j] = a; cfg.program = prog; } return .{ .run = cfg }; } fn parseTcp(spec: []const u8) ?nine.Address { const colon = std.mem.lastIndexOfScalar(u8, spec, ':') orelse return null; var host = spec[0..colon]; if (host.len >= 2 and host[0] == '[' and host[host.len - 1] == ']') host = host[1 .. host.len - 1]; if (host.len == 0) return null; const port = std.fmt.parseInt(u16, spec[colon + 1 ..], 10) catch return null; return .{ .tcp = .{ .host = host, .port = port } }; } /// A mount name is one path component: non-empty, no '/', no NUL, not `.` /// or `..`. fn validName(name: []const u8) bool { if (name.len == 0) return false; if (std.mem.eql(u8, name, ".") or std.mem.eql(u8, name, "..")) return false; for (name) |c| if (c == '/' or c == 0) return false; return true; } /// The mount name derived from the transport when `--name` is absent: /// `--unix PATH` → basename of PATH without a trailing `.sock`/`.9p`/ /// `.socket`; `--tcp IP:PORT` → `tcp-IP-PORT` with every ':' turned into /// '-' (so an IPv6 literal stays one component); `--spawn CMD` → basename /// of CMD's first word; `--fd N` → `fdN`. Anything that does not come out /// as a valid name (empty basename, `..`, ...) becomes `9p`. The result is /// written into `buf` (at most `buf.len` bytes; longer inputs fall back). fn defaultName(buf: []u8, cfg: Config) []const u8 { const raw: []const u8 = blk: { if (cfg.spawn_cmd) |cmd| { var words = std.mem.tokenizeAny(u8, cmd, " \t\r\n"); break :blk std.fs.path.basename(words.next() orelse ""); } switch (cfg.address orelse return fallback_name) { .unix => |path| { const base = std.fs.path.basename(path); inline for (.{ ".sock", ".socket", ".9p" }) |ext| { if (base.len > ext.len and std.mem.endsWith(u8, base, ext)) break :blk base[0 .. base.len - ext.len]; } break :blk base; }, .tcp => |t| { const text = std.fmt.bufPrint(buf, "tcp-{s}-{d}", .{ t.host, t.port }) catch return fallback_name; std.mem.replaceScalar(u8, text, ':', '-'); return if (validName(text)) text else fallback_name; }, .fd => |fd| { const text = std.fmt.bufPrint(buf, "fd{d}", .{fd}) catch return fallback_name; return text; }, } }; if (!validName(raw) or raw.len > buf.len) return fallback_name; @memcpy(buf[0..raw.len], raw); return buf[0..raw.len]; } /// `--spawn`: run CMD under /bin/sh with one end of a socketpair as its /// stdin/stdout; the other end is the 9P transport. const Server = struct { pid: i32, fd: i32 }; fn spawnServer(cmd: [:0]const u8, envp: [*:null]const ?[*:0]const u8) !Server { var sv: [2]i32 = undefined; switch (linux.errno(linux.socketpair(linux.AF.UNIX, linux.SOCK.STREAM | linux.SOCK.CLOEXEC, 0, &sv))) { .SUCCESS => {}, else => |e| { std.debug.print("9ns: socketpair: E{t}\n", .{e}); return error.SystemResources; }, } const rc = linux.fork(); switch (linux.errno(rc)) { .SUCCESS => {}, else => |e| { _ = linux.close(sv[0]); _ = linux.close(sv[1]); std.debug.print("9ns: fork: E{t}\n", .{e}); return error.SystemResources; }, } if (rc == 0) { // Child: dup2 clears CLOEXEC on 0 and 1; everything else is CLOEXEC. if (linux.errno(linux.dup2(sv[1], 0)) != .SUCCESS or linux.errno(linux.dup2(sv[1], 1)) != .SUCCESS) linux.exit_group(125); // The server shares our process group, so a Ctrl-C meant for the // program would kill it and take the mount down with it: ignore the // tty signals (inherited across exec). SIGPIPE goes back to its // default, we only ignore it for ourselves. ignoreSignal(.INT); ignoreSignal(.QUIT); defaultSignal(.PIPE); const argv = [_:null]?[*:0]const u8{ "sh", "-c", cmd.ptr }; const e = linux.errno(linux.execve("/bin/sh", &argv, envp)); std.debug.print("9ns: --spawn: exec /bin/sh: E{t}\n", .{e}); linux.exit_group(127); } _ = linux.close(sv[1]); return .{ .pid = @intCast(rc), .fd = sv[0] }; } fn stopServer(server: ?Server) void { const s = server orelse return; _ = linux.kill(s.pid, .TERM); ns.reapAny(s.pid); } /// Fail early (before spawning servers or forking) if /dev/fuse is unusable. fn probeFuseDevice() bool { const rc = linux.open("/dev/fuse", .{ .ACCMODE = .RDWR, .CLOEXEC = true }, 0); switch (linux.errno(rc)) { .SUCCESS => { _ = linux.close(@intCast(rc)); return true; }, .NOENT => std.debug.print("9ns: /dev/fuse: ENOENT (is the fuse module loaded? try: modprobe fuse)\n", .{}), else => |e| std.debug.print("9ns: open /dev/fuse: E{t}\n", .{e}), } return false; } fn ignoreSignal(sig: linux.SIG) void { const ign = linux.Sigaction{ .handler = .{ .handler = linux.SIG.IGN }, .mask = linux.sigemptyset(), .flags = 0 }; std.posix.sigaction(sig, &ign, null); } fn defaultSignal(sig: linux.SIG) void { const dfl = linux.Sigaction{ .handler = .{ .handler = linux.SIG.DFL }, .mask = linux.sigemptyset(), .flags = 0 }; std.posix.sigaction(sig, &dfl, null); } /// `--fd N`: the descriptor is ours from now on; it must not leak into the /// program (which could otherwise read 9P replies meant for us). Fails on a /// bad descriptor, which is the earliest place to report it. fn adoptFd(fd: i32) bool { switch (linux.errno(linux.fcntl(fd, linux.F.SETFD, linux.FD_CLOEXEC))) { .SUCCESS => return true, else => |e| { std.debug.print("9ns: --fd {d}: E{t}\n", .{ fd, e }); return false; }, } } fn describeAddress(a: nine.Address, buf: []u8) []const u8 { return switch (a) { .unix => |p| std.fmt.bufPrint(buf, "unix socket {s}", .{p}) catch "unix socket", .tcp => |t| std.fmt.bufPrint(buf, "tcp {s}:{d}", .{ t.host, t.port }) catch "tcp", .fd => |fd| std.fmt.bufPrint(buf, "fd {d}", .{fd}) catch "fd", }; } /// `--mntgen`: one FUSE mount whose root lists the posted-9P registry /// (`$XDG_RUNTIME_DIR/9p`, the /srv translation of cloud9.post). Servers /// are dialed lazily when the program walks into their name; see /// `bridge.serveMntgen` for the process model. Fails before anything is /// forked when XDG_RUNTIME_DIR is unset or /dev/fuse is unusable. fn runMntgen(init: std.process.Init, envp: [*:null]const ?[*:0]const u8, cfg: Config, uname: []const u8) !u8 { const gpa = init.gpa; const mount_arg = cfg.mount orelse mount_root; const mountpoint = ns.resolveMountpoint(gpa, mount_arg) catch |err| { std.debug.print("9ns: --mount {s}: {t}\n", .{ mount_arg, err }); return own_failure; }; defer gpa.free(mountpoint); // The registry must be nameable before anything is forked; there is no // fallback directory (post.registryDir errors rather than guess /tmp). var reg_buf: [128]u8 = undefined; _ = cloud9.post.registryDir(envp, ®_buf) catch |err| { std.debug.print("9ns: --mntgen: {t} (the posted-9P registry is $XDG_RUNTIME_DIR/9p)\n", .{err}); return own_failure; }; if (!probeFuseDevice()) return own_failure; // Writes to a dead server socket must not kill us. ignoreSignal(.PIPE); var child_pid: i32 = 0; const stop_fd = ns.installSignals(&child_pid) catch return own_failure; const uid = linux.getuid(); const gid = linux.getgid(); const child = ns.spawn(gpa, .{ .argv = cfg.program, .envp = envp, .mountpoint = mountpoint, .uid = uid, .gid = gid, .max_read = bridge.max_write, }) catch return own_failure; bridge.serveMntgen(gpa, child.fuse_fd, stop_fd, .{ .io = init.io, .env = envp, .uname = uname, .aname = cfg.aname, .msize = cfg.msize, }, .{ .uid = uid, .gid = gid, .attr_timeout_ns = cfg.cache_ns, .direct_io = cfg.direct_io, .debug = cfg.debug, }) catch |err| { std.debug.print("9ns: fuse: {t}\n", .{err}); }; // Closing the device aborts the FUSE connection: anything still using // the mount gets ENOTCONN instead of hanging on an unserved request. _ = linux.close(child.fuse_fd); const status = ns.reapIfExited(child.pid) orelse ns.waitChild(child.pid) catch own_failure; _ = ns.reportExecFailure(child); return status; } pub fn main(init: std.process.Init) !u8 { const gpa = init.gpa; const arena = init.arena.allocator(); const args = try init.minimal.args.toSlice(arena); const envp: [*:null]const ?[*:0]const u8 = init.minimal.environ.block.slice.ptr; var cfg = switch (try parseArgs(arena, args)) { .exit => |code| return code, .info => |text| { printStdout(text); return 0; }, .run => |c| c, }; // Defaults that come from the environment. if (cfg.program.len == 0) { const env_shell = ns.getenv(envp, "SHELL") orelse ""; const shell = if (env_shell.len == 0) "/bin/sh" else env_shell; cfg.program = try arena.dupe([]const u8, &.{shell}); } const uname = cfg.uname orelse ns.getenv(envp, "USER") orelse "none"; if (cfg.mntgen) return runMntgen(init, envp, cfg, uname); // `--mount PATH` wins; otherwise `/mnt/9p/` with `--name` or a // name derived from the transport. var name_buf: [512]u8 = undefined; const mount_arg: []const u8 = cfg.mount orelse blk: { const name = cfg.name orelse defaultName(&name_buf, cfg); break :blk try std.fmt.allocPrint(arena, mount_root ++ "/{s}", .{name}); }; const mountpoint = ns.resolveMountpoint(gpa, mount_arg) catch |err| { std.debug.print("9ns: --mount {s}: {t}\n", .{ mount_arg, err }); return own_failure; }; defer gpa.free(mountpoint); if (!probeFuseDevice()) return own_failure; // Writes to a dead server socket must not kill us. ignoreSignal(.PIPE); var server: ?Server = null; var address: nine.Address = undefined; if (cfg.spawn_cmd) |cmd| { const cmd_z = try arena.dupeZ(u8, cmd); server = spawnServer(cmd_z, envp) catch return own_failure; ns.watchServer(server.?.pid); address = .{ .fd = server.?.fd }; } else { address = cfg.address.?; if (address == .fd and !adoptFd(address.fd)) return own_failure; } var addr_buf: [256]u8 = undefined; var session = nine.Session.connect(gpa, address, cfg.msize) catch |err| { std.debug.print("9ns: connect to {s}: {t}\n", .{ describeAddress(address, &addr_buf), err }); stopServer(server); return own_failure; }; defer session.deinit(); defer stopServer(server); _ = session.attach(0, uname, cfg.aname) catch |err| { switch (err) { error.Nine => std.debug.print("9ns: attach (uname={s}, aname='{s}'): {s}\n", .{ uname, cfg.aname, session.ename[0..session.ename_len] }), else => std.debug.print("9ns: attach: {t}\n", .{err}), } return own_failure; }; if (cfg.debug) std.debug.print("9ns: attached to {s} (msize {d}), mounting on {s}\n", .{ describeAddress(address, &addr_buf), session.msize, mountpoint }); var child_pid: i32 = 0; const stop_fd = ns.installSignals(&child_pid) catch return own_failure; const uid = linux.getuid(); const gid = linux.getgid(); const child = ns.spawn(gpa, .{ .argv = cfg.program, .envp = envp, .mountpoint = mountpoint, .uid = uid, .gid = gid, .max_read = bridge.max_write, }) catch return own_failure; bridge.serve(gpa, child.fuse_fd, &session, 0, stop_fd, .{ .uid = uid, .gid = gid, .attr_timeout_ns = cfg.cache_ns, .direct_io = cfg.direct_io, .debug = cfg.debug, }) catch |err| switch (err) { error.Closed => std.debug.print("9ns: 9P server connection closed\n", .{}), else => std.debug.print("9ns: fuse: {t}\n", .{err}), }; // Closing the device aborts the FUSE connection: anything still using // the mount gets ENOTCONN instead of hanging on an unserved request. _ = linux.close(child.fuse_fd); const status = ns.reapIfExited(child.pid) orelse ns.waitChild(child.pid) catch own_failure; // An exec failure (126/127) is already in `status`; this prints its message. _ = ns.reportExecFailure(child); return status; } test "parseTcp" { const a = parseTcp("127.0.0.1:564").?; try std.testing.expectEqualStrings("127.0.0.1", a.tcp.host); try std.testing.expectEqual(@as(u16, 564), a.tcp.port); const b = parseTcp("[::1]:9999").?; try std.testing.expectEqualStrings("::1", b.tcp.host); try std.testing.expectEqual(@as(u16, 9999), b.tcp.port); try std.testing.expect(parseTcp("nohost") == null); try std.testing.expect(parseTcp(":564") == null); try std.testing.expect(parseTcp("1.2.3.4:") == null); try std.testing.expect(parseTcp("1.2.3.4:70000") == null); } test "parseArgs" { const arena = std.testing.allocator; { const args = [_][:0]const u8{ "9ns", "--unix", "/s", "--cache", "0.5", "--msize=8192", "--no-direct-io", "--", "sh", "-c", "x" }; const r = try parseArgs(arena, &args); defer arena.free(r.run.program); try std.testing.expectEqualStrings("/s", r.run.address.?.unix); try std.testing.expectEqual(@as(u64, 500_000_000), r.run.cache_ns); try std.testing.expectEqual(@as(u32, 8192), r.run.msize); try std.testing.expect(!r.run.direct_io); try std.testing.expectEqual(@as(usize, 3), r.run.program.len); try std.testing.expectEqualStrings("x", r.run.program[2]); } { const args = [_][:0]const u8{ "9ns", "--fd", "3" }; const r = try parseArgs(arena, &args); try std.testing.expectEqual(@as(i32, 3), r.run.address.?.fd); try std.testing.expectEqual(@as(usize, 0), r.run.program.len); try std.testing.expect(r.run.mount == null); try std.testing.expect(r.run.name == null); } { const named = [_][:0]const u8{ "9ns", "--fd", "3", "--name", "bar", "--mount=/x" }; const r = try parseArgs(arena, &named); try std.testing.expectEqualStrings("bar", r.run.name.?); try std.testing.expectEqualStrings("/x", r.run.mount.?); const eq = [_][:0]const u8{ "9ns", "--fd", "3", "--name=baz" }; try std.testing.expectEqualStrings("baz", (try parseArgs(arena, &eq)).run.name.?); // Invalid names: a path, empty, . and .. for ([_][:0]const u8{ "a/b", "", ".", "..", "/" }) |bad| { const args = [_][:0]const u8{ "9ns", "--fd", "3", "--name", bad }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &args)).exit); } } { // Two transports, no transport, unknown option, missing value: all 125. const two = [_][:0]const u8{ "9ns", "--fd", "3", "--unix", "/s" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &two)).exit); const none = [_][:0]const u8{ "9ns", "--", "sh" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &none)).exit); const unknown = [_][:0]const u8{ "9ns", "--bogus" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &unknown)).exit); const missing = [_][:0]const u8{ "9ns", "--unix" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &missing)).exit); const badcache = [_][:0]const u8{ "9ns", "--fd", "3", "--cache", "abc" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &badcache)).exit); // Empty values, a single-dash typo, and an msize that would allocate gigabytes. const emptyunix = [_][:0]const u8{ "9ns", "--unix=", "--", "sh" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &emptyunix)).exit); const emptymount = [_][:0]const u8{ "9ns", "--fd", "3", "--mount", "" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &emptymount)).exit); const singledash = [_][:0]const u8{ "9ns", "--fd", "3", "-mount", "/x" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &singledash)).exit); const hugemsize = [_][:0]const u8{ "9ns", "--fd", "3", "--msize", "4294967295" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &hugemsize)).exit); const okmsize = [_][:0]const u8{ "9ns", "--fd", "3", "--msize", "16777216" }; try std.testing.expectEqual(@as(u32, 16777216), (try parseArgs(arena, &okmsize)).run.msize); } { // --mntgen is a transport: exclusive with the others, no value, // --name rejected, options still apply to the per-server dials. const ok = [_][:0]const u8{ "9ns", "--mntgen", "--mount", "/m", "--msize=8192", "--", "sh" }; const r = try parseArgs(arena, &ok); defer arena.free(r.run.program); try std.testing.expect(r.run.mntgen); try std.testing.expect(r.run.address == null); try std.testing.expectEqualStrings("/m", r.run.mount.?); try std.testing.expectEqual(@as(u32, 8192), r.run.msize); try std.testing.expectEqual(@as(usize, 1), r.run.program.len); const withunix = [_][:0]const u8{ "9ns", "--mntgen", "--unix", "/s", "--", "sh" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &withunix)).exit); const withspawn = [_][:0]const u8{ "9ns", "--spawn", "x", "--mntgen", "--", "sh" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &withspawn)).exit); const withname = [_][:0]const u8{ "9ns", "--mntgen", "--name", "foo", "--", "sh" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &withname)).exit); const withvalue = [_][:0]const u8{ "9ns", "--mntgen=x", "--", "sh" }; try std.testing.expectEqual(@as(u8, 125), (try parseArgs(arena, &withvalue)).exit); } { const ver = [_][:0]const u8{ "9ns", "--version" }; try std.testing.expectEqualStrings(version_string ++ "\n", (try parseArgs(arena, &ver)).info); const help = [_][:0]const u8{ "9ns", "--help" }; try std.testing.expect(std.mem.startsWith(u8, (try parseArgs(arena, &help)).info, "Usage: 9ns")); } } test "defaultName" { var buf: [512]u8 = undefined; const Case = struct { cfg: Config, want: []const u8 }; const cases = [_]Case{ .{ .cfg = .{ .address = .{ .unix = "/tmp/9debug.sock" } }, .want = "9debug" }, .{ .cfg = .{ .address = .{ .unix = "/run/user/1000/acme" } }, .want = "acme" }, .{ .cfg = .{ .address = .{ .unix = "ramfs.9p" } }, .want = "ramfs" }, .{ .cfg = .{ .address = .{ .unix = "/x/y.socket" } }, .want = "y" }, .{ .cfg = .{ .address = .{ .unix = "/x/.sock" } }, .want = ".sock" }, // the whole name, not empty .{ .cfg = .{ .address = .{ .unix = "/x/y/" } }, .want = "y" }, .{ .cfg = .{ .address = .{ .unix = "/" } }, .want = "9p" }, .{ .cfg = .{ .address = .{ .unix = "/x/.." } }, .want = "9p" }, .{ .cfg = .{ .address = .{ .tcp = .{ .host = "127.0.0.1", .port = 564 } } }, .want = "tcp-127.0.0.1-564" }, .{ .cfg = .{ .address = .{ .tcp = .{ .host = "::1", .port = 9999 } } }, .want = "tcp---1-9999" }, .{ .cfg = .{ .address = .{ .fd = 3 } }, .want = "fd3" }, .{ .cfg = .{ .spawn_cmd = "/x/9proc-demo --stdio" }, .want = "9proc-demo" }, .{ .cfg = .{ .spawn_cmd = " ramfs\t-s" }, .want = "ramfs" }, .{ .cfg = .{ .spawn_cmd = " " }, .want = "9p" }, .{ .cfg = .{}, .want = "9p" }, }; for (cases) |c| try std.testing.expectEqualStrings(c.want, defaultName(&buf, c.cfg)); } test "validName" { try std.testing.expect(validName("a")); try std.testing.expect(validName("tcp-127.0.0.1-564")); try std.testing.expect(validName("...")); try std.testing.expect(!validName("")); try std.testing.expect(!validName(".")); try std.testing.expect(!validName("..")); try std.testing.expect(!validName("a/b")); try std.testing.expect(!validName("a\x00b")); } test { _ = ns; _ = @import("nine.zig"); _ = @import("bridge.zig"); _ = @import("fuse.zig"); }