From 15a2b6d823e4ccb15dac98d31bf888d4e3cc4da9 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 5 Dec 2023 17:13:43 +0100 Subject: [PATCH] Squashed 'lib/ini/' content from commit 2b11e8fe git-subtree-dir: lib/ini git-subtree-split: 2b11e8fef86d0eefb225156e695be1c1d5c35cbc --- .gitattributes | 2 + .github/FUNDING.yml | 1 + .gitignore | 2 + LICENCE | 19 +++++ README.md | 81 ++++++++++++++++++++ build.zig | 69 +++++++++++++++++ example/example.c | 41 ++++++++++ example/example.ini | 9 +++ example/example.zig | 22 ++++++ src/ini.h | 64 ++++++++++++++++ src/ini.zig | 93 ++++++++++++++++++++++ src/lib-test.zig | 89 +++++++++++++++++++++ src/lib.zig | 158 ++++++++++++++++++++++++++++++++++++++ src/test.zig | 183 ++++++++++++++++++++++++++++++++++++++++++++ 14 files changed, 833 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100644 .gitignore create mode 100644 LICENCE create mode 100644 README.md create mode 100644 build.zig create mode 100644 example/example.c create mode 100644 example/example.ini create mode 100644 example/example.zig create mode 100644 src/ini.h create mode 100644 src/ini.zig create mode 100644 src/lib-test.zig create mode 100644 src/lib.zig create mode 100644 src/test.zig diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1e97822 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.zig text=auto eol=lf +*.zig text=auto eol=lf diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..85b5393 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: MasterQ32 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e73c965 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +zig-cache/ +zig-out/ diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..16b66a9 --- /dev/null +++ b/LICENCE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc98cae --- /dev/null +++ b/README.md @@ -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 + +#include +#include + +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; +} +``` \ No newline at end of file diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..d0e2e2d --- /dev/null +++ b/build.zig @@ -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); +} diff --git a/example/example.c b/example/example.c new file mode 100644 index 0000000..dd468cb --- /dev/null +++ b/example/example.c @@ -0,0 +1,41 @@ +#include + +#include +#include + +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; +} \ No newline at end of file diff --git a/example/example.ini b/example/example.ini new file mode 100644 index 0000000..98b24e5 --- /dev/null +++ b/example/example.ini @@ -0,0 +1,9 @@ +[Meta] +author = xq +library = ini + +[Albums] +Thriller +Back in Black +Bat Out of Hell +The Dark Side of the Moon \ No newline at end of file diff --git a/example/example.zig b/example/example.zig new file mode 100644 index 0000000..38dd54b --- /dev/null +++ b/example/example.zig @@ -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}), + } + } +} diff --git a/src/ini.h b/src/ini.h new file mode 100644 index 0000000..a7dee28 --- /dev/null +++ b/src/ini.h @@ -0,0 +1,64 @@ +#ifndef ZIG_INI_H +#define ZIG_INI_H + +#include +#include +#include + +/// 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 diff --git a/src/ini.zig b/src/ini.zig new file mode 100644 index 0000000..9c8cc17 --- /dev/null +++ b/src/ini.zig @@ -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, + }; +} diff --git a/src/lib-test.zig b/src/lib-test.zig new file mode 100644 index 0000000..9c61130 --- /dev/null +++ b/src/lib-test.zig @@ -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); +} diff --git a/src/lib.zig b/src/lib.zig new file mode 100644 index 0000000..e4477b7 --- /dev/null +++ b/src/lib.zig @@ -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), + } +} diff --git a/src/test.zig b/src/test.zig new file mode 100644 index 0000000..19ee563 --- /dev/null +++ b/src/test.zig @@ -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()); +}