This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495const std = @import("std");const builtin = @import("builtin");const assert = std.debug.assert;const Allocator = std.mem.Allocator;const ArenaAllocator = std.heap.ArenaAllocator;
const cli = @import("../cli.zig");const internal_os = @import("../os/main.zig");const formatterpkg = @import("formatter.zig");
const log = std.log.scoped(.config);
pub const ParseError = error{ValueRequired} || Allocator.Error;
/// Path is like a string that represents a path value. The difference is that/// when loading the configuration the value for this will be automatically/// expanded relative to the path of the config file (or the home directory).pub const Path = union(enum) { /// No error if the file does not exist. optional: [:0]const u8,
/// The file is required to exist. required: [:0]const u8,
pub fn len(self: Path) usize { return switch (self) { inline else => |path| path.len, }; }
pub fn equal(self: Path, other: Path) bool { return std.meta.eql(self, other); }
/// Parse the input and return a Path. A leading `?` indicates that the path /// is _optional_ and an error should not be logged or displayed to the user /// if that path does not exist. Otherwise the path is required and an error /// should be logged if the path does not exist. pub fn parse( /// Allocator to use. This must be an arena allocator because we assume /// that any allocations will be cleaned up when the arena. arena_alloc: Allocator, /// The input. input: ?[]const u8, ) ParseError!?Path { var value = input orelse return error.ValueRequired;
if (value.len == 0) return null;
const optional = if (value[0] == '?') opt: { value = value[1..]; break :opt true; } else false;
if (value.len >= 2 and value[0] == '"' and value[value.len - 1] == '"') { value = value[1 .. value.len - 1]; }
if (optional) return .{ .optional = try arena_alloc.dupeZ(u8, value) } else return .{ .required = try arena_alloc.dupeZ(u8, value) }; }
/// Parse CLI option. pub fn parseCLI( /// The path. The value will be overwritten. self: *Path, /// Allocator to use. This must be an arena allocator because we assume /// that any allocations will be cleaned up when the arena. arena_alloc: Allocator, // The input. input: ?[]const u8, ) ParseError!void { assert(input != null); const item = try parse(arena_alloc, input) orelse return; if (item.len() == 0) return; self.* = item; }
/// Used by formatter. pub fn formatEntry(self: *const Path, formatter: anytype) !void { var buf: [std.fs.max_path_bytes + 1]u8 = undefined; const value = switch (self.*) { .optional => |path| std.fmt.bufPrint( &buf, "?{s}", .{path}, ) catch |err| switch (err) { // Required for builds on Linux where NoSpaceLeft // isn't an allowed error for fmt. error.NoSpaceLeft => return error.OutOfMemory, }, .required => |path| path, };
try formatter.formatEntry([]const u8, value); }
/// Return a clone of the path. pub fn clone( /// The path to clone. self: Path, /// This must be an arena allocator because we rely on the arena to /// clean up our allocations. arena_alloc: Allocator, ) Allocator.Error!Path { return switch (self) { .optional => |path| .{ .optional = try arena_alloc.dupeZ(u8, path), }, .required => |path| .{ .required = try arena_alloc.dupeZ(u8, path), }, }; }
/// Expand relative paths or paths prefixed with `~/`. The path will be /// overwritten. pub fn expand( /// The path to expand. self: *Path, /// This must be an arena allocator because we rely on the arena to /// clean up our allocations. arena_alloc: Allocator, /// The base directory to expand relative paths. It must be an absolute /// path. base: []const u8, /// Errors will be added to the list of diagnostics if they occur. diags: *cli.DiagnosticList, ) !void { assert(std.fs.path.isAbsolute(base));
const path = switch (self.*) { .optional, .required => |path| path, };
// If it is already absolute we can ignore it. if (path.len == 0 or std.fs.path.isAbsolute(path)) return;
// 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(arena_alloc, .{ .message = try std.fmt.allocPrintZ( arena_alloc, "error expanding home directory for path {s}: {}", .{ path, err }, ), });
// Blank this path so that we don't attempt to resolve it // again self.* = .{ .required = "" };
return; };
log.debug( "expanding file path from home directory: path={s}", .{expanded}, );
switch (self.*) { .optional, .required => |*p| p.* = try arena_alloc.dupeZ(u8, expanded), }
return; }
var dir = try std.fs.openDirAbsolute(base, .{}); defer dir.close();
const abs = dir.realpath(path, &buf) catch |err| abs: { if (err == error.FileNotFound) { // The file doesn't exist. Try to resolve the relative path // another way. const resolved = try std.fs.path.resolve(arena_alloc, &.{ base, path }); defer arena_alloc.free(resolved); @memcpy(buf[0..resolved.len], resolved); break :abs buf[0..resolved.len]; }
try diags.append(arena_alloc, .{ .message = try std.fmt.allocPrintZ( arena_alloc, "error resolving file path {s}: {}", .{ path, err }, ), });
// Blank this path so that we don't attempt to resolve it again self.* = .{ .required = "" };
return; };
log.debug( "expanding file path relative={s} abs={s}", .{ path, abs }, );
switch (self.*) { .optional, .required => |*p| p.* = try arena_alloc.dupeZ(u8, abs), } }
test "parse" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
const Tag = std.meta.Tag(Path);
{ const item = (try Path.parse(alloc, "config.1")).?; try testing.expectEqual(Tag.required, @as(Tag, item)); try testing.expectEqualStrings("config.1", item.required); }
{ const item = (try Path.parse(alloc, "?config.2")).?; try testing.expectEqual(Tag.optional, @as(Tag, item)); try testing.expectEqualStrings("config.2", item.optional); }
{ const item = (try Path.parse(alloc, "\"?config.3\"")).?; try testing.expectEqual(Tag.required, @as(Tag, item)); try testing.expectEqualStrings("?config.3", item.required); }
{ const item = (try Path.parse(alloc, "?\"config.4\"")).?; try testing.expectEqual(Tag.optional, @as(Tag, item)); try testing.expectEqualStrings("config.4", item.optional); }
{ const item = (try Path.parse(alloc, "?")).?; try testing.expectEqual(Tag.optional, @as(Tag, item)); try testing.expectEqualStrings("", item.optional); }
{ const item = (try Path.parse(alloc, "\"\"")).?; try testing.expectEqual(Tag.required, @as(Tag, item)); try testing.expectEqualStrings("", item.required); }
{ const item = (try Path.parse(alloc, "?\"\"")).?; try testing.expectEqual(Tag.optional, @as(Tag, item)); try testing.expectEqualStrings("", item.optional); } }
test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
const Tag = std.meta.Tag(Path); var item: Path = undefined;
try item.parseCLI(alloc, "config.1"); try testing.expectEqual(Tag.required, @as(Tag, item)); try testing.expectEqualStrings("config.1", item.required);
try item.parseCLI(alloc, "?config.2"); try testing.expectEqual(Tag.optional, @as(Tag, item)); try testing.expectEqualStrings("config.2", item.optional);
try item.parseCLI(alloc, "\"?config.3\""); try testing.expectEqual(Tag.required, @as(Tag, item)); try testing.expectEqualStrings("?config.3", item.required);
// Zero-length values, ignored
try item.parseCLI(alloc, "?"); try testing.expectEqual(Tag.required, @as(Tag, item)); try testing.expectEqualStrings("?config.3", item.required);
try item.parseCLI(alloc, "\"\""); try testing.expectEqual(Tag.required, @as(Tag, item)); try testing.expectEqualStrings("?config.3", item.required);
try item.parseCLI(alloc, "?\"\""); try testing.expectEqual(Tag.required, @as(Tag, item)); try testing.expectEqualStrings("?config.3", item.required); }
test "formatConfig single item" { const testing = std.testing; var buf = std.ArrayList(u8).init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var item: Path = undefined; try item.parseCLI(alloc, "A"); try item.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); try std.testing.expectEqualSlices(u8, "a = A\n", buf.items); }
test "formatConfig multiple items" { const testing = std.testing; var buf = std.ArrayList(u8).init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var item: Path = undefined; try item.parseCLI(alloc, "A"); try item.parseCLI(alloc, "?B"); try item.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); try std.testing.expectEqualSlices(u8, "a = ?B\n", buf.items); }};
/// RepeatablePath is like repeatable string but represents a path value. The/// difference is that when loading the configuration any values for this will/// be automatically expanded relative to the path of the config file (or the home/// directory).pub const RepeatablePath = struct { value: std.ArrayListUnmanaged(Path) = .{},
pub fn parseCLI(self: *RepeatablePath, alloc: Allocator, input: ?[]const u8) ParseError!void { const item = try Path.parse(alloc, input) orelse { self.value.clearRetainingCapacity(); return; };
if (item.len() == 0) { // This handles the case of zero length paths after removing any ? // prefixes or surrounding quotes. In this case, we don't reset the // list. return; }
try self.value.append(alloc, item); }
/// Deep copy of the struct. Required by Config. pub fn clone(self: *const RepeatablePath, alloc: Allocator) Allocator.Error!RepeatablePath { const value = try self.value.clone(alloc); for (value.items) |*item| { item.* = try item.clone(alloc); }
return .{ .value = value, }; }
/// Compare if two of our value are equal. Required by Config. pub fn equal(self: RepeatablePath, other: RepeatablePath) bool { if (self.value.items.len != other.value.items.len) return false; for (self.value.items, other.value.items) |a, b| { if (!a.equal(b)) return false; }
return true; }
/// Used by Formatter pub fn formatEntry(self: RepeatablePath, formatter: anytype) !void { if (self.value.items.len == 0) { try formatter.formatEntry(void, {}); return; }
var buf: [std.fs.max_path_bytes + 1]u8 = undefined; for (self.value.items) |item| { const value = switch (item) { .optional => |path| std.fmt.bufPrint( &buf, "?{s}", .{path}, ) catch |err| switch (err) { // Required for builds on Linux where NoSpaceLeft // isn't an allowed error for fmt. error.NoSpaceLeft => return error.OutOfMemory, }, .required => |path| path, };
try formatter.formatEntry([]const u8, value); } }
/// Expand all the paths relative to the base directory. pub fn expand( self: *RepeatablePath, alloc: Allocator, base: []const u8, diags: *cli.DiagnosticList, ) !void { for (self.value.items) |*path| { try path.expand(alloc, base, diags); } } test "parseCLI" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: RepeatablePath = .{}; try list.parseCLI(alloc, "config.1"); try list.parseCLI(alloc, "?config.2"); try list.parseCLI(alloc, "\"?config.3\"");
try testing.expectEqual(@as(usize, 3), list.value.items.len);
// Zero-length values, ignored try list.parseCLI(alloc, "?"); try list.parseCLI(alloc, "\"\"");
try testing.expectEqual(@as(usize, 3), list.value.items.len);
const Tag = std.meta.Tag(Path); try testing.expectEqual(Tag.required, @as(Tag, list.value.items[0])); try testing.expectEqualStrings("config.1", list.value.items[0].required);
try testing.expectEqual(Tag.optional, @as(Tag, list.value.items[1])); try testing.expectEqualStrings("config.2", list.value.items[1].optional);
try testing.expectEqual(Tag.required, @as(Tag, list.value.items[2])); try testing.expectEqualStrings("?config.3", list.value.items[2].required);
try list.parseCLI(alloc, ""); try testing.expectEqual(@as(usize, 0), list.value.items.len); }
test "formatConfig empty" { const testing = std.testing; var buf = std.ArrayList(u8).init(testing.allocator); defer buf.deinit();
var list: RepeatablePath = .{}; try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); try std.testing.expectEqualSlices(u8, "a = \n", buf.items); }
test "formatConfig single item" { const testing = std.testing; var buf = std.ArrayList(u8).init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: RepeatablePath = .{}; try list.parseCLI(alloc, "A"); try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); try std.testing.expectEqualSlices(u8, "a = A\n", buf.items); }
test "formatConfig multiple items" { const testing = std.testing; var buf = std.ArrayList(u8).init(testing.allocator); defer buf.deinit();
var arena = ArenaAllocator.init(testing.allocator); defer arena.deinit(); const alloc = arena.allocator();
var list: RepeatablePath = .{}; try list.parseCLI(alloc, "A"); try list.parseCLI(alloc, "?B"); try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); try std.testing.expectEqualSlices(u8, "a = A\na = ?B\n", buf.items); }};