diff --git a/README.md b/README.md index 6932478..35b6284 100644 --- a/README.md +++ b/README.md @@ -12,19 +12,22 @@ This is a very simple ini-parser library that provides: ### 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 gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer if (gpa.deinit() != .ok) @panic("memory leaked"); - var parser = ini.parse(gpa.allocator(), file.reader(), ";#"); + + var read_buffer: [1024]u8 = undefined; + var file_reader = file.reader(&read_buffer); + var parser = ini.parse(gpa.allocator(), &file_reader.interface, ";#"); defer parser.deinit(); - var writer = std.io.getStdOut().writer(); + var write_buffer: [1024]u8 = undefined; + var file_writer = std.fs.File.stdout().writer(&write_buffer); + var writer = &file_writer.interface; + defer writer.flush() catch @panic("Could not flush to stdout"); while (try parser.next()) |record| { switch (record) { @@ -45,12 +48,13 @@ pub fn main() !void { #include int main() { - FILE * f = fopen("example.ini", "rb"); + FILE * f = fopen("example.ini", "r"); if(!f) return 1; struct ini_Parser parser; - ini_create_file(&parser, f, ";#", 2); + char read_buffer[1024] = {0}; + ini_create_file(&parser, read_buffer, sizeof read_buffer, f, ";#", 2); struct ini_Record record; while(true) diff --git a/build.zig b/build.zig index 1120586..1a2c840 100644 --- a/build.zig +++ b/build.zig @@ -6,6 +6,8 @@ pub fn build(b: *std.Build) void { _ = b.addModule("ini", .{ .root_source_file = b.path("src/ini.zig"), + .optimize = optimize, + .target = target, }); const lib = b.addLibrary(.{ diff --git a/example/example.c b/example/example.c index 5944e46..09ae3cb 100644 --- a/example/example.c +++ b/example/example.c @@ -4,12 +4,13 @@ #include int main() { - FILE * f = fopen("example.ini", "rb"); + FILE * f = fopen("example.ini", "r"); if(!f) return 1; struct ini_Parser parser; - ini_create_file(&parser, f, ";#", 2); + char read_buffer[1024] = {0}; + ini_create_file(&parser, read_buffer, sizeof read_buffer, f, ";#", 2); struct ini_Record record; while(true) diff --git a/example/example.zig b/example/example.zig index 9c480f2..1682de4 100644 --- a/example/example.zig +++ b/example/example.zig @@ -7,10 +7,16 @@ pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer if (gpa.deinit() != .ok) @panic("memory leaked"); - var parser = ini.parse(gpa.allocator(), file.reader(), ";#"); + + var read_buffer: [1024]u8 = undefined; + var file_reader = file.reader(&read_buffer); + var parser = ini.parse(gpa.allocator(), &file_reader.interface, ";#"); defer parser.deinit(); - var writer = std.io.getStdOut().writer(); + var write_buffer: [1024]u8 = undefined; + var file_writer = std.fs.File.stdout().writer(&write_buffer); + var writer = &file_writer.interface; + defer writer.flush() catch @panic("Could not flush to stdout"); while (try parser.next()) |record| { switch (record) { diff --git a/src/ini.h b/src/ini.h index 1ccf5d1..71c88e0 100644 --- a/src/ini.h +++ b/src/ini.h @@ -11,7 +11,7 @@ /// after being initialized with `ini_create_*`! struct ini_Parser { - alignas(16) char opaque[128]; + alignas(16) char opaque[256]; }; enum ini_RecordType : int @@ -56,6 +56,8 @@ extern void ini_create_buffer( extern void ini_create_file( struct ini_Parser * parser, + char * read_buffer, + size_t read_buffer_length, FILE * file, char const * comment_characters, size_t comment_characters_length diff --git a/src/ini.zig b/src/ini.zig index 056bc55..90fdc79 100644 --- a/src/ini.zig +++ b/src/ini.zig @@ -34,85 +34,96 @@ fn insertNulTerminator(slice: []const u8) [:0]const u8 { return mut_ptr[0..slice.len :0]; } -pub fn Parser(comptime Reader: type) type { - return struct { - const Self = @This(); - - allocator: std.mem.Allocator, - line_buffer: std.array_list.Managed(u8), - reader: Reader, - comment_characters: []const u8, - - 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) { +pub const Parser = struct { + const Self = @This(); + + allocator: std.mem.Allocator, + line_buffer: std.array_list.Managed(u8), + reader: *std.io.Reader, + comment_characters: []const u8, + + pub fn deinit(self: *Self) void { + self.line_buffer.deinit(); + self.* = undefined; + } + + pub fn next(self: *Self) !?Record { + var write_buffer: [1024]u8 = undefined; + var old_writer_adapter = self.line_buffer.writer().adaptToNewApi(&write_buffer); + var writer = &old_writer_adapter.new_interface; + self.line_buffer.clearRetainingCapacity(); + while (true) { + _ = try self.reader.streamDelimiterLimit(writer, '\n', .limited(4096)); + try writer.flush(); + const discarded = self.reader.discard(.limited(1)) catch |e| blk: { + switch (e) { error.EndOfStream => { if (self.line_buffer.items.len == 0) return null; + break :blk 0; }, - else => |e| return e, - }; - try self.line_buffer.append(0); // append guaranteed space for sentinel - - var line: []const u8 = self.line_buffer.items; - var last_index: usize = 0; - - // handle comments and escaping - while (last_index < line.len) { - if (std.mem.indexOfAnyPos(u8, line, last_index, self.comment_characters)) |index| { - // escape character if needed, then skip it (it's not a comment) - if (index > 0) { - const previous_index = index - 1; - const previous_char = line[previous_index]; - - if (previous_char == '\\') { - _ = self.line_buffer.orderedRemove(previous_index); - line = self.line_buffer.items; - - last_index = index + 1; - continue; - } + else => return e, + } + }; + if (self.line_buffer.items.len == 0 and discarded == 0) + return null; + try self.line_buffer.append(0); // append guaranteed space for sentinel + + var line: []const u8 = self.line_buffer.items; + var last_index: usize = 0; + + // handle comments and escaping + while (last_index < line.len) { + if (std.mem.indexOfAnyPos(u8, line, last_index, self.comment_characters)) |index| { + // escape character if needed, then skip it (it's not a comment) + if (index > 0) { + const previous_index = index - 1; + const previous_char = line[previous_index]; + + if (previous_char == '\\') { + _ = self.line_buffer.orderedRemove(previous_index); + line = self.line_buffer.items; + + last_index = index + 1; + continue; } - - line = std.mem.trim(u8, line[0..index], whitespace); - } else { - line = std.mem.trim(u8, line, whitespace); } - break; + line = std.mem.trim(u8, line[0..index], whitespace); + } else { + line = std.mem.trim(u8, line, whitespace); } - if (line.len == 0) - continue; + break; + } - if (std.mem.startsWith(u8, line, "[") and std.mem.endsWith(u8, line, "]")) { - return Record{ .section = insertNulTerminator(line[1 .. line.len - 1]) }; - } + if (line.len == 0) { + self.line_buffer.clearRetainingCapacity(); + continue; + } - 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)), - }, - }; - } + if (std.mem.startsWith(u8, line, "[") and std.mem.endsWith(u8, line, "]")) { + return Record{ .section = insertNulTerminator(line[1 .. line.len - 1]) }; + } - return Record{ .enumeration = insertNulTerminator(line) }; + 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, comment_characters: []const u8) Parser(@TypeOf(reader)) { - return Parser(@TypeOf(reader)){ +pub fn parse(allocator: std.mem.Allocator, reader: *std.io.Reader, comment_characters: []const u8) Parser { + return Parser{ .allocator = allocator, .line_buffer = std.array_list.Managed(u8).init(allocator), .reader = reader, diff --git a/src/lib-test.zig b/src/lib-test.zig index 54f0996..fcf69c6 100644 --- a/src/lib-test.zig +++ b/src/lib-test.zig @@ -82,7 +82,8 @@ test "file parser" { defer _ = c.fclose(file); var parser: c.ini_Parser = undefined; - c.ini_create_file(&parser, file, ";#", 2); + var read_buffer: [1024]u8 = undefined; + c.ini_create_file(&parser, &read_buffer, read_buffer.len, file, ";#", 2); defer c.ini_destroy(&parser); try commonTest(&parser); diff --git a/src/lib.zig b/src/lib.zig index 926578f..f0b436c 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -29,13 +29,18 @@ const Record = extern struct { }; const BufferParser = struct { - stream: std.io.FixedBufferStream([]const u8), - parser: ini.Parser(std.io.FixedBufferStream([]const u8).Reader), + stream: std.io.Reader, + parser: ini.Parser, +}; + +const FileParser = struct { + old_reader_adapter: CReader.Adapter, + parser: ini.Parser, }; const IniParser = union(enum) { buffer: BufferParser, - file: ini.Parser(CReader), + file: FileParser, }; const IniError = enum(c.ini_Error) { @@ -65,29 +70,34 @@ comptime { export fn ini_create_buffer(parser: *IniParser, data: [*]const u8, data_length: usize, comment_characters: [*]const u8, comment_characters_length: usize) void { parser.* = IniParser{ .buffer = .{ - .stream = std.io.fixedBufferStream(data[0..data_length]), + .stream = std.io.Reader.fixed(data[0..data_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(), comment_characters[0..comment_characters_length]); + parser.buffer.parser = ini.parse(std.heap.c_allocator, &parser.buffer.stream, comment_characters[0..comment_characters_length]); } -export fn ini_create_file(parser: *IniParser, file: *std.c.FILE, comment_characters: [*]const u8, comment_characters_length: usize) void { +export fn ini_create_file(parser: *IniParser, read_buffer: [*]u8, read_buffer_length: usize, file: *std.c.FILE, comment_characters: [*]const u8, comment_characters_length: usize) void { parser.* = IniParser{ - .file = ini.parse(std.heap.c_allocator, cReader(file), comment_characters[0..comment_characters_length]), + .file = .{ + .old_reader_adapter = cReader(file).adaptToNewApi(read_buffer[0..read_buffer_length]), + .parser = undefined, + }, }; + + parser.file.parser = ini.parse(std.heap.c_allocator, &parser.file.old_reader_adapter.new_interface, comment_characters[0..comment_characters_length]); } export fn ini_destroy(parser: *IniParser) void { switch (parser.*) { .buffer => |*p| p.parser.deinit(), - .file => |*p| p.deinit(), + .file => |*p| p.parser.deinit(), } parser.* = undefined; } -const ParseError = error{ OutOfMemory, StreamTooLong } || CReader.Error; +const ParseError = error{ OutOfMemory, StreamTooLong } || std.io.Reader.Error || std.io.Writer.Error; fn mapError(err: ParseError) IniError { return switch (err) { @@ -100,7 +110,7 @@ fn mapError(err: ParseError) IniError { 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), + .file => |*p| p.parser.next() catch |e| return mapError(e), }; if (src_record_or_null) |src_record| { diff --git a/src/test.zig b/src/test.zig index b6717ab..0bfb821 100644 --- a/src/test.zig +++ b/src/test.zig @@ -22,8 +22,8 @@ fn expectEnumeration(enumeration: []const u8, record: ?Record) !void { } test "empty file" { - var stream = std.io.fixedBufferStream(""); - var parser = parse(std.testing.allocator, stream.reader(), ";#"); + var stream = std.io.Reader.fixed(""); + var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); try expectNull(try parser.next()); @@ -33,8 +33,8 @@ test "empty file" { } test "section" { - var stream = std.io.fixedBufferStream("[Hello]"); - var parser = parse(std.testing.allocator, stream.reader(), ";#"); + var stream = std.io.Reader.fixed("[Hello]"); + var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); try expectSection("Hello", try parser.next()); @@ -60,8 +60,8 @@ test "key-value-pair" { "key = value ", " key = value ", }) |pattern| { - var stream = std.io.fixedBufferStream(pattern); - var parser = parse(std.testing.allocator, stream.reader(), ";#"); + var stream = std.io.Reader.fixed(pattern); + var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); try expectKeyValue("key", "value", try parser.next()); @@ -70,8 +70,8 @@ test "key-value-pair" { } test "enumeration" { - var stream = std.io.fixedBufferStream("enum"); - var parser = parse(std.testing.allocator, stream.reader(), ";#"); + var stream = std.io.Reader.fixed("enum"); + var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); try expectEnumeration("enum", try parser.next()); @@ -79,8 +79,8 @@ test "enumeration" { } 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(), ";#"); + var stream = std.io.Reader.fixed("item a\r\n\r\n\r\nitem b"); + var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); try expectEnumeration("item a", try parser.next()); @@ -89,8 +89,8 @@ test "empty line skipping" { } test "multiple sections" { - var stream = std.io.fixedBufferStream(" [Hello] \r\n[Foo Bar]\n[Hello!]\n"); - var parser = parse(std.testing.allocator, stream.reader(), ";#"); + var stream = std.io.Reader.fixed(" [Hello] \r\n[Foo Bar]\n[Hello!]\n"); + var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); try expectSection("Hello", try parser.next()); @@ -100,8 +100,8 @@ test "multiple sections" { } 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(), ";#"); + var stream = std.io.Reader.fixed("a = b\r\nc =\r\nkey value = core property"); + var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); try expectKeyValue("a", "b", try parser.next()); @@ -111,8 +111,8 @@ test "multiple properties" { } test "multiple enumeration" { - var stream = std.io.fixedBufferStream(" a \n b \r\n c "); - var parser = parse(std.testing.allocator, stream.reader(), ";#"); + var stream = std.io.Reader.fixed(" a \n b \r\n c "); + var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); try expectEnumeration("a", try parser.next()); @@ -122,7 +122,7 @@ test "multiple enumeration" { } test "mixed data" { - var stream = std.io.fixedBufferStream( + var stream = std.io.Reader.fixed( \\[Meta] \\author = xq \\library = ini @@ -133,7 +133,7 @@ test "mixed data" { \\Bat Out of Hell \\The Dark Side of the Moon ); - var parser = parse(std.testing.allocator, stream.reader(), ";#"); + var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); try expectSection("Meta", try parser.next()); @@ -151,12 +151,12 @@ test "mixed data" { } test "# comments" { - var stream = std.io.fixedBufferStream( + var stream = std.io.Reader.fixed( \\[section] # comment \\key = value # comment \\enum # comment ); - var parser = parse(std.testing.allocator, stream.reader(), ";#"); + var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); try expectSection("section", try parser.next()); @@ -167,12 +167,12 @@ test "# comments" { } test "; comments" { - var stream = std.io.fixedBufferStream( + var stream = std.io.Reader.fixed( \\[section] ; comment \\key = value ; comment \\enum ; comment ); - var parser = parse(std.testing.allocator, stream.reader(), ";#"); + var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); try expectSection("section", try parser.next()); @@ -183,7 +183,7 @@ test "; comments" { } test "comment escaping" { - var stream = std.io.fixedBufferStream( + var stream = std.io.Reader.fixed( \\# This comment should be ignored \\# \\# Amazing! @@ -192,7 +192,7 @@ test "comment escaping" { \\escaped = \# \\no_value = #This doesn't have any value ); - var parser = parse(std.testing.allocator, stream.reader(), "#"); + var parser = parse(std.testing.allocator, &stream, "#"); defer parser.deinit(); try expectKeyValue("names", "Budgie;GNOME", try parser.next()); @@ -204,12 +204,12 @@ test "comment escaping" { } test "comment character selection" { - var stream = std.io.fixedBufferStream( + var stream = std.io.Reader.fixed( \\# This is a comment \\? But this is also one! \\just_a_key = value ); - var parser = parse(std.testing.allocator, stream.reader(), "#?"); + var parser = parse(std.testing.allocator, &stream, "#?"); defer parser.deinit(); try expectKeyValue("just_a_key", "value", try parser.next());