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()); }