lib: add simple ini file format parser library

will be used to parse lnd and bitcoind config files.

    git subtree add --prefix=lib/ini --squash \
      https://github.com/ziglibs/ini \
      2b11e8fef86d0eefb225156e695be1c1d5c35cbc
master
alex 9 months ago
commit 55531668eb
Signed by: x1ddos
GPG Key ID: FDEFB4A63CBD8460

@ -0,0 +1,2 @@
*.zig text=auto eol=lf
*.zig text=auto eol=lf

@ -0,0 +1 @@
github: MasterQ32

@ -0,0 +1,2 @@
zig-cache/
zig-out/

@ -0,0 +1,19 @@
Copyright (c) 2021 Felix "xq" Queißner
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
OR OTHER DEALINGS IN THE SOFTWARE.

@ -0,0 +1,81 @@
# INI parser library
This is a very simple ini-parser library that provides:
- Raw record reading
- Leading/trailing whitespace removal
- comments based on `;` and `#`
- Zig API
- C API
## Usage example
### Zig
```zig
const std = @import("std");
const ini = @import("ini");
pub fn main() !void {
const file = try std.fs.cwd().openFile("example.ini", .{});
defer file.close();
var parser = ini.parse(std.testing.allocator, file.reader());
defer parser.deinit();
var writer = std.io.getStdOut().writer();
while (try parser.next()) |record| {
switch (record) {
.section => |heading| try writer.print("[{s}]\n", .{heading}),
.property => |kv| try writer.print("{s} = {s}\n", .{ kv.key, kv.value }),
.enumeration => |value| try writer.print("{s}\n", .{value}),
}
}
}
```
### C
```c
#include <ini.h>
#include <stdio.h>
#include <stdbool.h>
int main() {
FILE * f = fopen("example.ini", "rb");
if(!f)
return 1;
struct ini_Parser parser;
ini_create_file(&parser, f);
struct ini_Record record;
while(true)
{
enum ini_Error error = ini_next(&parser, &record);
if(error != INI_SUCCESS)
goto cleanup;
switch(record.type) {
case INI_RECORD_NUL: goto done;
case INI_RECORD_SECTION:
printf("[%s]\n", record.section);
break;
case INI_RECORD_PROPERTY:
printf("%s = %s\n", record.property.key, record.property.value);
break;
case INI_RECORD_ENUMERATION:
printf("%s\n", record.enumeration);
break;
}
}
done:
cleanup:
ini_destroy(&parser);
fclose(f);
return 0;
}
```

@ -0,0 +1,69 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
_ = b.addModule("ini", .{
.source_file = .{
.path = "src/ini.zig",
},
});
const lib = b.addStaticLibrary(.{
.name = "ini",
.root_source_file = .{ .path = "src/lib.zig" },
.target = b.standardTargetOptions(.{}),
.optimize = optimize,
});
lib.bundle_compiler_rt = true;
lib.addIncludePath(.{ .path = "src" });
lib.linkLibC();
b.installArtifact(lib);
const example_c = b.addExecutable(.{
.name = "example-c",
.optimize = optimize,
});
example_c.addCSourceFile(.{
.file = .{
.path = "example/example.c",
},
.flags = &.{
"-Wall",
"-Wextra",
"-pedantic",
},
});
example_c.addIncludePath(.{ .path = "src" });
example_c.linkLibrary(lib);
example_c.linkLibC();
b.installArtifact(example_c);
const example_zig = b.addExecutable(.{
.name = "example-zig",
.root_source_file = .{ .path = "example/example.zig" },
.optimize = optimize,
});
example_zig.addModule("ini", b.modules.get("ini").?);
b.installArtifact(example_zig);
var main_tests = b.addTest(.{
.root_source_file = .{ .path = "src/test.zig" },
.optimize = optimize,
});
var binding_tests = b.addTest(.{
.root_source_file = .{ .path = "src/lib-test.zig" },
.optimize = optimize,
});
binding_tests.addIncludePath(.{ .path = "src" });
binding_tests.linkLibrary(lib);
binding_tests.linkLibC();
const test_step = b.step("test", "Run library tests");
test_step.dependOn(&main_tests.step);
test_step.dependOn(&binding_tests.step);
}

@ -0,0 +1,41 @@
#include <ini.h>
#include <stdio.h>
#include <stdbool.h>
int main() {
FILE * f = fopen("example.ini", "rb");
if(!f)
return 1;
struct ini_Parser parser;
ini_create_file(&parser, f);
struct ini_Record record;
while(true)
{
enum ini_Error error = ini_next(&parser, &record);
if(error != INI_SUCCESS)
goto cleanup;
switch(record.type) {
case INI_RECORD_NUL: goto done;
case INI_RECORD_SECTION:
printf("[%s]\n", record.section);
break;
case INI_RECORD_PROPERTY:
printf("%s = %s\n", record.property.key, record.property.value);
break;
case INI_RECORD_ENUMERATION:
printf("%s\n", record.enumeration);
break;
}
}
done:
cleanup:
ini_destroy(&parser);
fclose(f);
return 0;
}

@ -0,0 +1,9 @@
[Meta]
author = xq
library = ini
[Albums]
Thriller
Back in Black
Bat Out of Hell
The Dark Side of the Moon

@ -0,0 +1,22 @@
const std = @import("std");
const ini = @import("ini");
pub fn main() !void {
const file = try std.fs.cwd().openFile("example.ini", .{});
defer file.close();
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() != .ok) @panic("memory leaked");
var parser = ini.parse(gpa.allocator(), file.reader());
defer parser.deinit();
var writer = std.io.getStdOut().writer();
while (try parser.next()) |record| {
switch (record) {
.section => |heading| try writer.print("[{s}]\n", .{heading}),
.property => |kv| try writer.print("{s} = {s}\n", .{ kv.key, kv.value }),
.enumeration => |value| try writer.print("{s}\n", .{value}),
}
}
}

@ -0,0 +1,64 @@
#ifndef ZIG_INI_H
#define ZIG_INI_H
#include <stddef.h>
#include <stdio.h>
#include <stdalign.h>
/// Opaque parser type. Consider the bytes in this struct
/// as "implementation defined".
/// This must also be fixed memory and must not be copied
/// after being initialized with `ini_create_*`!
struct ini_Parser
{
alignas(16) char opaque[128];
};
enum ini_RecordType : int
{
INI_RECORD_NUL = 0,
INI_RECORD_SECTION = 1,
INI_RECORD_PROPERTY = 2,
INI_RECORD_ENUMERATION = 3,
};
struct ini_KeyValuePair
{
char const * key;
char const * value;
};
struct ini_Record
{
enum ini_RecordType type;
union {
char const * section;
struct ini_KeyValuePair property;
char const * enumeration;
};
};
enum ini_Error
{
INI_SUCCESS = 0,
INI_ERR_OUT_OF_MEMORY = 1,
INI_ERR_IO = 2,
INI_ERR_INVALID_DATA = 3,
};
extern void ini_create_buffer(
struct ini_Parser * parser,
char const * data,
size_t length
);
extern void ini_create_file(
struct ini_Parser * parser,
FILE * file
);
extern void ini_destroy(struct ini_Parser * parser);
extern enum ini_Error ini_next(struct ini_Parser * parser, struct ini_Record * record);
#endif

@ -0,0 +1,93 @@
const std = @import("std");
/// An entry in a ini file. Each line that contains non-whitespace text can
/// be categorized into a record type.
pub const Record = union(enum) {
/// A section heading enclosed in `[` and `]`. The brackets are not included.
section: [:0]const u8,
/// A line that contains a key-value pair separated by `=`.
/// Both key and value have the excess whitespace trimmed.
/// Both key and value allow escaping with C string syntax.
property: KeyValue,
/// A line that is either escaped as a C string or contains no `=`
enumeration: [:0]const u8,
};
pub const KeyValue = struct {
key: [:0]const u8,
value: [:0]const u8,
};
const whitespace = " \r\t\x00";
/// WARNING:
/// This function is not a general purpose function but
/// requires to be executed on slices of the line_buffer *after*
/// the NUL terminator appendix.
/// This function will override the character after the slice end,
/// so make sure there is one available!
fn insertNulTerminator(slice: []const u8) [:0]const u8 {
const mut_ptr = @as([*]u8, @ptrFromInt(@intFromPtr(slice.ptr)));
mut_ptr[slice.len] = 0;
return mut_ptr[0..slice.len :0];
}
pub fn Parser(comptime Reader: type) type {
return struct {
const Self = @This();
line_buffer: std.ArrayList(u8),
reader: Reader,
pub fn deinit(self: *Self) void {
self.line_buffer.deinit();
self.* = undefined;
}
pub fn next(self: *Self) !?Record {
while (true) {
self.reader.readUntilDelimiterArrayList(&self.line_buffer, '\n', 4096) catch |err| switch (err) {
error.EndOfStream => {
if (self.line_buffer.items.len == 0)
return null;
},
else => |e| return e,
};
try self.line_buffer.append(0); // append guaranteed space for sentinel
const line = if (std.mem.indexOfAny(u8, self.line_buffer.items, ";#")) |index|
std.mem.trim(u8, self.line_buffer.items[0..index], whitespace)
else
std.mem.trim(u8, self.line_buffer.items, whitespace);
if (line.len == 0)
continue;
if (std.mem.startsWith(u8, line, "[") and std.mem.endsWith(u8, line, "]")) {
return Record{ .section = insertNulTerminator(line[1 .. line.len - 1]) };
}
if (std.mem.indexOfScalar(u8, line, '=')) |index| {
return Record{
.property = KeyValue{
// note: the key *might* replace the '=' in the slice with 0!
.key = insertNulTerminator(std.mem.trim(u8, line[0..index], whitespace)),
.value = insertNulTerminator(std.mem.trim(u8, line[index + 1 ..], whitespace)),
},
};
}
return Record{ .enumeration = insertNulTerminator(line) };
}
}
};
}
/// Returns a new parser that can read the ini structure
pub fn parse(allocator: std.mem.Allocator, reader: anytype) Parser(@TypeOf(reader)) {
return Parser(@TypeOf(reader)){
.line_buffer = std.ArrayList(u8).init(allocator),
.reader = reader,
};
}

@ -0,0 +1,89 @@
const std = @import("std");
const c = @cImport({
@cInclude("ini.h");
});
test "parser create/destroy" {
var buffer: c.ini_Parser = undefined;
c.ini_create_buffer(&buffer, "", 0);
c.ini_destroy(&buffer);
}
fn expectNull(record: c.ini_Record) !void {
try std.testing.expectEqual(c.INI_RECORD_NUL, record.type);
}
fn expectSection(heading: []const u8, record: c.ini_Record) !void {
try std.testing.expectEqual(c.INI_RECORD_SECTION, record.type);
try std.testing.expectEqualStrings(heading, std.mem.span(record.unnamed_0.section));
}
fn expectKeyValue(key: []const u8, value: []const u8, record: c.ini_Record) !void {
try std.testing.expectEqual(c.INI_RECORD_PROPERTY, record.type);
try std.testing.expectEqualStrings(key, std.mem.span(record.unnamed_0.property.key));
try std.testing.expectEqualStrings(value, std.mem.span(record.unnamed_0.property.value));
}
fn expectEnumeration(enumeration: []const u8, record: c.ini_Record) !void {
try std.testing.expectEqual(c.INI_RECORD_ENUMERATION, record.type);
try std.testing.expectEqualStrings(enumeration, std.mem.span(record.unnamed_0.enumeration));
}
fn parseNext(parser: *c.ini_Parser) !c.ini_Record {
var record: c.ini_Record = undefined;
const err = c.ini_next(parser, &record);
switch (err) {
c.INI_SUCCESS => return record,
c.INI_ERR_OUT_OF_MEMORY => return error.OutOfMemory,
c.INI_ERR_IO => return error.InputOutput,
c.INI_ERR_INVALID_DATA => return error.InvalidData,
else => unreachable,
}
}
fn commonTest(parser: *c.ini_Parser) !void {
try expectSection("Meta", try parseNext(parser));
try expectKeyValue("author", "xq", try parseNext(parser));
try expectKeyValue("library", "ini", try parseNext(parser));
try expectSection("Albums", try parseNext(parser));
try expectEnumeration("Thriller", try parseNext(parser));
try expectEnumeration("Back in Black", try parseNext(parser));
try expectEnumeration("Bat Out of Hell", try parseNext(parser));
try expectEnumeration("The Dark Side of the Moon", try parseNext(parser));
try expectNull(try parseNext(parser));
}
test "buffer parser" {
const slice =
\\[Meta]
\\author = xq
\\library = ini
\\
\\[Albums]
\\Thriller
\\Back in Black
\\Bat Out of Hell
\\The Dark Side of the Moon
;
var parser: c.ini_Parser = undefined;
c.ini_create_buffer(&parser, slice, slice.len);
defer c.ini_destroy(&parser);
try commonTest(&parser);
}
test "file parser" {
var file = c.fopen("example/example.ini", "rb") orelse unreachable;
defer _ = c.fclose(file);
var parser: c.ini_Parser = undefined;
c.ini_create_file(&parser, file);
defer c.ini_destroy(&parser);
try commonTest(&parser);
}

@ -0,0 +1,158 @@
const std = @import("std");
const ini = @import("ini.zig");
const c = @cImport({
@cInclude("ini.h");
});
const Record = extern struct {
type: Type,
value: Data,
const Type = enum(c.ini_RecordType) {
nul = 0,
section = 1,
property = 2,
enumeration = 3,
};
const Data = extern union {
section: [*:0]const u8,
property: KeyValuePair,
enumeration: [*:0]const u8,
};
const KeyValuePair = extern struct {
key: [*:0]const u8,
value: [*:0]const u8,
};
};
const BufferParser = struct {
stream: std.io.FixedBufferStream([]const u8),
parser: ini.Parser(std.io.FixedBufferStream([]const u8).Reader),
};
const IniParser = union(enum) {
buffer: BufferParser,
file: ini.Parser(CReader),
};
const IniError = enum(c.ini_Error) {
success = 0,
out_of_memory = 1,
io = 2,
invalid_data = 3,
};
comptime {
if (@sizeOf(c.ini_Parser) < @sizeOf(IniParser))
@compileError(std.fmt.comptimePrint("ini_Parser struct in header is too small. Please set the char array to at least {d} chars!", .{@sizeOf(IniParser)}));
if (@alignOf(c.ini_Parser) < @alignOf(IniParser))
@compileError("align mismatch: ini_Parser struct does not match IniParser");
if (@sizeOf(c.ini_Record) != @sizeOf(Record))
@compileError("size mismatch: ini_Record struct does not match Record!");
if (@alignOf(c.ini_Record) != @alignOf(Record))
@compileError("align mismatch: ini_Record struct does not match Record!");
if (@sizeOf(c.ini_KeyValuePair) != @sizeOf(Record.KeyValuePair))
@compileError("size mismatch: ini_KeyValuePair struct does not match Record.KeyValuePair!");
if (@alignOf(c.ini_KeyValuePair) != @alignOf(Record.KeyValuePair))
@compileError("align mismatch: ini_KeyValuePair struct does not match Record.KeyValuePair!");
}
export fn ini_create_buffer(parser: *IniParser, data: [*]const u8, length: usize) void {
parser.* = IniParser{
.buffer = .{
.stream = std.io.fixedBufferStream(data[0..length]),
.parser = undefined,
},
};
// this is required to have the parser store a pointer to the stream.
parser.buffer.parser = ini.parse(std.heap.c_allocator, parser.buffer.stream.reader());
}
export fn ini_create_file(parser: *IniParser, file: *std.c.FILE) void {
parser.* = IniParser{
.file = ini.parse(std.heap.c_allocator, cReader(file)),
};
}
export fn ini_destroy(parser: *IniParser) void {
switch (parser.*) {
.buffer => |*p| p.parser.deinit(),
.file => |*p| p.deinit(),
}
parser.* = undefined;
}
const ParseError = error{ OutOfMemory, StreamTooLong } || CReader.Error;
fn mapError(err: ParseError) IniError {
return switch (err) {
error.OutOfMemory => IniError.out_of_memory,
error.StreamTooLong => IniError.invalid_data,
else => IniError.io,
};
}
export fn ini_next(parser: *IniParser, record: *Record) IniError {
const src_record_or_null: ?ini.Record = switch (parser.*) {
.buffer => |*p| p.parser.next() catch |e| return mapError(e),
.file => |*p| p.next() catch |e| return mapError(e),
};
if (src_record_or_null) |src_record| {
record.* = switch (src_record) {
.section => |heading| Record{
.type = .section,
.value = .{ .section = heading.ptr },
},
.enumeration => |enumeration| Record{
.type = .enumeration,
.value = .{ .enumeration = enumeration.ptr },
},
.property => |property| Record{
.type = .property,
.value = .{ .property = .{
.key = property.key.ptr,
.value = property.value.ptr,
} },
},
};
} else {
record.* = Record{
.type = .nul,
.value = undefined,
};
}
return .success;
}
const CReader = std.io.Reader(*std.c.FILE, std.fs.File.ReadError, cReaderRead);
fn cReader(c_file: *std.c.FILE) CReader {
return .{ .context = c_file };
}
fn cReaderRead(c_file: *std.c.FILE, bytes: []u8) std.fs.File.ReadError!usize {
const amt_read = std.c.fread(bytes.ptr, 1, bytes.len, c_file);
if (amt_read >= 0) return amt_read;
switch (@as(std.os.E, @enumFromInt(std.c._errno().*))) {
.SUCCESS => unreachable,
.INVAL => unreachable,
.FAULT => unreachable,
.AGAIN => unreachable, // this is a blocking API
.BADF => unreachable, // always a race condition
.DESTADDRREQ => unreachable, // connect was never called
.DQUOT => return error.DiskQuota,
.FBIG => return error.FileTooBig,
.IO => return error.InputOutput,
.NOSPC => return error.NoSpaceLeft,
.PERM => return error.AccessDenied,
.PIPE => return error.BrokenPipe,
else => |err| return std.os.unexpectedErrno(err),
}
}

@ -0,0 +1,183 @@
const std = @import("std");
const ini = @import("ini.zig");
const parse = ini.parse;
const Record = ini.Record;
fn expectNull(record: ?Record) !void {
try std.testing.expectEqual(@as(?Record, null), record);
}
fn expectSection(heading: []const u8, record: ?Record) !void {
try std.testing.expectEqualStrings(heading, record.?.section);
}
fn expectKeyValue(key: []const u8, value: []const u8, record: ?Record) !void {
try std.testing.expectEqualStrings(key, record.?.property.key);
try std.testing.expectEqualStrings(value, record.?.property.value);
}
fn expectEnumeration(enumeration: []const u8, record: ?Record) !void {
try std.testing.expectEqualStrings(enumeration, record.?.enumeration);
}
test "empty file" {
var stream = std.io.fixedBufferStream("");
var parser = parse(std.testing.allocator, stream.reader());
defer parser.deinit();
try expectNull(try parser.next());
try expectNull(try parser.next());
try expectNull(try parser.next());
try expectNull(try parser.next());
}
test "section" {
var stream = std.io.fixedBufferStream("[Hello]");
var parser = parse(std.testing.allocator, stream.reader());
defer parser.deinit();
try expectSection("Hello", try parser.next());
try expectNull(try parser.next());
}
test "key-value-pair" {
for (&[_][]const u8{
"key=value",
" key=value",
"key=value ",
" key=value ",
"key =value",
" key =value",
"key =value ",
" key =value ",
"key= value",
" key= value",
"key= value ",
" key= value ",
"key = value",
" key = value",
"key = value ",
" key = value ",
}) |pattern| {
var stream = std.io.fixedBufferStream(pattern);
var parser = parse(std.testing.allocator, stream.reader());
defer parser.deinit();
try expectKeyValue("key", "value", try parser.next());
try expectNull(try parser.next());
}
}
test "enumeration" {
var stream = std.io.fixedBufferStream("enum");
var parser = parse(std.testing.allocator, stream.reader());
defer parser.deinit();
try expectEnumeration("enum", try parser.next());
try expectNull(try parser.next());
}
test "empty line skipping" {
var stream = std.io.fixedBufferStream("item a\r\n\r\n\r\nitem b");
var parser = parse(std.testing.allocator, stream.reader());
defer parser.deinit();
try expectEnumeration("item a", try parser.next());
try expectEnumeration("item b", try parser.next());
try expectNull(try parser.next());
}
test "multiple sections" {
var stream = std.io.fixedBufferStream(" [Hello] \r\n[Foo Bar]\n[Hello!]\n");
var parser = parse(std.testing.allocator, stream.reader());
defer parser.deinit();
try expectSection("Hello", try parser.next());
try expectSection("Foo Bar", try parser.next());
try expectSection("Hello!", try parser.next());
try expectNull(try parser.next());
}
test "multiple properties" {
var stream = std.io.fixedBufferStream("a = b\r\nc =\r\nkey value = core property");
var parser = parse(std.testing.allocator, stream.reader());
defer parser.deinit();
try expectKeyValue("a", "b", try parser.next());
try expectKeyValue("c", "", try parser.next());
try expectKeyValue("key value", "core property", try parser.next());
try expectNull(try parser.next());
}
test "multiple enumeration" {
var stream = std.io.fixedBufferStream(" a \n b \r\n c ");
var parser = parse(std.testing.allocator, stream.reader());
defer parser.deinit();
try expectEnumeration("a", try parser.next());
try expectEnumeration("b", try parser.next());
try expectEnumeration("c", try parser.next());
try expectNull(try parser.next());
}
test "mixed data" {
var stream = std.io.fixedBufferStream(
\\[Meta]
\\author = xq
\\library = ini
\\
\\[Albums]
\\Thriller
\\Back in Black
\\Bat Out of Hell
\\The Dark Side of the Moon
);
var parser = parse(std.testing.allocator, stream.reader());
defer parser.deinit();
try expectSection("Meta", try parser.next());
try expectKeyValue("author", "xq", try parser.next());
try expectKeyValue("library", "ini", try parser.next());
try expectSection("Albums", try parser.next());
try expectEnumeration("Thriller", try parser.next());
try expectEnumeration("Back in Black", try parser.next());
try expectEnumeration("Bat Out of Hell", try parser.next());
try expectEnumeration("The Dark Side of the Moon", try parser.next());
try expectNull(try parser.next());
}
test "# comments" {
var stream = std.io.fixedBufferStream(
\\[section] # comment
\\key = value # comment
\\enum # comment
);
var parser = parse(std.testing.allocator, stream.reader());
defer parser.deinit();
try expectSection("section", try parser.next());
try expectKeyValue("key", "value", try parser.next());
try expectEnumeration("enum", try parser.next());
try expectNull(try parser.next());
}
test "; comments" {
var stream = std.io.fixedBufferStream(
\\[section] ; comment
\\key = value ; comment
\\enum ; comment
);
var parser = parse(std.testing.allocator, stream.reader());
defer parser.deinit();
try expectSection("section", try parser.next());
try expectKeyValue("key", "value", try parser.next());
try expectEnumeration("enum", try parser.next());
try expectNull(try parser.next());
}