commit d0da286289e8d17e89ebeebbd5bdc1cd3df60076
parent 34fdb97585fee2ce307b1190d8e5dd0a8ccf8936
Author: Alexey Yerin <yyp@disroot.org>
Date: Sat, 25 Sep 2021 10:48:49 +0300
format::ini: new module
Signed-off-by: Alexey Yerin <yyp@disroot.org>
Diffstat:
5 files changed, 237 insertions(+), 0 deletions(-)
diff --git a/format/ini/+test.ha b/format/ini/+test.ha
@@ -0,0 +1,75 @@
+use bufio;
+use io;
+use strings;
+
+@test fn simple() void = {
+ const buf = bufio::fixed(strings::toutf8(
+"# This is a comment
+[sourcehut.org]
+name=Sourcehut
+description=The hacker's forge
+[harelang.org]
+name=Hare
+description=The Hare programming language"), io::mode::READ);
+ defer io::close(buf);
+ const sc = scan(buf);
+ defer finish(&sc);
+
+ // [sourcehut.org]
+ ini_test(&sc, "sourcehut.org", "name", "Sourcehut");
+ ini_test(&sc, "sourcehut.org", "description", "The hacker's forge");
+ // [harelang.org]
+ ini_test(&sc, "harelang.org", "name", "Hare");
+ ini_test(&sc, "harelang.org", "description",
+ "The Hare programming language");
+ assert(next(&sc) is io::EOF);
+};
+
+@test fn extended() void = {
+ // TODO: expand?
+ const buf = bufio::fixed(strings::toutf8(
+"# Equal sign in the value
+exec=env VARIABLE=value binary
+
+# Unicode
+trademark=™
+"), io::mode::READ);
+ defer io::close(buf);
+ const sc = scan(buf);
+ defer finish(&sc);
+
+ ini_test(&sc, "", "exec", "env VARIABLE=value binary");
+ ini_test(&sc, "", "trademark", "™");
+ assert(next(&sc) is io::EOF);
+};
+
+
+@test fn invalid() void = {
+ // Missing equal sign
+ const buf = bufio::fixed(strings::toutf8("novalue\n"), io::mode::READ);
+ defer io::close(buf);
+ const sc = scan(buf);
+ defer finish(&sc);
+
+ assert(next(&sc) as error is syntaxerr); // TODO: test line numbering?
+
+ // Unterminated section header
+ const buf = bufio::fixed(strings::toutf8("[dangling\n"), io::mode::READ);
+ defer io::close(buf);
+ const sc = scan(buf);
+ defer finish(&sc);
+
+ assert(next(&sc) as error is syntaxerr);
+};
+
+fn ini_test(
+ sc: *scanner,
+ section: const str,
+ key: const str,
+ value: const str,
+) void = {
+ const ent = next(sc)! as entry;
+ assert(ent.0 == section);
+ assert(ent.1 == key);
+ assert(ent.2 == value);
+};
diff --git a/format/ini/scan.ha b/format/ini/scan.ha
@@ -0,0 +1,92 @@
+use bufio;
+use encoding::utf8;
+use fmt;
+use io;
+use strings;
+
+export type scanner = struct {
+ in: *io::stream,
+ line: str,
+ lineno: size,
+ section: str,
+};
+
+// Creates an INI file scanner. Use [[next]] to read entries. The caller must
+// call [[finish]] once they're done with this object.
+export fn scan(in: *io::stream) scanner = scanner {
+ in = in,
+ lineno = 1,
+ ...
+};
+
+// Frees resources associated with a [[scanner]].
+export fn finish(sc: *scanner) void = {
+ free(sc.line);
+ free(sc.section);
+};
+
+// An entry in an INI file: (section, key, value).
+export type entry = (const str, const str, const str);
+
+// Duplicates an [[entry]]. Use [[entry_finish]] to get rid of it.
+export fn entry_dup(ent: entry) entry = (
+ strings::dup(ent.0),
+ strings::dup(ent.1),
+ strings::dup(ent.2),
+);
+
+// Frees an [[entry]] previously duplicated with [[entry_dup]].
+export fn entry_finish(ent: entry) void = {
+ free(ent.0);
+ free(ent.1);
+ free(ent.2);
+};
+
+// Returns the next entry from an INI file. The return value is overwritten on
+// subsequent calls, use [[entry_dup]] or [[strings::dup]] to extend the
+// lifetime of the entry or its fields respectively.
+export fn next(sc: *scanner) (entry | io::EOF | error) = {
+ for (true) {
+ const line = match (bufio::scanline(sc.in)?) {
+ case b: []u8 =>
+ yield strings::try_fromutf8(b)?;
+ case io::EOF =>
+ return io::EOF;
+ };
+ sc.lineno += 1;
+
+ free(sc.line);
+ sc.line = line;
+
+ const line = strings::trim(sc.line);
+
+ if (len(line) == 0 || strings::has_prefix(line, "#")) {
+ continue;
+ };
+
+ if (strings::has_prefix(line, "[")) {
+ const end = match (strings::index(line, ']')) {
+ case idx: size =>
+ yield idx;
+ case void =>
+ return (sc.in.name, sc.lineno): syntaxerr;
+ };
+ free(sc.section);
+ sc.section = strings::dup(strings::sub(line, 1, end));
+ continue;
+ };
+
+ const eq = match (strings::index(line, '=')) {
+ case idx: size =>
+ yield idx;
+ case void =>
+ return (sc.in.name, sc.lineno): syntaxerr;
+ };
+ return (
+ sc.section,
+ strings::sub(line, 0, eq),
+ strings::sub(line, eq + 1, strings::end),
+ );
+ };
+ abort(); // Unreachable
+};
diff --git a/format/ini/types.ha b/format/ini/types.ha
@@ -0,0 +1,21 @@
+use encoding::utf8;
+use fmt;
+use io;
+
+// A syntax error occured during parsing.
+export type syntaxerr = !(str, size);
+
+// Any error that may occur during parsing.
+export type error = !(io::error | utf8::invalid | syntaxerr);
+
+// Returns a user-friendly representation of [[error]].
+export fn strerror(err: error) const str = match (err) {
+case err: io::error =>
+ return io::strerror(err);
+case utf8::invalid =>
+ return "File is invalid UTF-8";
+case s: syntaxerr =>
+ // XXX: tuple unpacking could improve this
+ static let buf: [1024]u8 = [0...];
+ yield fmt::bsprintf(buf, "{}:{}: Invalid syntax", s.0, s.1);
+};
diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib
@@ -308,6 +308,23 @@ format_elf() {
gen_ssa format::elf
}
+gensrcs_format_ini() {
+ gen_srcs format::ini \
+ scan.ha \
+ types.ha \
+ $*
+}
+
+format_ini() {
+ if [ $testing -eq 0 ]
+ then
+ gensrcs_format_ini
+ else
+ gensrcs_format_ini +test.ha
+ fi
+ gen_ssa format::ini bufio encoding::utf8 fmt io strings
+}
+
gensrcs_format_xml() {
gen_srcs format::xml \
types.ha \
@@ -874,6 +891,7 @@ errors
fmt
fnmatch
format::elf
+format::ini
format::xml
fs
fs::mem
diff --git a/stdlib.mk b/stdlib.mk
@@ -162,6 +162,10 @@ hare_stdlib_deps+=$(stdlib_fnmatch)
stdlib_format_elf=$(HARECACHE)/format/elf/format_elf.o
hare_stdlib_deps+=$(stdlib_format_elf)
+# gen_lib format::ini
+stdlib_format_ini=$(HARECACHE)/format/ini/format_ini.o
+hare_stdlib_deps+=$(stdlib_format_ini)
+
# gen_lib format::xml
stdlib_format_xml=$(HARECACHE)/format/xml/format_xml.o
hare_stdlib_deps+=$(stdlib_format_xml)
@@ -589,6 +593,17 @@ $(HARECACHE)/format/elf/format_elf.ssa: $(stdlib_format_elf_srcs) $(stdlib_rt)
@HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Nformat::elf \
-t$(HARECACHE)/format/elf/format_elf.td $(stdlib_format_elf_srcs)
+# format::ini
+stdlib_format_ini_srcs= \
+ $(STDLIB)/format/ini/scan.ha \
+ $(STDLIB)/format/ini/types.ha
+
+$(HARECACHE)/format/ini/format_ini.ssa: $(stdlib_format_ini_srcs) $(stdlib_rt) $(stdlib_bufio) $(stdlib_encoding_utf8) $(stdlib_fmt) $(stdlib_io) $(stdlib_strings)
+ @printf 'HAREC \t$@\n'
+ @mkdir -p $(HARECACHE)/format/ini
+ @HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Nformat::ini \
+ -t$(HARECACHE)/format/ini/format_ini.td $(stdlib_format_ini_srcs)
+
# format::xml
stdlib_format_xml_srcs= \
$(STDLIB)/format/xml/types.ha \
@@ -1375,6 +1390,10 @@ hare_testlib_deps+=$(testlib_fnmatch)
testlib_format_elf=$(TESTCACHE)/format/elf/format_elf.o
hare_testlib_deps+=$(testlib_format_elf)
+# gen_lib format::ini
+testlib_format_ini=$(TESTCACHE)/format/ini/format_ini.o
+hare_testlib_deps+=$(testlib_format_ini)
+
# gen_lib format::xml
testlib_format_xml=$(TESTCACHE)/format/xml/format_xml.o
hare_testlib_deps+=$(testlib_format_xml)
@@ -1810,6 +1829,18 @@ $(TESTCACHE)/format/elf/format_elf.ssa: $(testlib_format_elf_srcs) $(testlib_rt)
@HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nformat::elf \
-t$(TESTCACHE)/format/elf/format_elf.td $(testlib_format_elf_srcs)
+# format::ini
+testlib_format_ini_srcs= \
+ $(STDLIB)/format/ini/scan.ha \
+ $(STDLIB)/format/ini/types.ha \
+ $(STDLIB)/format/ini/+test.ha
+
+$(TESTCACHE)/format/ini/format_ini.ssa: $(testlib_format_ini_srcs) $(testlib_rt) $(testlib_bufio) $(testlib_encoding_utf8) $(testlib_fmt) $(testlib_io) $(testlib_strings)
+ @printf 'HAREC \t$@\n'
+ @mkdir -p $(TESTCACHE)/format/ini
+ @HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nformat::ini \
+ -t$(TESTCACHE)/format/ini/format_ini.td $(testlib_format_ini_srcs)
+
# format::xml
testlib_format_xml_srcs= \
$(STDLIB)/format/xml/types.ha \