Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: expand tildes ~ in config file paths to HOME #3811

Merged
merged 8 commits into from
Jan 2, 2025
39 changes: 39 additions & 0 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 65 additions & 3 deletions src/os/homedir.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions src/os/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down