From b68eb67e956b051372910cfe6b453e43121b76e3 Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Mon, 10 Aug 2026 13:21:45 -0700 Subject: [PATCH] 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). --- src/config/edit.zig | 70 ++++++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/src/config/edit.zig b/src/config/edit.zig index 24097498e..113ec8b2a 100644 --- a/src/config/edit.zig +++ b/src/config/edit.zig @@ -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