diff --git a/src/config/Config.zig b/src/config/Config.zig index 64fea91eb1..e9052a66e1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4361,6 +4361,45 @@ pub const RepeatablePath = struct { // If it isn't absolute, we need to make it absolute relative // to the base. var buf: [std.fs.max_path_bytes]u8 = undefined; + + // Check if the path starts with a tilde and expand it to the + // home directory on Linux/macOS. We explicitly look for "~/" + // because we don't support alternate users such as "~alice/" + if (std.mem.startsWith(u8, path, "~/")) expand: { + // Windows isn't supported yet + if (comptime builtin.os.tag == .windows) break :expand; + + const expanded: []const u8 = internal_os.expandHome( + path, + &buf, + ) catch |err| { + try diags.append(alloc, .{ + .message = try std.fmt.allocPrintZ( + alloc, + "error expanding home directory for path {s}: {}", + .{ path, err }, + ), + }); + + // Blank this path so that we don't attempt to resolve it + // again + self.value.items[i] = .{ .required = "" }; + + continue; + }; + + log.debug( + "expanding file path from home directory: path={s}", + .{expanded}, + ); + + switch (self.value.items[i]) { + .optional, .required => |*p| p.* = try alloc.dupeZ(u8, expanded), + } + + continue; + } + const abs = dir.realpath(path, &buf) catch |err| abs: { if (err == error.FileNotFound) { // The file doesn't exist. Try to resolve the relative path diff --git a/src/os/homedir.zig b/src/os/homedir.zig index cf6931f229..b5629fd658 100644 --- a/src/os/homedir.zig +++ b/src/os/homedir.zig @@ -12,7 +12,7 @@ const Error = error{ /// Determine the home directory for the currently executing user. This /// is generally an expensive process so the value should be cached. -pub inline fn home(buf: []u8) !?[]u8 { +pub inline fn home(buf: []u8) !?[]const u8 { return switch (builtin.os.tag) { inline .linux, .macos => try homeUnix(buf), .windows => try homeWindows(buf), @@ -24,7 +24,7 @@ pub inline fn home(buf: []u8) !?[]u8 { }; } -fn homeUnix(buf: []u8) !?[]u8 { +fn homeUnix(buf: []u8) !?[]const u8 { // First: if we have a HOME env var, then we use that. if (posix.getenv("HOME")) |result| { if (buf.len < result.len) return Error.BufferTooSmall; @@ -77,7 +77,7 @@ fn homeUnix(buf: []u8) !?[]u8 { return null; } -fn homeWindows(buf: []u8) !?[]u8 { +fn homeWindows(buf: []u8) !?[]const u8 { const drive_len = blk: { var fba_instance = std.heap.FixedBufferAllocator.init(buf); const fba = fba_instance.allocator(); @@ -110,6 +110,68 @@ fn trimSpace(input: []const u8) []const u8 { return std.mem.trim(u8, input, " \n\t"); } +pub const ExpandError = error{ + HomeDetectionFailed, + BufferTooSmall, +}; + +/// Expands a path that starts with a tilde (~) to the home directory of +/// the current user. +/// +/// Errors if `home` fails or if the size of the expanded path is larger +/// than `buf.len`. +pub fn expandHome(path: []const u8, buf: []u8) ExpandError![]const u8 { + return switch (builtin.os.tag) { + .linux, .macos => try expandHomeUnix(path, buf), + .ios => return path, + else => @compileError("unimplemented"), + }; +} + +fn expandHomeUnix(path: []const u8, buf: []u8) ExpandError![]const u8 { + if (!std.mem.startsWith(u8, path, "~/")) return path; + const home_dir: []const u8 = if (home(buf)) |home_| + home_ orelse return error.HomeDetectionFailed + else |_| + return error.HomeDetectionFailed; + const rest = path[1..]; // Skip the ~ + const expanded_len = home_dir.len + rest.len; + + if (expanded_len > buf.len) return Error.BufferTooSmall; + @memcpy(buf[home_dir.len..expanded_len], rest); + + return buf[0..expanded_len]; +} + +test "expandHomeUnix" { + const testing = std.testing; + const allocator = testing.allocator; + var buf: [std.fs.max_path_bytes]u8 = undefined; + const home_dir = try expandHomeUnix("~/", &buf); + // Joining the home directory `~` with the path `/` + // the result should end with a separator here. (e.g. `/home/user/`) + try testing.expect(home_dir[home_dir.len - 1] == std.fs.path.sep); + + const downloads = try expandHomeUnix("~/Downloads/shader.glsl", &buf); + const expected_downloads = try std.mem.concat(allocator, u8, &[_][]const u8{ home_dir, "Downloads/shader.glsl" }); + defer allocator.free(expected_downloads); + try testing.expectEqualStrings(expected_downloads, downloads); + + try testing.expectEqualStrings("~", try expandHomeUnix("~", &buf)); + try testing.expectEqualStrings("~abc/", try expandHomeUnix("~abc/", &buf)); + try testing.expectEqualStrings("/home/user", try expandHomeUnix("/home/user", &buf)); + try testing.expectEqualStrings("", try expandHomeUnix("", &buf)); + + // Expect an error if the buffer is large enough to hold the home directory, + // but not the expanded path + var small_buf = try allocator.alloc(u8, home_dir.len); + defer allocator.free(small_buf); + try testing.expectError(error.BufferTooSmall, expandHomeUnix( + "~/Downloads", + small_buf[0..], + )); +} + test { const testing = std.testing; diff --git a/src/os/main.zig b/src/os/main.zig index 98e57b4fc1..fb17828628 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -38,6 +38,7 @@ pub const freeTmpDir = file.freeTmpDir; pub const isFlatpak = flatpak.isFlatpak; pub const FlatpakHostCommand = flatpak.FlatpakHostCommand; pub const home = homedir.home; +pub const expandHome = homedir.expandHome; pub const ensureLocale = locale.ensureLocale; pub const clickInterval = mouse.clickInterval; pub const open = openpkg.open;