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 \ 2b11e8fef86d0eefb225156e695be1c1d5c35cbcmaster
commit
55531668eb
@ -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());
|
||||||
|
}
|
Reference in New Issue