summaryrefslogtreecommitdiff
path: root/9ns/src/main.zig
diff options
context:
space:
mode:
Diffstat (limited to '9ns/src/main.zig')
-rw-r--r--9ns/src/main.zig133
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");
}