~sircmpwn/hare unlisted

d0da286289e8d17e89ebeebbd5bdc1cd3df60076 — Alexey Yerin a day ago 34fdb97 master
format::ini: new module

Signed-off-by: Alexey Yerin <yyp@disroot.org>
5 files changed, 237 insertions(+), 0 deletions(-)

A format/ini/+test.ha
A format/ini/scan.ha
A format/ini/types.ha
M scripts/gen-stdlib
M stdlib.mk
A format/ini/+test.ha => format/ini/+test.ha +75 -0
@@ 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);
};

A format/ini/scan.ha => format/ini/scan.ha +92 -0
@@ 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
};

A format/ini/types.ha => format/ini/types.ha +21 -0
@@ 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);
};

M scripts/gen-stdlib => scripts/gen-stdlib +18 -0
@@ 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

M stdlib.mk => stdlib.mk +31 -0
@@ 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 \