mirror of
https://github.com/DeBrosOfficial/orama.git
synced 2026-06-17 02:14:12 +00:00
217 lines
8.0 KiB
Zig
217 lines
8.0 KiB
Zig
/// Configuration loading for vault-guardian.
|
|
/// Reads a simple key=value config file. Lines starting with '#' are comments.
|
|
const std = @import("std");
|
|
|
|
pub const Config = struct {
|
|
/// Address to bind client-facing server
|
|
listen_address: []const u8 = "0.0.0.0",
|
|
/// Client-facing port (TLS in production, plain TCP for MVP)
|
|
client_port: u16 = 7500,
|
|
/// Guardian-to-guardian port (WireGuard-only interface)
|
|
peer_port: u16 = 7501,
|
|
/// Data storage directory
|
|
data_dir: []const u8 = "/opt/orama/.orama/data/vault",
|
|
/// RQLite endpoint for node discovery
|
|
rqlite_url: []const u8 = "http://127.0.0.1:4001",
|
|
|
|
/// Arena allocator that owns duped strings (null when using defaults)
|
|
_arena: ?std.heap.ArenaAllocator = null,
|
|
|
|
/// Free all allocated memory.
|
|
pub fn deinit(self: *Config) void {
|
|
if (self._arena) |*arena| {
|
|
arena.deinit();
|
|
}
|
|
self.* = undefined;
|
|
}
|
|
};
|
|
|
|
pub const ConfigError = error{
|
|
InvalidPort,
|
|
LineTooLong,
|
|
};
|
|
|
|
/// Loads config from file, or returns defaults if file doesn't exist.
|
|
pub fn loadOrDefault(allocator: std.mem.Allocator, path: []const u8) !Config {
|
|
// Read whole file into memory (config files are small)
|
|
const contents = std.fs.cwd().readFileAlloc(allocator, path, 64 * 1024) catch |err| {
|
|
if (err == error.FileNotFound) {
|
|
return Config{};
|
|
}
|
|
return err;
|
|
};
|
|
defer allocator.free(contents);
|
|
|
|
var cfg = Config{};
|
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
|
errdefer arena.deinit();
|
|
const arena_alloc = arena.allocator();
|
|
|
|
var line_iter = std.mem.splitScalar(u8, contents, '\n');
|
|
while (line_iter.next()) |raw_line| {
|
|
// Strip trailing \r for Windows line endings
|
|
const line_trimmed = if (raw_line.len > 0 and raw_line[raw_line.len - 1] == '\r')
|
|
raw_line[0 .. raw_line.len - 1]
|
|
else
|
|
raw_line;
|
|
|
|
const line = std.mem.trim(u8, line_trimmed, " \t");
|
|
|
|
// Skip empty lines and comments
|
|
if (line.len == 0) continue;
|
|
if (line[0] == '#') continue;
|
|
|
|
// Find '='
|
|
const eq_idx = std.mem.indexOfScalar(u8, line, '=') orelse continue;
|
|
const key = std.mem.trim(u8, line[0..eq_idx], " \t");
|
|
const value = std.mem.trim(u8, line[eq_idx + 1 ..], " \t");
|
|
|
|
if (std.mem.eql(u8, key, "listen_address")) {
|
|
cfg.listen_address = try arena_alloc.dupe(u8, value);
|
|
} else if (std.mem.eql(u8, key, "client_port")) {
|
|
cfg.client_port = std.fmt.parseInt(u16, value, 10) catch return ConfigError.InvalidPort;
|
|
} else if (std.mem.eql(u8, key, "peer_port")) {
|
|
cfg.peer_port = std.fmt.parseInt(u16, value, 10) catch return ConfigError.InvalidPort;
|
|
} else if (std.mem.eql(u8, key, "data_dir")) {
|
|
cfg.data_dir = try arena_alloc.dupe(u8, value);
|
|
} else if (std.mem.eql(u8, key, "rqlite_url")) {
|
|
cfg.rqlite_url = try arena_alloc.dupe(u8, value);
|
|
}
|
|
// Unknown keys are silently ignored
|
|
}
|
|
|
|
cfg._arena = arena;
|
|
return cfg;
|
|
}
|
|
|
|
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
|
|
test "config: defaults when file not found" {
|
|
var cfg = try loadOrDefault(std.testing.allocator, "/tmp/nonexistent-vault-config-file-xyz");
|
|
defer cfg.deinit();
|
|
|
|
try std.testing.expectEqualSlices(u8, "0.0.0.0", cfg.listen_address);
|
|
try std.testing.expectEqual(@as(u16, 7500), cfg.client_port);
|
|
try std.testing.expectEqual(@as(u16, 7501), cfg.peer_port);
|
|
try std.testing.expectEqualSlices(u8, "/opt/orama/.orama/data/vault", cfg.data_dir);
|
|
try std.testing.expectEqualSlices(u8, "http://127.0.0.1:4001", cfg.rqlite_url);
|
|
}
|
|
|
|
test "config: parse key=value file" {
|
|
// Write a temp config file
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
|
|
const file = try tmp_dir.dir.createFile("test.conf", .{});
|
|
try file.writeAll(
|
|
\\# This is a comment
|
|
\\listen_address = 127.0.0.1
|
|
\\client_port = 8080
|
|
\\peer_port = 8081
|
|
\\data_dir = /tmp/vault-test
|
|
\\rqlite_url = http://10.0.0.1:4001
|
|
\\
|
|
\\# Another comment
|
|
\\unknown_key = ignored
|
|
\\
|
|
);
|
|
file.close();
|
|
|
|
// Get the full path
|
|
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const dir_path = try tmp_dir.dir.realpath(".", &path_buf);
|
|
var full_path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const full_path = try std.fmt.bufPrint(&full_path_buf, "{s}/test.conf", .{dir_path});
|
|
|
|
var cfg = try loadOrDefault(std.testing.allocator, full_path);
|
|
defer cfg.deinit();
|
|
|
|
try std.testing.expectEqualSlices(u8, "127.0.0.1", cfg.listen_address);
|
|
try std.testing.expectEqual(@as(u16, 8080), cfg.client_port);
|
|
try std.testing.expectEqual(@as(u16, 8081), cfg.peer_port);
|
|
try std.testing.expectEqualSlices(u8, "/tmp/vault-test", cfg.data_dir);
|
|
try std.testing.expectEqualSlices(u8, "http://10.0.0.1:4001", cfg.rqlite_url);
|
|
}
|
|
|
|
test "config: partial config uses defaults for missing keys" {
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
|
|
const file = try tmp_dir.dir.createFile("partial.conf", .{});
|
|
try file.writeAll("client_port = 9000\n");
|
|
file.close();
|
|
|
|
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const dir_path = try tmp_dir.dir.realpath(".", &path_buf);
|
|
var full_path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const full_path = try std.fmt.bufPrint(&full_path_buf, "{s}/partial.conf", .{dir_path});
|
|
|
|
var cfg = try loadOrDefault(std.testing.allocator, full_path);
|
|
defer cfg.deinit();
|
|
|
|
try std.testing.expectEqual(@as(u16, 9000), cfg.client_port);
|
|
// Defaults for everything else
|
|
try std.testing.expectEqualSlices(u8, "0.0.0.0", cfg.listen_address);
|
|
try std.testing.expectEqual(@as(u16, 7501), cfg.peer_port);
|
|
}
|
|
|
|
test "config: invalid port returns error" {
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
|
|
const file = try tmp_dir.dir.createFile("bad.conf", .{});
|
|
try file.writeAll("client_port = not_a_number\n");
|
|
file.close();
|
|
|
|
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const dir_path = try tmp_dir.dir.realpath(".", &path_buf);
|
|
var full_path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const full_path = try std.fmt.bufPrint(&full_path_buf, "{s}/bad.conf", .{dir_path});
|
|
|
|
const result = loadOrDefault(std.testing.allocator, full_path);
|
|
try std.testing.expectError(ConfigError.InvalidPort, result);
|
|
}
|
|
|
|
test "config: empty file returns defaults" {
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
|
|
const file = try tmp_dir.dir.createFile("empty.conf", .{});
|
|
file.close();
|
|
|
|
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const dir_path = try tmp_dir.dir.realpath(".", &path_buf);
|
|
var full_path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const full_path = try std.fmt.bufPrint(&full_path_buf, "{s}/empty.conf", .{dir_path});
|
|
|
|
var cfg = try loadOrDefault(std.testing.allocator, full_path);
|
|
defer cfg.deinit();
|
|
|
|
try std.testing.expectEqual(@as(u16, 7500), cfg.client_port);
|
|
}
|
|
|
|
test "config: comments and blank lines are skipped" {
|
|
var tmp_dir = std.testing.tmpDir(.{});
|
|
defer tmp_dir.cleanup();
|
|
|
|
const file = try tmp_dir.dir.createFile("comments.conf", .{});
|
|
try file.writeAll(
|
|
\\# Full line comment
|
|
\\
|
|
\\ # Indented comment
|
|
\\client_port = 1234
|
|
\\
|
|
);
|
|
file.close();
|
|
|
|
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const dir_path = try tmp_dir.dir.realpath(".", &path_buf);
|
|
var full_path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
|
const full_path = try std.fmt.bufPrint(&full_path_buf, "{s}/comments.conf", .{dir_path});
|
|
|
|
var cfg = try loadOrDefault(std.testing.allocator, full_path);
|
|
defer cfg.deinit();
|
|
|
|
try std.testing.expectEqual(@as(u16, 1234), cfg.client_port);
|
|
}
|