diff options
| -rw-r--r-- | docs/fs.md | 6 | ||||
| -rw-r--r-- | src/ShaderBuild.zig | 8 | ||||
| -rw-r--r-- | src/builtins.zig | 3 | ||||
| -rw-r--r-- | src/dump.zig | 7 | ||||
| -rw-r--r-- | src/exec.zig | 3 | ||||
| -rw-r--r-- | src/fs.zig | 57 | ||||
| -rw-r--r-- | src/look.zig | 5 | ||||
| -rw-r--r-- | src/main.zig | 7 | ||||
| -rw-r--r-- | src/ninep/pane.zig | 3 | ||||
| -rw-r--r-- | src/tagline.zig | 3 | ||||
| -rw-r--r-- | test/fs.py | 22 |
11 files changed, 105 insertions, 19 deletions
@@ -199,7 +199,11 @@ A line written to `look` is a right click on it: selects line 12, newline included; `file:12:5` puts the caret at line 12, byte column 5; `file:<addr>` takes any address (below), **evaluated from the file's dot**: `file:/re/` finds the next match after the selection, - `file:0/re/` the first. `:addr` addresses the pane itself. + `file:0/re/` the first. `:addr` addresses the pane itself. A leading `~` + is the home directory ($HOME, else the passwd entry's; `~user` that + user's) here and wherever a path is typed (`name`, `Save`, `ThemeFile`, + `DumpDir`, `Restore`, `pardes '~/x'`), even beside a file named `~`: write + `./~` for that. - `@p<serial>:<addr>` addresses a pane by serial, a terminal's logical lines too. - a directory types `ls` into a terminal idle there, else opens one there. diff --git a/src/ShaderBuild.zig b/src/ShaderBuild.zig index c2610fd7..b50999a5 100644 --- a/src/ShaderBuild.zig +++ b/src/ShaderBuild.zig @@ -236,11 +236,9 @@ fn collect(b: *Build, gpa: std.mem.Allocator, core: *pardes.Pardes, job: *Job) v } } -/// `~/` is the home directory. -fn expand(buf: []u8, path: []const u8) []const u8 { - if (!std.mem.startsWith(u8, path, "~/")) return path; - const home = libc.getenv("HOME") orelse return path; - return std.fmt.bufPrint(buf, "{s}/{s}", .{ std.mem.span(home), path[2..] }) catch path; +/// `~` is the home directory (the one rule, filesystem.expandHome). +fn expand(buf: *[4096]u8, path: []const u8) []const u8 { + return pardes.filesystem.expandHome(path, buf); } /// The compile thread: each file behind the prefix, through glslc with the diff --git a/src/builtins.zig b/src/builtins.zig index ba67766d..6aabba8d 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -818,7 +818,8 @@ pub const ThemeFile = struct { pub const enabled = pardes.hosted; pub fn run(c: Ctx) void { if (comptime !enabled) unreachable; - const arg = std.mem.trim(u8, c.arg orelse return, " \t\r\n"); + var home_buf: [4096]u8 = undefined; + const arg = pardes.filesystem.expandHome(std.mem.trim(u8, c.arg orelse return, " \t\r\n"), &home_buf); // Said with why, not "invalid theme path". if (!std.mem.endsWith(u8, arg, ".zon")) { var said: [320]u8 = undefined; diff --git a/src/dump.zig b/src/dump.zig index 1cc115be..8e767240 100644 --- a/src/dump.zig +++ b/src/dump.zig @@ -100,13 +100,10 @@ fn hostEnv(name: [*:0]const u8) ?[*:0]u8 { /// setting when set (a leading `~/` is the home directory), else the default. pub fn directory(buf: []u8, dir_setting: []const u8) ?[]const u8 { if (dir_setting.len == 0) return defaultDirectory(buf); - var joined: [2048]u8 = undefined; + var joined: [4096]u8 = undefined; // `~/x`, else a relative one from the directory pardes started in: the // same place however it was written, and shown so. - const raw = if (std.mem.startsWith(u8, dir_setting, "~/")) (if (hostEnv("HOME")) |h| - std.fmt.bufPrint(&joined, "{s}/{s}", .{ std.mem.span(h), dir_setting[2..] }) catch return null - else - dir_setting) else dir_setting; + const raw = pardes.filesystem.expandHome(dir_setting, &joined); var fba_buf: [4096]u8 = undefined; var fba = std.heap.FixedBufferAllocator.init(&fba_buf); var cwd_buf: [1024]u8 = undefined; diff --git a/src/exec.zig b/src/exec.zig index 7d682d74..d114dbf8 100644 --- a/src/exec.zig +++ b/src/exec.zig @@ -295,9 +295,10 @@ pub fn saveTo(p: *Pardes, id: usize, path: []const u8) void { if (path.len == 0 or path[path.len - 1] == '/' or std.mem.indexOfScalar(u8, path, '\n') != null) return p.reportError(id, "save", error.NoFilename); + var home_buf: [4096]u8 = undefined; const full = std.fs.path.resolvePosix( p.scratch.allocator(), - &.{ Pardes.paneDir(pane), path }, + &.{ Pardes.paneDir(pane), @import("fs.zig").expandHome(path, &home_buf) }, ) catch return; if (!std.fs.path.isAbsolute(full)) return p.reportError(id, "save", error.NoDirectory); if (full.len > Pardes.SavePath.cap) return p.reportError(id, "save", error.PathTooLong); @@ -732,6 +732,59 @@ pub fn dotRelative(word: []const u8) bool { /// Where a path really is on the host, and whether it is a directory. The /// path may lie in a mount this editor serves, so the turn is given up for /// the syscalls: another thread answers them. +/// A path's leading `~` -- alone, or before `/` or `:` (`~/x:12`) -- is the +/// home directory: $HOME, else the passwd entry's. `~user/...` is that +/// user's. As in a shell, `~` is home even beside a file named `~` in the +/// current directory. Every entry point that takes a path the user typed +/// runs it through here: look (B3 and 9P), name, Save, ThemeFile, DumpDir, +/// Restore, a forwarded `pardes ~/x`. The path itself when there is no +/// `~` to expand, or no home for it. +pub fn expandHome(path: []const u8, out: *[4096]u8) []const u8 { + if (comptime !pardes.hosted) return path; + return expandHomeFrom(path, out, std.c.getenv("HOME")); +} + +/// expandHome with HOME given (null: unset), so a test need not change the +/// process's environment, which a child spawned later reads. +fn expandHomeFrom(path: []const u8, out: *[4096]u8, env_home: ?[*:0]const u8) []const u8 { + if (path.len == 0 or path[0] != '~') return path; + const end = std.mem.indexOfAny(u8, path, "/:") orelse path.len; + const home = (if (end == 1) homeDir(env_home) else userHome(path[1..end])) orelse return path; + const trimmed = std.mem.trimEnd(u8, home, "/"); + const rest = path[end..]; + if (trimmed.len == 0 and (rest.len == 0 or rest[0] != '/')) return std.fmt.bufPrint(out, "/{s}", .{rest}) catch path; + return std.fmt.bufPrint(out, "{s}{s}", .{ trimmed, rest }) catch path; +} + +fn homeDir(env_home: ?[*:0]const u8) ?[]const u8 { + if (env_home) |h| if (h[0] != 0) return std.mem.span(h); + const pw = std.c.getpwuid(std.c.getuid()) orelse return null; + return std.mem.span(pw.dir orelse return null); +} + +fn userHome(name: []const u8) ?[]const u8 { + var z: [256]u8 = undefined; + const name_z = std.fmt.bufPrintSentinel(&z, "{s}", .{name}, 0) catch return null; + const pw = std.c.getpwnam(name_z) orelse return null; + return std.mem.span(pw.dir orelse return null); +} + +test "a leading ~ is home, from HOME or else the passwd entry" { + var out: [4096]u8 = undefined; + const home = homeDir(std.c.getenv("HOME")) orelse return error.SkipZigTest; + var want: [4096]u8 = undefined; + try std.testing.expectEqualStrings(try std.fmt.bufPrint(&want, "{s}/x:12", .{std.mem.trimEnd(u8, home, "/")}), expandHome("~/x:12", &out)); + try std.testing.expectEqualStrings(std.mem.trimEnd(u8, home, "/"), expandHome("~", &out)); + try std.testing.expectEqualStrings("a/~/b", expandHome("a/~/b", &out)); + try std.testing.expectEqualStrings("~no-such-user-pardes/x", expandHome("~no-such-user-pardes/x", &out)); + try std.testing.expectEqualStrings("/", expandHome("~root", &out)[0..1]); + + // HOME unset: the passwd entry's home. + const pw = std.c.getpwuid(std.c.getuid()) orelse return error.SkipZigTest; + var from_passwd: [4096]u8 = undefined; + try std.testing.expectEqualStrings(try std.fmt.bufPrint(&from_passwd, "{s}/y", .{std.mem.trimEnd(u8, std.mem.span(pw.dir.?), "/")}), expandHomeFrom("~/y", &out, null)); +} + pub fn resolveOs(path: []const u8, out: *[4096]u8) ?Resolved { if (comptime !platform_has_fs) return null; var z: [4096]u8 = undefined; @@ -1171,7 +1224,9 @@ pub fn readFile(gpa: std.mem.Allocator, path: []const u8) ![]u8 { /// Restore first tries a relative argument under the default dump directory, /// then preserves the ordinary file lookup. Absolute paths remain verbatim. -pub fn readRestore(gpa: std.mem.Allocator, path: []const u8, dir_setting: []const u8) ![]u8 { +pub fn readRestore(gpa: std.mem.Allocator, typed: []const u8, dir_setting: []const u8) ![]u8 { + var home_buf: [4096]u8 = undefined; + const path = expandHome(typed, &home_buf); var dir_buf: [4096]u8 = undefined; const dir = @import("dump.zig").directory(&dir_buf, dir_setting); return readRestoreIn(gpa, path, dir); diff --git a/src/look.zig b/src/look.zig index de51804d..357a9c63 100644 --- a/src/look.zig +++ b/src/look.zig @@ -1499,8 +1499,11 @@ fn isRecentList(pane: *const Pane) bool { return std.meta.eql(o.from, panes.Output.Origin{ .cmd = .Recent }) or std.meta.eql(o.from, panes.Output.Origin{ .cmd = .Jumplist }); } -pub fn lookAt(p: *Pardes, id: usize, operand: []const u8) void { +pub fn lookAt(p: *Pardes, id: usize, operand_raw: []const u8) void { const pane = p.panes[id] orelse return; + // `~/x`, `~/x:12`, `~/x:/re/`: home, before anything reads the text. + var home_buf: [4096]u8 = undefined; + const operand = filesystem.expandHome(std.mem.trim(u8, operand_raw, " \t\r\n"), &home_buf); // A +Recent row is a remembered place: reopened and clamped to the file // as it is now, never a miss for a line the file no longer has. if (isRecentList(pane)) { diff --git a/src/main.zig b/src/main.zig index 576efc8e..dfeed52d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -407,7 +407,7 @@ fn nativeMain(init: std.process.Init) !void { // itself, which is what a right click in that pane would have been. var look_buf: [64]u8 = undefined; const look = try std.fmt.bufPrint(&look_buf, "/pane/{d}/look", .{parent.serial}); - const word = positional orelse { + const typed_word = positional orelse { var tag_buf: [64]u8 = undefined; const tag = try std.fmt.bufPrint(&tag_buf, "/pane/{d}/tag", .{parent.serial}); const contents = ninep_io.Client.read(arena, parent.dial, tag, tag) catch |err| Refuse.with(init.io, tag, err); @@ -415,7 +415,10 @@ fn nativeMain(init: std.process.Init) !void { try std.Io.File.stderr().writeStreamingAll(init.io, nested_text); std.process.exit(1); }; - if (std.mem.indexOfAny(u8, word, "\r\n") != null) Refuse.with(init.io, "a file name", error.NotOneLine); + if (std.mem.indexOfAny(u8, typed_word, "\r\n") != null) Refuse.with(init.io, "a file name", error.NotOneLine); + // `pardes '~/x'`, quoted so the shell left it: home, as a look's. + var home_buf: [4096]u8 = undefined; + const word = pardes.filesystem.expandHome(typed_word, &home_buf); const target = @import("look.zig").parsePathLine(word); var realbuf: [4096]u8 = undefined; var newbuf: [4096]u8 = undefined; diff --git a/src/ninep/pane.zig b/src/ninep/pane.zig index dc8b4b10..7526075b 100644 --- a/src/ninep/pane.zig +++ b/src/ninep/pane.zig @@ -860,7 +860,8 @@ fn writeName(p: *Pardes, req: Req, id: usize, pane: *Pane) Reply { "rename not allowed: a terminal is named by its shell's directory; cd there, or Tty in another" else "rename not allowed: an image or PDF is named by the file it shows"); - const full = std.fs.path.resolvePosix(p.scratch.allocator(), &.{ pardes.Pardes.paneDir(pane), name }) catch + var home_buf: [4096]u8 = undefined; + const full = std.fs.path.resolvePosix(p.scratch.allocator(), &.{ pardes.Pardes.paneDir(pane), fs.expandHome(name, &home_buf) }) catch return Reply.fail(req.tag, E.NOMEM); if (!std.fs.path.isAbsolute(full) or full.len >= 4096) return tree.failText(req.tag, E.INVAL, "invalid file name: longer than a path may be"); nameBuffer(p, id, full, false) catch |err| return Reply.fail(req.tag, switch (err) { diff --git a/src/tagline.zig b/src/tagline.zig index 1923871a..dc7d10d4 100644 --- a/src/tagline.zig +++ b/src/tagline.zig @@ -344,7 +344,8 @@ pub fn commitTagName(p: *Pardes, pane: *Pane) bool { p.reportError(id, "name", error.InvalidName); return false; }; - const full = std.fs.path.resolvePosix(p.scratch.allocator(), &.{ Pardes.paneDir(pane), name }) catch |err| { + var home_buf: [4096]u8 = undefined; + const full = std.fs.path.resolvePosix(p.scratch.allocator(), &.{ Pardes.paneDir(pane), pardes.filesystem.expandHome(name, &home_buf) }) catch |err| { p.reportError(id, "name", err); return false; }; @@ -383,6 +383,19 @@ def discovery(binary, embedded=False): client.remove(f'/pane/{scratch}') # A Save makes the directories its file goes in, whatever names # it: the pane's own name or Save's path. + # `~` is home (this session's HOME, the test's root) wherever a + # path is typed: Save, name, a look, with an address or not. + tilde = new_pane(client, b'one\ntwo\nthree\n') + client.write(f'/pane/{tilde}/ctl', b'Save ~/tilde-made/sub/x.txt\n') + assert (root / 'tilde-made/sub/x.txt').read_bytes() == b'one\ntwo\nthree\n' + client.write(f'/pane/{tilde}/name', b'~/tilde-named.txt\n') + assert client.read(f'/pane/{tilde}/name') == f'{root}/tilde-named.txt\n'.encode() + client.remove(f'/pane/{tilde}') + client.write('/look', b'~/tilde-made/sub/x.txt:3\n') + looked = [row for row in client.read('/index').decode().splitlines() if row.split()[3] == f'{root}/tilde-made/sub/x.txt'] + assert len(looked) == 1, client.read('/index') + assert client.read(f'/pane/{looked[0].split()[0]}/addr').startswith(b'8 '), client.read(f'/pane/{looked[0].split()[0]}/addr') + client.remove(f'/pane/{looked[0].split()[0]}') made_dirs = new_pane(client, b'deep\n') client.write(f'/pane/{made_dirs}/ctl', f'Save {root}/save-made/a/b/x.txt\n'.encode()) assert (root / 'save-made/a/b/x.txt').read_bytes() == b'deep\n' @@ -1333,6 +1346,15 @@ def test(binary, quic=False): execute(client, named, 'Save') client.remove(f'/pane/{named}') assert deep.read_bytes() == b'deep\n' + # `pardes '~/x'`, quoted so the shell left the `~`: home all the + # same: the launching shell's HOME. + before = serials() + quoted = subprocess.run([binary, '~/fwd-tilde.txt'], cwd=root, env=dict(env, HOME=str(root)), + stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, timeout=10) + assert quoted.returncode == 0, quoted.stderr + assert f'{root}/fwd-tilde.txt'.encode() in client.read('/index'), client.read('/index') + for serial in serials() - before: + client.remove(f'/pane/{serial}') # A name the session refuses: its reason and exit 1, as B's, # never a nested editor, and no pane left for it. before = serials() |
