diff options
Diffstat (limited to 'src/conf.zig')
| -rw-r--r-- | src/conf.zig | 382 |
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()); +} |