const std = @import("std"); const testing = std.testing; const Self = @This(); pub const KeyValue = struct { key: []const u8, value: []const u8, }; pub const Section = struct { pub const KeyValues = std.ArrayList(KeyValue); name: []const u8, key_values: KeyValues, pub fn init(allocator: std.mem.Allocator, name: []const u8) Section { return Section{ .name = name, .key_values = KeyValues.init(allocator), }; } pub fn deinit(self: Section) void { self.key_values.deinit(); } }; fn scrapComments(str: []const u8) []const u8 { if (std.mem.indexOf(u8, str, ";")) |index| { return std.mem.trim(u8, str[0..index], &std.ascii.spaces); } else { return std.mem.trim(u8, str, &std.ascii.spaces); } } pub const Sections = std.ArrayList(Section); allocator: std.mem.Allocator, sections: Sections, pub fn parse(allocator: std.mem.Allocator, str: []const u8) !Self { var self = Self{ .allocator = allocator, .sections = Sections.init(allocator), }; errdefer self.sections.deinit(); errdefer for (self.sections.items) |section| section.deinit(); var iter = std.mem.tokenize(u8, str, "\n"); var line_num: usize = 1; while (iter.next()) |line| { defer line_num += 1; const actual_line = scrapComments(line); if (actual_line.len == 0) { continue; } else if (actual_line[0] == '[') { var section_name = actual_line[1..]; if (actual_line[actual_line.len - 1] != ']') { std.log.warn("Invalid section name at line {}", .{line_num}); } else { section_name = section_name[0 .. section_name.len - 1]; } try self.sections.append(Section.init(allocator, section_name)); } else { if (std.mem.indexOf(u8, actual_line, "=")) |index| { const key = actual_line[0..index]; const value = actual_line[index + 1 ..]; if (self.sections.items.len > 0) { try self.sections.items[self.sections.items.len - 1] .key_values.append(.{ .key = key, .value = value }); } else { std.log.warn("Ignored key-value without section at line {}", .{line_num}); } } else { std.log.warn("Ignored invalid key-value at line {}", .{line_num}); } } } return self; } pub fn deinit(self: Self) void { for (self.sections.items) |section| { section.deinit(); } self.sections.deinit(); } test "try parsing" { const ini_str = \\; Device level parameters \\; UseTheseDomainSizes - When enabled (=1), use the sizes defined in the INI \\; to defined the memory sizes for each domain \\; When disabled (=0), for STAT_PLC, use the following \\; defaults: \\; Max Coils : 32768 elements \\; Max Input Status: 32768 elements \\; Max Input Regs: 16384 elements \\; Max Holding Regs: 16384 elements \\; All other memory types are 0 elements \\;; For all other device models, the device communication \\; interface will attempt to size the memory \\; \\; The default value is 0 \\; \\; UseCounts - When enabled (=1), indicates sizes are in elements \\; When disabled (=0) indicates sizes are in bytes \\; \\; Default value is 0 \\; \\; ConservesConn - When enabled (=1), indicates that it is normal for the device \\; to close the connection (typically based on inactivity). The \\; device communication interface will not assume that the \\; device is down unless it is unable to create a connection and \\; get a response when it attempts the current scheduled \\; operation to retrieve data from the device or modify data. \\; \\; When disabled (=0), indicates that a termination of the \\; connection between the device communication interface and \\; the device will cause the device communication interface to \\; assume that the connection is down and terminate the \\; connection \\; \\; Default value is 0. \\; \\; ConnSecondary - When Enabled (=1), in a Host Redundant environment, \\; the device communciation interface will attempt to \\; maintain a connection with the device on the acting \\; secondary. \\; \\; When Disabled(=0), in a Host Redundant environment, \\; the device communication interface will terminate its \\; connection to the device when transitioning to the \\; secondary. \\; \\; Default value is 1. \\; \\; OneCoilWrite - When enabled (=1) use Function 5 to write single coils \\; When disabled (=0) use Function 15 to write single coils \\; \\; VersaMax ENIU, VersaPoint ENIU and Modicon 484's ignore this \\; parameter. \\; \\;OneRegiserWrite - When enabled (=1) use Function 6 to write single holding registers \\; When disabled (=0) use Function 16 to write single holding registers \\; \\; VersaMax ENIU, VersaPoint ENIU and Modicon 484's ignore this \\; parameter. \\; \\[DEVICE1] \\UseTheseDomainSizes=1 \\UseCounts=0 \\OneCoilWrite=0 \\OneRegWrite=0 \\ConservesConn=1 \\ConnSecondary=0 \\COILS=65535 \\DISC INPUTS=65535 \\INPUT REG.=65535 \\HOLDING REG.=65535 \\GEN REF FILE1=0 \\GEN REF FILE2=0 \\GEN REF FILE3=0 \\GEN REF FILE4=0 \\GEN REF FILE5=0 \\GEN REF FILE6=0 \\GEN REF FILE7=0 \\GEN REF FILE8=0 \\GEN REF FILE9=0 \\GEN REF FILE10=0 \\DP_INPUT REG.=0 \\DP_HOLDING REG.=0 \\ \\[DEVICE2] \\UseTheseDomainSizes=1 \\UseCounts=0 \\OneCoilWrite=0 \\OneRegWrite=0 \\ConservesConn=1 \\ConnSecondary=0 \\COILS=65535 \\DISC INPUTS=65535 \\INPUT REG.=65535 \\HOLDING REG.=65535 \\GEN REF FILE1=0 \\GEN REF FILE2=0 \\GEN REF FILE3=0 \\GEN REF FILE4=0 \\GEN REF FILE5=0 \\GEN REF FILE6=0 \\GEN REF FILE7=0 \\GEN REF FILE8=0 \\GEN REF FILE9=0 \\GEN REF FILE10=0 \\DP_INPUT REG.=0 \\DP_HOLDING REG.=0 ; const ini = try parse(testing.allocator, ini_str); defer ini.deinit(); try testing.expectEqual(@as(usize, 2), ini.sections.items.len); try testing.expectEqualStrings("DEVICE1", ini.sections.items[0].name); try testing.expectEqualStrings("DEVICE2", ini.sections.items[1].name); try testing.expectEqual(@as(usize, 22), ini.sections.items[0].key_values.items.len); try testing.expectEqualStrings("OneCoilWrite", ini.sections.items[0].key_values.items[2].key); try testing.expectEqualStrings("0", ini.sections.items[0].key_values.items[2].value); }