config/edit: better handling of existing paths

This adds some better handling of existing paths when editing
configuration files:

* If we've found an existing file we just skip any attempts to create
  files/dirs, and just return the path.

* If the path (including file) does not exist, we check to see if the
  directory exists first (possibly following symlinks). Directory
  creation happens normally after this (note that any intermediary
  symlinks in this process will still cause the process to fail, this is
  to prevent infinite loops, as per the comments in
  std.Io.Threaded.dirCreateDirPath).
This commit is contained in:
Chris Marchesi
2026-08-10 13:21:45 -07:00
parent 951a03b58b
commit b68eb67e95

View File

@@ -27,31 +27,52 @@ pub fn openPath(alloc_gpa: Allocator) ![:0]const u8 {
// Get the path we should open
const config_path = try configPath(alloc_arena);
// Create config directory recursively.
if (std.fs.path.dirname(config_path)) |config_dir| {
try std.Io.Dir.cwd().createDirPath(global.io(), config_dir);
if (!config_path.exists) {
if (std.fs.path.dirname(config_path.name)) |config_dir| check_dir: {
// Check to see if dir exists.
const dir = std.Io.Dir.cwd().openDir(global.io(), config_dir, .{ .follow_symlinks = true }) catch |err| {
switch (err) {
error.FileNotFound => {
// Create config directory recursively. Note that this does not
// allow intermediate symlinks by design, see
// std.Io.Threaded.dirCreateDirPath for why. If some sort of
// complex symlink structure is needed, it will need to be created
// manually.
try std.Io.Dir.cwd().createDirPath(global.io(), config_dir);
break :check_dir;
},
else => return err,
}
};
dir.close(global.io());
}
// Try to create file and go on if it already exists
_ = std.Io.Dir.createFileAbsolute(
global.io(),
config_path.name,
.{ .exclusive = true },
) catch |err| {
switch (err) {
error.PathAlreadyExists => {},
else => return err,
}
};
}
// Try to create file and go on if it already exists
_ = std.Io.Dir.createFileAbsolute(
global.io(),
config_path,
.{ .exclusive = true },
) catch |err| {
switch (err) {
error.PathAlreadyExists => {},
else => return err,
}
};
return try alloc_gpa.dupeZ(u8, config_path);
return try alloc_gpa.dupeZ(u8, config_path.name);
}
const ConfigPathResult = struct {
name: []const u8,
exists: bool,
};
/// Returns the config path to use for open for the current OS.
///
/// The allocator must be an arena allocator. No memory is freed by this
/// function and the resulting path is not all the memory that is allocated.
fn configPath(alloc_arena: Allocator) ![]const u8 {
fn configPath(alloc_arena: Allocator) !ConfigPathResult {
const paths: []const []const u8 = try configPathCandidates(alloc_arena);
assert(paths.len > 0);
@@ -75,17 +96,26 @@ fn configPath(alloc_arena: Allocator) ![]const u8 {
const stat = try f.stat(global.io());
// If the file is non-empty, return it.
if (stat.size > 0) return path;
if (stat.size > 0) return .{
.name = path,
.exists = true,
};
// If the file is empty, remember it exists.
if (exists == null) exists = path;
}
// No paths are non-empty, return the first path that exists.
if (exists) |v| return v;
if (exists) |v| return .{
.name = v,
.exists = true,
};
// No paths are non-empty or exist, return the first path.
return paths[0];
return .{
.name = paths[0],
.exists = false,
};
}
/// Returns a const list of possible paths the main config file could be