diff options
Diffstat (limited to '9ns/src/main.zig')
| -rw-r--r-- | 9ns/src/main.zig | 133 |
1 files changed, 127 insertions, 6 deletions
diff --git a/9ns/src/main.zig b/9ns/src/main.zig index 26bd699..076aa42 100644 --- a/9ns/src/main.zig +++ b/9ns/src/main.zig @@ -21,7 +21,9 @@ const usage_text = \\ --fd N already-connected inherited descriptor \\ --spawn CMD run CMD (via /bin/sh -c) with a socketpair on its stdin/stdout \\Options: - \\ --mount PATH mountpoint inside the new namespace (default /mnt/9p) + \\ --name NAME mount name: the tree appears at /mnt/9p/NAME (one path + \\ component; default derived from the transport, see below) + \\ --mount PATH mountpoint inside the new namespace (overrides --name) \\ --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) @@ -30,9 +32,17 @@ const usage_text = \\ --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. \\ ; +/// 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. @@ -55,7 +65,10 @@ fn printStdout(text: []const u8) void { const Config = struct { address: ?nine.Address = null, spawn_cmd: ?[]const u8 = null, - mount: []const u8 = "/mnt/9p", + /// `--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, @@ -106,7 +119,7 @@ fn parseArgs(arena: std.mem.Allocator, args: []const [:0]const u8) !ParseResult name = arg[0..eq]; inline_value = arg[eq + 1 ..]; } - const Opt = enum { unix, tcp, fd, spawn, mount, uname, aname, msize, cache, @"no-direct-io", debug, help, version, unknown }; + const Opt = enum { unix, tcp, fd, spawn, 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) { .@"no-direct-io", .debug, .help, .version => if (inline_value != null) return usageError("{s} takes no value", .{name}), @@ -142,6 +155,10 @@ fn parseArgs(arena: std.mem.Allocator, args: []const [:0]const u8) !ParseResult cfg.spawn_cmd = value; 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; @@ -183,6 +200,52 @@ fn parseTcp(spec: []const u8) ?nine.Address { 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 }; @@ -298,8 +361,15 @@ pub fn main(init: std.process.Init) !u8 { cfg.program = try arena.dupe([]const u8, &.{shell}); } const uname = cfg.uname orelse ns.getenv(envp, "USER") orelse "none"; - const mountpoint = ns.resolveMountpoint(gpa, cfg.mount) catch |err| { - std.debug.print("9ns: --mount {s}: {t}\n", .{ cfg.mount, err }); + // `--mount PATH` wins; otherwise `/mnt/9p/<name>` 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); @@ -405,7 +475,21 @@ test "parseArgs" { 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.expectEqualStrings("/mnt/9p", r.run.mount); + 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. @@ -439,6 +523,43 @@ test "parseArgs" { } } +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"); } |
