diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5cdc051..e768801 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,5 +10,5 @@ jobs: - uses: actions/checkout@v3 - uses: mlugg/setup-zig@v2 with: - version: 0.15.0 + version: 0.16.0 - run: zig build test diff --git a/README.md b/README.md index 35b6284..32d382b 100644 --- a/README.md +++ b/README.md @@ -12,21 +12,19 @@ This is a very simple ini-parser library that provides: ### Zig ```zig -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"); +pub fn main(init: std.process.Init) !void { + const io = init.io; + const file = try std.Io.Dir.cwd().openFile(io, "example.ini", .{}); + defer file.close(io); var read_buffer: [1024]u8 = undefined; - var file_reader = file.reader(&read_buffer); - var parser = ini.parse(gpa.allocator(), &file_reader.interface, ";#"); + var file_reader = file.reader(io, &read_buffer); + var parser = ini.parse(init.gpa, &file_reader.interface, ";#"); defer parser.deinit(); var write_buffer: [1024]u8 = undefined; - var file_writer = std.fs.File.stdout().writer(&write_buffer); - var writer = &file_writer.interface; + var file_writer = std.Io.File.stdout().writer(io, &write_buffer); + const writer = &file_writer.interface; defer writer.flush() catch @panic("Could not flush to stdout"); while (try parser.next()) |record| { @@ -36,8 +34,7 @@ pub fn main() !void { .enumeration => |value| try writer.print("{s}\n", .{value}), } } -} -``` +}``` ### C diff --git a/build.zig b/build.zig index 1a2c840..cd5736e 100644 --- a/build.zig +++ b/build.zig @@ -10,17 +10,29 @@ pub fn build(b: *std.Build) void { .target = target, }); + const ini_c_header = b.addTranslateC(.{ + .root_source_file = b.path("src/ini.h"), + .target = target, + .optimize = optimize, + }); + const lib = b.addLibrary(.{ .name = "ini", .root_module = b.createModule(.{ .root_source_file = b.path("src/lib.zig"), .target = target, .optimize = optimize, + .link_libc = true, + .imports = &.{ + .{ + .name = "c", + .module = ini_c_header.createModule(), + }, + }, }), }); lib.bundle_compiler_rt = true; - lib.addIncludePath(b.path("src")); - lib.linkLibC(); + lib.root_module.addIncludePath(b.path("src")); lib.installHeader(b.path("src/ini.h"), "ini.h"); b.installArtifact(lib); @@ -30,9 +42,10 @@ pub fn build(b: *std.Build) void { .root_module = b.createModule(.{ .optimize = optimize, .target = target, + .link_libc = true, }), }); - example_c.addCSourceFile(.{ + example_c.root_module.addCSourceFile(.{ .file = b.path("example/example.c"), .flags = &.{ "-Wall", @@ -40,9 +53,8 @@ pub fn build(b: *std.Build) void { "-pedantic", }, }); - example_c.addIncludePath(b.path("src")); - example_c.linkLibrary(lib); - example_c.linkLibC(); + example_c.root_module.addIncludePath(b.path("src")); + example_c.root_module.linkLibrary(lib); example_step.dependOn(&b.addInstallArtifact(example_c, .{}).step); const example_zig = b.addExecutable(.{ @@ -51,9 +63,11 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("example/example.zig"), .optimize = optimize, .target = target, + .imports = &.{ + .{ .name = "ini", .module = b.modules.get("ini").? }, + }, }), }); - example_zig.root_module.addImport("ini", b.modules.get("ini").?); example_step.dependOn(&b.addInstallArtifact(example_zig, .{}).step); const test_step = b.step("test", "Run library tests"); @@ -71,10 +85,16 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/lib-test.zig"), .optimize = optimize, .target = target, + .link_libc = true, + .imports = &.{ + .{ + .name = "c", + .module = ini_c_header.createModule(), + }, + }, }), }); - binding_tests.addIncludePath(b.path("src")); - binding_tests.linkLibrary(lib); - binding_tests.linkLibC(); + binding_tests.root_module.addIncludePath(b.path("src")); + binding_tests.root_module.linkLibrary(lib); test_step.dependOn(&b.addRunArtifact(binding_tests).step); } diff --git a/build.zig.zon b/build.zig.zon index a66071a..e48ed39 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,7 +2,7 @@ .name = .ini, .fingerprint = 0x7757b668623d2460, .version = "0.1.0", - .minimum_zig_version = "0.15.0", + .minimum_zig_version = "0.16.0", .paths = .{ "LICENCE", "README.md", diff --git a/example/example.zig b/example/example.zig index 1682de4..3785889 100644 --- a/example/example.zig +++ b/example/example.zig @@ -1,21 +1,19 @@ 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"); +pub fn main(init: std.process.Init) !void { + const io = init.io; + const file = try std.Io.Dir.cwd().openFile(io, "example.ini", .{}); + defer file.close(io); var read_buffer: [1024]u8 = undefined; - var file_reader = file.reader(&read_buffer); - var parser = ini.parse(gpa.allocator(), &file_reader.interface, ";#"); + var file_reader = file.reader(io, &read_buffer); + var parser = ini.parse(init.gpa, &file_reader.interface, ";#"); defer parser.deinit(); var write_buffer: [1024]u8 = undefined; - var file_writer = std.fs.File.stdout().writer(&write_buffer); - var writer = &file_writer.interface; + var file_writer = std.Io.File.stdout().writer(io, &write_buffer); + const writer = &file_writer.interface; defer writer.flush() catch @panic("Could not flush to stdout"); while (try parser.next()) |record| { diff --git a/src/ini.zig b/src/ini.zig index 90fdc79..73d2b99 100644 --- a/src/ini.zig +++ b/src/ini.zig @@ -37,39 +37,37 @@ fn insertNulTerminator(slice: []const u8) [:0]const u8 { pub const Parser = struct { const Self = @This(); - allocator: std.mem.Allocator, - line_buffer: std.array_list.Managed(u8), - reader: *std.io.Reader, + line_writer: std.Io.Writer.Allocating, + reader: *std.Io.Reader, comment_characters: []const u8, pub fn deinit(self: *Self) void { - self.line_buffer.deinit(); + self.line_writer.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(); + self.line_writer.clearRetainingCapacity(); while (true) { - _ = try self.reader.streamDelimiterLimit(writer, '\n', .limited(4096)); - try writer.flush(); + _ = try self.reader.streamDelimiterLimit(&self.line_writer.writer, '\n', .limited(4096)); + + var line: []const u8 = self.line_writer.written(); + const discarded = self.reader.discard(.limited(1)) catch |e| blk: { switch (e) { error.EndOfStream => { - if (self.line_buffer.items.len == 0) + if (line.len == 0) return null; break :blk 0; }, else => return e, } }; - if (self.line_buffer.items.len == 0 and discarded == 0) + if (line.len == 0 and discarded == 0) return null; - try self.line_buffer.append(0); // append guaranteed space for sentinel + try self.line_writer.writer.writeByte(0); // append guaranteed space for sentinel + line = self.line_writer.written(); - var line: []const u8 = self.line_buffer.items; var last_index: usize = 0; // handle comments and escaping @@ -81,8 +79,9 @@ pub const Parser = struct { const previous_char = line[previous_index]; if (previous_char == '\\') { - _ = self.line_buffer.orderedRemove(previous_index); - line = self.line_buffer.items; + var buf = self.line_writer.written(); + @memmove(buf[previous_index .. buf.len - 1], buf[index..buf.len]); + self.line_writer.shrinkRetainingCapacity(buf.len - 1); last_index = index + 1; continue; @@ -98,7 +97,7 @@ pub const Parser = struct { } if (line.len == 0) { - self.line_buffer.clearRetainingCapacity(); + self.line_writer.clearRetainingCapacity(); continue; } @@ -122,10 +121,9 @@ pub const Parser = struct { }; /// Returns a new parser that can read the ini structure -pub fn parse(allocator: std.mem.Allocator, reader: *std.io.Reader, comment_characters: []const u8) Parser { +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), + .line_writer = std.Io.Writer.Allocating.init(allocator), .reader = reader, .comment_characters = comment_characters, }; diff --git a/src/lib-test.zig b/src/lib-test.zig index fcf69c6..cb4ff0d 100644 --- a/src/lib-test.zig +++ b/src/lib-test.zig @@ -1,8 +1,5 @@ const std = @import("std"); - -const c = @cImport({ - @cInclude("ini.h"); -}); +const c = @import("c"); test "parser create/destroy" { var buffer: c.ini_Parser = undefined; diff --git a/src/lib.zig b/src/lib.zig index f0b436c..b81fdb4 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -1,9 +1,6 @@ const std = @import("std"); const ini = @import("ini.zig"); - -const c = @cImport({ - @cInclude("ini.h"); -}); +const c = @import("c"); const Record = extern struct { type: Type, @@ -29,12 +26,12 @@ const Record = extern struct { }; const BufferParser = struct { - stream: std.io.Reader, + stream: std.Io.Reader, parser: ini.Parser, }; const FileParser = struct { - old_reader_adapter: CReader.Adapter, + reader: CReader, parser: ini.Parser, }; @@ -70,7 +67,7 @@ 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.Reader.fixed(data[0..data_length]), + .stream = std.Io.Reader.fixed(data[0..data_length]), .parser = undefined, }, }; @@ -81,12 +78,12 @@ export fn ini_create_buffer(parser: *IniParser, data: [*]const u8, data_length: 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 = .{ - .old_reader_adapter = cReader(file).adaptToNewApi(read_buffer[0..read_buffer_length]), + .reader = CReader.init(file, 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]); + parser.file.parser = ini.parse(std.heap.c_allocator, &parser.file.reader.interface, comment_characters[0..comment_characters_length]); } export fn ini_destroy(parser: *IniParser) void { @@ -97,7 +94,7 @@ export fn ini_destroy(parser: *IniParser) void { parser.* = undefined; } -const ParseError = error{ OutOfMemory, StreamTooLong } || std.io.Reader.Error || std.io.Writer.Error; +const ParseError = error{ OutOfMemory, StreamTooLong } || std.Io.Reader.Error || std.Io.Writer.Error; fn mapError(err: ParseError) IniError { return switch (err) { @@ -141,28 +138,37 @@ export fn ini_next(parser: *IniParser, record: *Record) IniError { return .success; } -const CReader = std.Io.GenericReader(*std.c.FILE, std.fs.File.ReadError, cReaderRead); +extern "c" fn feof(stream: *std.c.FILE) c_int; -fn cReader(c_file: *std.c.FILE) CReader { - return .{ .context = c_file }; -} +const CReader = struct { + file: *std.c.FILE, + interface: std.Io.Reader, -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), + fn init(file: *std.c.FILE, buffer: []u8) CReader { + return .{ + .file = file, + .interface = .{ + .vtable = &.{ .stream = stream }, + .buffer = buffer, + .seek = 0, + .end = 0, + }, + }; } -} + + fn stream(r: *std.Io.Reader, w: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { + const creader: *CReader = @alignCast(@fieldParentPtr("interface", r)); + + if (limit == .nothing) return 0; + const dest = limit.slice(try w.writableSliceGreedy(1)); + + const n = std.c.fread(dest.ptr, 1, dest.len, creader.file); + if (n > 0) { + w.advance(n); + return n; + } + + if (feof(creader.file) != 0) return error.EndOfStream; + return error.ReadFailed; + } +}; diff --git a/src/test.zig b/src/test.zig index 0bfb821..7da1205 100644 --- a/src/test.zig +++ b/src/test.zig @@ -22,7 +22,7 @@ fn expectEnumeration(enumeration: []const u8, record: ?Record) !void { } test "empty file" { - var stream = std.io.Reader.fixed(""); + var stream = std.Io.Reader.fixed(""); var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); @@ -33,7 +33,7 @@ test "empty file" { } test "section" { - var stream = std.io.Reader.fixed("[Hello]"); + var stream = std.Io.Reader.fixed("[Hello]"); var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); @@ -60,7 +60,7 @@ test "key-value-pair" { "key = value ", " key = value ", }) |pattern| { - var stream = std.io.Reader.fixed(pattern); + var stream = std.Io.Reader.fixed(pattern); var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); @@ -70,7 +70,7 @@ test "key-value-pair" { } test "enumeration" { - var stream = std.io.Reader.fixed("enum"); + var stream = std.Io.Reader.fixed("enum"); var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); @@ -79,7 +79,7 @@ test "enumeration" { } test "empty line skipping" { - var stream = std.io.Reader.fixed("item a\r\n\r\n\r\nitem b"); + 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(); @@ -89,7 +89,7 @@ test "empty line skipping" { } test "multiple sections" { - var stream = std.io.Reader.fixed(" [Hello] \r\n[Foo Bar]\n[Hello!]\n"); + var stream = std.Io.Reader.fixed(" [Hello] \r\n[Foo Bar]\n[Hello!]\n"); var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); @@ -100,7 +100,7 @@ test "multiple sections" { } test "multiple properties" { - var stream = std.io.Reader.fixed("a = b\r\nc =\r\nkey value = core property"); + 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(); @@ -111,7 +111,7 @@ test "multiple properties" { } test "multiple enumeration" { - var stream = std.io.Reader.fixed(" a \n b \r\n c "); + var stream = std.Io.Reader.fixed(" a \n b \r\n c "); var parser = parse(std.testing.allocator, &stream, ";#"); defer parser.deinit(); @@ -122,7 +122,7 @@ test "multiple enumeration" { } test "mixed data" { - var stream = std.io.Reader.fixed( + var stream = std.Io.Reader.fixed( \\[Meta] \\author = xq \\library = ini @@ -151,7 +151,7 @@ test "mixed data" { } test "# comments" { - var stream = std.io.Reader.fixed( + var stream = std.Io.Reader.fixed( \\[section] # comment \\key = value # comment \\enum # comment @@ -167,7 +167,7 @@ test "# comments" { } test "; comments" { - var stream = std.io.Reader.fixed( + var stream = std.Io.Reader.fixed( \\[section] ; comment \\key = value ; comment \\enum ; comment @@ -183,7 +183,7 @@ test "; comments" { } test "comment escaping" { - var stream = std.io.Reader.fixed( + var stream = std.Io.Reader.fixed( \\# This comment should be ignored \\# \\# Amazing! @@ -204,7 +204,7 @@ test "comment escaping" { } test "comment character selection" { - var stream = std.io.Reader.fixed( + var stream = std.Io.Reader.fixed( \\# This is a comment \\? But this is also one! \\just_a_key = value