//! Native startup configuration discovery and loading. //! //! The file is named `pardes` inside the platform's per-user configuration //! directory. Discovery is deliberately outside the sans-IO core: native //! launchers opt in by putting the bytes in `Options.startup_config`, while //! tests and the browser keep the default `null`. const std = @import("std"); const builtin = @import("builtin"); const max_bytes = 1024 * 1024; /// Resolve the native per-user config file with stdlib environment and path /// APIs. XDG_CONFIG_HOME is accepted only when absolute, as required by the /// XDG base-directory specification; an empty/relative value falls back. pub fn path(gpa: std.mem.Allocator, env: *const std.process.Environ.Map) !?[]u8 { if (builtin.os.tag == .windows) { if (nonEmpty(env.get("LOCALAPPDATA"))) |base| return try std.fs.path.join(gpa, &.{ base, "pardes" }); if (nonEmpty(env.get("USERPROFILE"))) |home| return try std.fs.path.join(gpa, &.{ home, "AppData", "Local", "pardes" }); return null; } if (xdgBase(env)) |base| return try std.fs.path.join(gpa, &.{ base, "pardes" }); const home = nonEmpty(env.get("HOME")) orelse return null; if (builtin.os.tag == .macos) return try std.fs.path.join(gpa, &.{ home, "Library", "Application Support", "pardes" }); return try std.fs.path.join(gpa, &.{ home, ".config", "pardes" }); } /// Missing, unreadable, oversized, or otherwise unusable config is simply no /// config. The arena passed by main owns successful bytes for the process. pub fn load( io: std.Io, gpa: std.mem.Allocator, env: *const std.process.Environ.Map, ) ?[]u8 { const config_path = path(gpa, env) catch return null; defer if (config_path) |p| gpa.free(p); const p = config_path orelse return null; return std.Io.Dir.cwd().readFileAlloc(io, p, gpa, .limited(max_bytes)) catch null; } fn nonEmpty(value: ?[]const u8) ?[]const u8 { const v = value orelse return null; return if (v.len == 0) null else v; } fn xdgBase(env: *const std.process.Environ.Map) ?[]const u8 { const value = nonEmpty(env.get("XDG_CONFIG_HOME")) orelse return null; return if (std.fs.path.isAbsolute(value)) value else null; } test "config path honors XDG and rejects a relative XDG directory" { if (builtin.os.tag == .windows) return; var env: std.process.Environ.Map = .init(std.testing.allocator); defer env.deinit(); try env.put("HOME", "/home/pardes-test"); try env.put("XDG_CONFIG_HOME", "/var/tmp/pardes-xdg"); const xdg = (try path(std.testing.allocator, &env)).?; defer std.testing.allocator.free(xdg); try std.testing.expectEqualStrings("/var/tmp/pardes-xdg/pardes", xdg); try env.put("XDG_CONFIG_HOME", "relative/config"); const fallback = (try path(std.testing.allocator, &env)).?; defer std.testing.allocator.free(fallback); const expected = if (builtin.os.tag == .macos) "/home/pardes-test/Library/Application Support/pardes" else "/home/pardes-test/.config/pardes"; try std.testing.expectEqualStrings(expected, fallback); } test "config loader is silent when missing and returns exact file bytes" { if (builtin.os.tag == .windows) return; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var base_buf: [std.fs.max_path_bytes]u8 = undefined; const base_len = try tmp.dir.realPath(std.testing.io, &base_buf); var env: std.process.Environ.Map = .init(std.testing.allocator); defer env.deinit(); try env.put("XDG_CONFIG_HOME", base_buf[0..base_len]); try std.testing.expect(load(std.testing.io, std.testing.allocator, &env) == null); const source = "Theme dark\nUnknown command\nTheme acme\n"; try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "pardes", .data = source }); const bytes = load(std.testing.io, std.testing.allocator, &env).?; defer std.testing.allocator.free(bytes); try std.testing.expectEqualStrings(source, bytes); }