aboutsummaryrefslogtreecommitdiff
path: root/src/conf.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf.zig')
-rw-r--r--src/conf.zig382
1 files changed, 382 insertions, 0 deletions
diff --git a/src/conf.zig b/src/conf.zig
new file mode 100644
index 0000000..419c369
--- /dev/null
+++ b/src/conf.zig
@@ -0,0 +1,382 @@
+const std = @import("std");
+const assert = std.debug.assert;
+
+pub const Var = struct {
+ key: []const u8,
+ val: []const u8,
+
+ pub fn format(self: Var, w: *std.io.Writer) !void {
+ const spacebuf = [_]u8{' '} ** 32;
+ const equal_sign = " = ";
+
+ try w.writeAll(self.key);
+ try w.writeAll(equal_sign);
+ if (std.mem.indexOfScalar(u8, self.val, '\n')) |eol0| {
+ try w.writeAll(self.val[0 .. eol0 + 1]);
+ var val = self.val[eol0 + 1 ..];
+ while (val.len > 0) {
+ const eol = std.mem.indexOfScalar(u8, val, '\n') orelse val.len - 1;
+ const line = val[0 .. eol + 1];
+ var spaces = self.key.len;
+ while (spaces > 0) {
+ const n = @min(spaces, spacebuf.len);
+ try w.writeAll(spacebuf[0..n]);
+ spaces -= n;
+ }
+ try w.writeAll(equal_sign);
+ try w.writeAll(line);
+ val = val[eol + 1 ..];
+ }
+ } else {
+ try w.writeAll(self.val);
+ }
+ }
+};
+
+const Var_Header = packed struct(u64) {
+ key_len: u32,
+ val_len: u32,
+
+ fn encode(self: Var_Header) [8]u8 {
+ var header: u64 = @intCast(self.key_len);
+ header <<= 32;
+ header |= @intCast(self.val_len);
+ return std.mem.toBytes(std.mem.nativeToBig(u64, header));
+ }
+
+ fn decode(buf: []const u8) Var_Header {
+ const header = std.mem.bigToNative(u64, std.mem.bytesToValue(u64, buf[0..8]));
+ return .{
+ .key_len = @intCast(header >> 32),
+ .val_len = @intCast(header & 0xffffffff),
+ };
+ }
+};
+
+pub const Iterator = struct {
+ rest: []const u8,
+
+ pub fn next(self: *Iterator) ?Var {
+ if (self.rest.len == 0) return null;
+
+ const header = Var_Header.decode(self.rest);
+ const v: Var = .{
+ .key = self.rest[@sizeOf(Var_Header) .. @sizeOf(Var_Header) + header.key_len],
+ .val = self.rest[@sizeOf(Var_Header) + header.key_len .. @sizeOf(Var_Header) + header.key_len + header.val_len],
+ };
+ self.rest = self.rest[@sizeOf(Var_Header) + v.key.len + v.val.len ..];
+ return v;
+ }
+};
+
+pub const Parse_Result = union(enum) {
+ conf: Config,
+ err: struct {
+ msg: []const u8,
+ line: usize,
+ },
+};
+
+pub const Config = struct {
+ allocator: std.mem.Allocator,
+ buf: []u8, // encoded key/value pairs
+ len: usize, // used length of `buf`
+
+ pub fn init(allocator: std.mem.Allocator) Config {
+ return .{ .allocator = allocator, .buf = &.{}, .len = 0 };
+ }
+
+ pub fn from_file(allocator: std.mem.Allocator, path: []const u8) !Parse_Result {
+ var file = std.fs.openFileAbsolute(path, .{}) catch |err| {
+ return switch (err) {
+ error.FileNotFound => .{ .conf = .init(allocator) },
+ else => err,
+ };
+ };
+ defer file.close();
+
+ var read_buf: [1024]u8 = undefined;
+ var reader = file.reader(&read_buf);
+ return parse(allocator, &reader.interface, read_buf.len);
+ }
+
+ fn parse(allocator: std.mem.Allocator, reader: *std.io.Reader, comptime max_line_len: usize) !Parse_Result {
+ const spaces = " \t\r";
+
+ var allocating: std.io.Writer.Allocating = .init(allocator);
+
+ var header: Var_Header = undefined;
+ var header_off: usize = 0;
+ var need_key = true;
+ var file_line: usize = 0;
+ while (true) {
+ file_line += 1;
+ const raw_line = reader.takeDelimiterExclusive('\n') catch |err| switch (err) {
+ error.StreamTooLong => {
+ return .{
+ .err = .{
+ .msg = std.fmt.comptimePrint("key/value line exceeds the maximum of {} bytes", .{max_line_len}),
+ .line = file_line,
+ },
+ };
+ },
+ error.EndOfStream => {
+ break;
+ },
+ else => unreachable,
+ };
+
+ const line = std.mem.trim(u8, raw_line, spaces);
+ if (line.len == 0 or line[0] == '#') {
+ // After an empty line, we consider the end of a value.
+ need_key = true;
+ continue;
+ }
+
+ const eq = std.mem.indexOfScalar(u8, line, '=') orelse {
+ return .{
+ .err = .{
+ .msg = "invalid variable definition (no '=' found)",
+ .line = file_line,
+ },
+ };
+ };
+
+ const key = std.mem.trimRight(u8, line[0..eq], spaces);
+ const val = std.mem.trimLeft(u8, line[eq + 1 ..], spaces);
+
+ if (key.len == 0) {
+ // We have a value continuation here. So we append a newline
+ // to the existing value and write the value line below.
+ if (need_key) {
+ return .{
+ .err = .{
+ .msg = "missing key name",
+ .line = file_line,
+ },
+ };
+ }
+ header.val_len += 1;
+ try allocating.writer.writeByte('\n');
+ } else {
+ // We have a new key/value pair starting here. First, we need
+ // to write the header from the previous key/value pair if there
+ // was some.
+ if (allocating.writer.end > 0) {
+ @memcpy(allocating.writer.buffer[header_off .. header_off + @sizeOf(Var_Header)], &header.encode());
+ }
+
+ header_off = allocating.writer.end;
+ header.key_len = @intCast(key.len);
+ header.val_len = 0;
+
+ try allocating.writer.writeAll(&[_]u8{0} ** @sizeOf(Var_Header));
+ try allocating.writer.writeAll(key);
+ need_key = false;
+ }
+
+ header.val_len += @intCast(val.len);
+ try allocating.writer.writeAll(val);
+ }
+
+ if (allocating.writer.end > 0) {
+ @memcpy(allocating.writer.buffer[header_off .. header_off + @sizeOf(Var_Header)], &header.encode());
+ }
+ return .{
+ .conf = .{
+ .allocator = allocator,
+ .buf = allocating.writer.buffer,
+ .len = allocating.writer.end,
+ },
+ };
+ }
+
+ pub fn deinit(self: Config) void {
+ if (self.buf.len > 0) {
+ self.allocator.free(self.buf);
+ }
+ }
+
+ pub fn iterate(self: Config) Iterator {
+ return .{ .rest = self.buf[0..self.len] };
+ }
+
+ pub fn add_var(self: *Config, key: []const u8, val: []const u8) !void {
+ const var_size = @sizeOf(Var_Header) + key.len + val.len;
+ if (self.len + var_size > self.buf.len) {
+ const new_size = @max(256, self.buf.len + @max(var_size, self.buf.len / 2));
+ self.buf = try self.allocator.realloc(self.buf, new_size);
+ }
+ assert(self.len + var_size <= self.buf.len);
+
+ const header: Var_Header = .{
+ .key_len = @intCast(key.len),
+ .val_len = @intCast(val.len),
+ };
+ @memcpy(self.buf[self.len .. self.len + 8], &header.encode());
+ self.len += 8;
+ @memcpy(self.buf[self.len .. self.len + key.len], key);
+ self.len += key.len;
+ @memcpy(self.buf[self.len .. self.len + val.len], val);
+ self.len += val.len;
+ }
+
+ /// Find a variable by its key and returns the first variable that
+ /// is found. If the key cannot be found, null will be returned.
+ pub fn find_var(self: Config, key: []const u8) ?Var {
+ var iter = self.iterate();
+ while (iter.next()) |v| {
+ if (std.mem.eql(u8, v.key, key)) {
+ return v;
+ }
+ }
+ return null;
+ }
+};
+
+test "parse config - empty" {
+ var input = std.io.Reader.fixed("");
+ var buf: [32]u8 = undefined;
+ var reader = std.io.Reader.limited(&input, std.io.Limit.limited(input.buffer.len), &buf);
+
+ var result = try Config.parse(std.testing.allocator, &reader.interface, buf.len);
+ try std.testing.expect(result == .conf);
+ try std.testing.expectEqualSlices(u8, "", result.conf.buf[0..result.conf.len]);
+ defer result.conf.deinit();
+
+ var vars = result.conf.iterate();
+ try std.testing.expect(vars.next() == null);
+}
+
+test "parse config - singe line" {
+ var input = std.io.Reader.fixed("foo one = bar one");
+ var buf: [32]u8 = undefined;
+ var reader = std.io.Reader.limited(&input, std.io.Limit.limited(input.buffer.len), &buf);
+
+ var result = try Config.parse(std.testing.allocator, &reader.interface, buf.len);
+ try std.testing.expect(result == .conf);
+ try std.testing.expectEqualSlices(
+ u8,
+ "\x00\x00\x00\x07\x00\x00\x00\x07foo onebar one",
+ result.conf.buf[0..result.conf.len],
+ );
+ defer result.conf.deinit();
+
+ var vars = result.conf.iterate();
+
+ const var_one = vars.next() orelse return error.missing_var_one;
+ try std.testing.expectEqualSlices(u8, "foo one", var_one.key);
+ try std.testing.expectEqualSlices(u8, "bar one", var_one.val);
+
+ try std.testing.expectEqual(@as(?Var, null), vars.next());
+}
+
+test "parse config - mixed" {
+ var input = std.io.Reader.fixed(
+ \\foo_one = bar one
+ \\foo_two = bar two 1
+ \\ = bar two 2
+ \\ =
+ \\ = bar two 4
+ \\
+ \\foo_three =
+ \\ = bar three 2
+ \\ = bar three 3
+ \\
+ \\
+ \\
+ \\foo_four = bar four 1
+ \\ = bar four 2
+ \\ = bar four 3
+ \\ =
+ \\
+ \\foo_five =
+ \\
+ );
+ var buf: [32]u8 = undefined;
+ var reader = std.io.Reader.limited(&input, std.io.Limit.limited(input.buffer.len), &buf);
+
+ var result = try Config.parse(std.testing.allocator, &reader.interface, buf.len);
+ try std.testing.expect(result == .conf);
+ try std.testing.expectEqualSlices(
+ u8,
+ "" ++
+ "\x00\x00\x00\x07\x00\x00\x00\x07foo_onebar one" ++
+ "\x00\x00\x00\x07\x00\x00\x00\x1efoo_twobar two 1\nbar two 2\n\nbar two 4" ++
+ "\x00\x00\x00\x09\x00\x00\x00\x18foo_three\nbar three 2\nbar three 3" ++
+ "\x00\x00\x00\x08\x00\x00\x00\x21foo_fourbar four 1\nbar four 2\nbar four 3\n" ++
+ "\x00\x00\x00\x08\x00\x00\x00\x00foo_five",
+ result.conf.buf[0..result.conf.len],
+ );
+ defer result.conf.deinit();
+
+ var vars = result.conf.iterate();
+
+ const var_one = vars.next() orelse return error.missing_var_one;
+ try std.testing.expectEqualSlices(u8, "foo_one", var_one.key);
+ try std.testing.expectEqualSlices(u8, "bar one", var_one.val);
+
+ const var_two = vars.next() orelse return error.missing_var_two;
+ try std.testing.expectEqualSlices(u8, "foo_two", var_two.key);
+ try std.testing.expectEqualSlices(u8, "bar two 1\nbar two 2\n\nbar two 4", var_two.val);
+
+ const var_three = vars.next() orelse return error.missing_var_three;
+ try std.testing.expectEqualSlices(u8, "foo_three", var_three.key);
+ try std.testing.expectEqualSlices(u8, "\nbar three 2\nbar three 3", var_three.val);
+
+ const var_four = vars.next() orelse return error.missing_var_four;
+ try std.testing.expectEqualSlices(u8, "foo_four", var_four.key);
+ try std.testing.expectEqualSlices(u8, "bar four 1\nbar four 2\nbar four 3\n", var_four.val);
+
+ const var_five = vars.next() orelse return error.missing_var_five;
+ try std.testing.expectEqualSlices(u8, "foo_five", var_five.key);
+ try std.testing.expectEqualSlices(u8, "", var_five.val);
+
+ try std.testing.expectEqual(@as(?Var, null), vars.next());
+}
+
+test "parse config - with comment" {
+ var input = std.io.Reader.fixed(
+ \\# first var:
+ \\foo_one = bar one
+ \\# second var:
+ \\foo_two = bar two 1
+ \\ = bar two 2
+ \\ =
+ \\ = bar two 4
+ \\ # third var:
+ \\foo_three =
+ \\ = bar three 2
+ \\ = bar three 3
+ );
+ var buf: [32]u8 = undefined;
+ var reader = std.io.Reader.limited(&input, std.io.Limit.limited(input.buffer.len), &buf);
+
+ var result = try Config.parse(std.testing.allocator, &reader.interface, buf.len);
+ try std.testing.expect(result == .conf);
+ try std.testing.expectEqualSlices(
+ u8,
+ "" ++
+ "\x00\x00\x00\x07\x00\x00\x00\x07foo_onebar one" ++
+ "\x00\x00\x00\x07\x00\x00\x00\x1efoo_twobar two 1\nbar two 2\n\nbar two 4" ++
+ "\x00\x00\x00\x09\x00\x00\x00\x18foo_three\nbar three 2\nbar three 3",
+ result.conf.buf[0..result.conf.len],
+ );
+ defer result.conf.deinit();
+
+ var vars = result.conf.iterate();
+
+ const var_one = vars.next() orelse return error.missing_var_one;
+ try std.testing.expectEqualSlices(u8, "foo_one", var_one.key);
+ try std.testing.expectEqualSlices(u8, "bar one", var_one.val);
+
+ const var_two = vars.next() orelse return error.missing_var_two;
+ try std.testing.expectEqualSlices(u8, "foo_two", var_two.key);
+ try std.testing.expectEqualSlices(u8, "bar two 1\nbar two 2\n\nbar two 4", var_two.val);
+
+ const var_three = vars.next() orelse return error.missing_var_three;
+ try std.testing.expectEqualSlices(u8, "foo_three", var_three.key);
+ try std.testing.expectEqualSlices(u8, "\nbar three 2\nbar three 3", var_three.val);
+
+ try std.testing.expectEqual(@as(?Var, null), vars.next());
+}