commit e89800188d6151b03c6872835aecb555d0a54c51
parent 113cdf3fd568feaa6ddbc42fb0df5a2d03ac2fb9
Author: Dmitry Matveyev <public@greenfork.me>
Date: Sun, 11 Jun 2023 11:28:06 +0600
unix::hosts: refactor into more general interface
Main changes:
* Add types: host, iterator, !error, !invalid
* Add functions: next, iter, iter_lookup, strerror, finish
* Add proper error handling
* Add tests
References: https://todo.sr.ht/~sircmpwn/hare/442
Signed-off-by: Dmitry Matveyev <public@greenfork.me>
Diffstat:
7 files changed, 242 insertions(+), 76 deletions(-)
diff --git a/net/dial/registry.ha b/net/dial/registry.ha
@@ -3,6 +3,7 @@
// (c) 2021 Ember Sawady <ecs@d2evs.net>
use net;
use net::dns;
+use unix::hosts;
// Returned if the address parameter was invalid, for example if it specifies an
// invalid port number.
@@ -13,7 +14,8 @@ export type invalid_address = !void;
export type unknown_service = !void;
// Errors which can occur from dial.
-export type error = !(invalid_address | unknown_service | net::error | dns::error);
+export type error = !(invalid_address | unknown_service | net::error | dns::error
+ | hosts::error);
// Converts an [[error]] to a human-readable string.
export fn strerror(err: error) const str = {
@@ -27,6 +29,8 @@ export fn strerror(err: error) const str = {
return net::strerror(err);
case let err: dns::error =>
return dns::strerror(err);
+ case let err: hosts::error =>
+ return hosts::strerror(err);
};
};
diff --git a/net/dial/resolve.ha b/net/dial/resolve.ha
@@ -86,7 +86,7 @@ fn resolve_addr(addr: str) ([]ip::addr | error) = {
case ip::invalid => void;
};
- const addrs = hosts::lookup(addr);
+ const addrs = hosts::lookup(addr)?;
if (len(addrs) != 0) {
return addrs;
};
diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib
@@ -1466,15 +1466,34 @@ unix() {
}
unix_hosts() {
- gen_srcs -plinux unix::hosts \
- +linux.ha \
- lookup.ha
- gen_ssa -plinux unix::hosts os io bufio net::ip strings
-
- gen_srcs -pfreebsd unix::hosts \
- +freebsd.ha \
- lookup.ha
- gen_ssa -pfreebsd unix::hosts os io bufio net::ip strings
+ if [ $testing -eq 0 ]
+ then
+ gen_srcs -plinux unix::hosts \
+ +linux.ha \
+ hosts.ha
+ gen_ssa -plinux unix::hosts bufio encoding::utf8 fs io \
+ net::ip os strings
+
+ gen_srcs -pfreebsd unix::hosts \
+ +freebsd.ha \
+ hosts.ha
+ gen_ssa -pfreebsd unix::hosts bufio encoding::utf8 fs io \
+ net::ip os strings
+ else
+ gen_srcs -plinux unix::hosts \
+ +linux.ha \
+ test+test.ha \
+ hosts.ha
+ gen_ssa -plinux unix::hosts bufio encoding::utf8 fs io \
+ net::ip os strings
+
+ gen_srcs -pfreebsd unix::hosts \
+ +freebsd.ha \
+ test+test.ha \
+ hosts.ha
+ gen_ssa -pfreebsd unix::hosts bufio encoding::utf8 fs io \
+ net::ip os strings
+ fi
}
unix_passwd() {
diff --git a/stdlib.mk b/stdlib.mk
@@ -2269,9 +2269,9 @@ $(HARECACHE)/unix/unix-freebsd.ssa: $(stdlib_unix_freebsd_srcs) $(stdlib_rt) $(s
# unix::hosts (+linux)
stdlib_unix_hosts_linux_srcs = \
$(STDLIB)/unix/hosts/+linux.ha \
- $(STDLIB)/unix/hosts/lookup.ha
+ $(STDLIB)/unix/hosts/hosts.ha
-$(HARECACHE)/unix/hosts/unix_hosts-linux.ssa: $(stdlib_unix_hosts_linux_srcs) $(stdlib_rt) $(stdlib_os_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
+$(HARECACHE)/unix/hosts/unix_hosts-linux.ssa: $(stdlib_unix_hosts_linux_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
@printf 'HAREC \t$@\n'
@mkdir -p $(HARECACHE)/unix/hosts
@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nunix::hosts \
@@ -2280,9 +2280,9 @@ $(HARECACHE)/unix/hosts/unix_hosts-linux.ssa: $(stdlib_unix_hosts_linux_srcs) $(
# unix::hosts (+freebsd)
stdlib_unix_hosts_freebsd_srcs = \
$(STDLIB)/unix/hosts/+freebsd.ha \
- $(STDLIB)/unix/hosts/lookup.ha
+ $(STDLIB)/unix/hosts/hosts.ha
-$(HARECACHE)/unix/hosts/unix_hosts-freebsd.ssa: $(stdlib_unix_hosts_freebsd_srcs) $(stdlib_rt) $(stdlib_os_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
+$(HARECACHE)/unix/hosts/unix_hosts-freebsd.ssa: $(stdlib_unix_hosts_freebsd_srcs) $(stdlib_rt) $(stdlib_bufio_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_net_ip_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_strings_$(PLATFORM))
@printf 'HAREC \t$@\n'
@mkdir -p $(HARECACHE)/unix/hosts
@$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nunix::hosts \
@@ -4742,9 +4742,10 @@ $(TESTCACHE)/unix/unix-freebsd.ssa: $(testlib_unix_freebsd_srcs) $(testlib_rt) $
# unix::hosts (+linux)
testlib_unix_hosts_linux_srcs = \
$(STDLIB)/unix/hosts/+linux.ha \
- $(STDLIB)/unix/hosts/lookup.ha
+ $(STDLIB)/unix/hosts/test+test.ha \
+ $(STDLIB)/unix/hosts/hosts.ha
-$(TESTCACHE)/unix/hosts/unix_hosts-linux.ssa: $(testlib_unix_hosts_linux_srcs) $(testlib_rt) $(testlib_os_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
+$(TESTCACHE)/unix/hosts/unix_hosts-linux.ssa: $(testlib_unix_hosts_linux_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
@printf 'HAREC \t$@\n'
@mkdir -p $(TESTCACHE)/unix/hosts
@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nunix::hosts \
@@ -4753,9 +4754,10 @@ $(TESTCACHE)/unix/hosts/unix_hosts-linux.ssa: $(testlib_unix_hosts_linux_srcs) $
# unix::hosts (+freebsd)
testlib_unix_hosts_freebsd_srcs = \
$(STDLIB)/unix/hosts/+freebsd.ha \
- $(STDLIB)/unix/hosts/lookup.ha
+ $(STDLIB)/unix/hosts/test+test.ha \
+ $(STDLIB)/unix/hosts/hosts.ha
-$(TESTCACHE)/unix/hosts/unix_hosts-freebsd.ssa: $(testlib_unix_hosts_freebsd_srcs) $(testlib_rt) $(testlib_os_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
+$(TESTCACHE)/unix/hosts/unix_hosts-freebsd.ssa: $(testlib_unix_hosts_freebsd_srcs) $(testlib_rt) $(testlib_bufio_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_net_ip_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_strings_$(PLATFORM))
@printf 'HAREC \t$@\n'
@mkdir -p $(TESTCACHE)/unix/hosts
@$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nunix::hosts \
diff --git a/unix/hosts/hosts.ha b/unix/hosts/hosts.ha
@@ -0,0 +1,129 @@
+// License: MPL-2.0
+// (c) 2023 Dmitry Matveyev <public@greenfork.me>
+// (c) 2022 Alexey Yerin <yyp@disroot.org>
+// (c) 2021 Drew DeVault <sir@cmpwn.com>
+// (c) 2021 Ember Sawady <ecs@d2evs.net>
+use bufio;
+use encoding::utf8;
+use fs;
+use io;
+use net::ip;
+use os;
+use strings;
+
+// Represents a host line in /etc/hosts, guaranteed to have at least a single
+// name. The first name is the canonical one.
+export type host = struct {
+ addr: ip::addr,
+ names: []str,
+};
+
+// Iterator through the host lines in a host file.
+export type iterator = struct {
+ handle: io::handle,
+};
+
+// Returned when an invalid host line was found.
+export type invalid = !void;
+
+// All possible errors returned from this module.
+export type error = !(io::error | invalid | utf8::invalid | ip::invalid
+ | fs::error);
+
+// Converts an [[error]] to a human-friendly representation.
+export fn strerror(err: error) const str = match (err) {
+case invalid =>
+ return "Host file format is invalid";
+case utf8::invalid =>
+ return "File is invalid UTF-8";
+case ip::invalid =>
+ return "IP address is invalid";
+case let err: io::error =>
+ return io::strerror(err);
+case let err: fs::error =>
+ return fs::strerror(err);
+};
+
+// Creates an [[hosts::iterator]] for a provided [[io::handle]] pointing to
+// the /etc/hosts file. The user should call [[hosts::next]] to iterate through
+// host lines.
+export fn iter(in: io::handle) iterator = iterator {
+ handle = in,
+};
+
+// Returns the next host line as a [[host]] type.
+export fn next(it: *iterator) (host | void | error) = for (true) {
+ const line = match (bufio::scanline(it.handle)) {
+ case io::EOF =>
+ return void;
+ case let line: []u8 =>
+ yield line;
+ };
+ defer free(line);
+ if (len(line) == 0 || line[0] == '#') {
+ continue;
+ };
+
+ const scanner = bufio::fixed(line, io::mode::READ);
+ const tok = match (bufio::scantok(&scanner, ' ', '\t')?) {
+ case io::EOF =>
+ return void;
+ case let tok: []u8 =>
+ yield tok;
+ };
+ defer free(tok);
+ const addr = ip::parse(strings::fromutf8(tok)?)?;
+
+ let names: []str = [];
+ for (true) {
+ const tok = match (bufio::scantok(&scanner, ' ', '\t')?) {
+ case io::EOF =>
+ break;
+ case let tok: []u8 =>
+ yield tok;
+ };
+ if (len(tok) == 0) continue;
+
+ append(names, strings::fromutf8(tok)?);
+ };
+ if (len(names) == 0) {
+ return invalid;
+ };
+
+ return host {
+ addr = addr,
+ names = names,
+ };
+};
+
+// Looks up a slice of addresses from /etc/hosts.
+export fn lookup(name: const str) ([]ip::addr | error) = {
+ const file = os::open(PATH)?;
+ defer io::close(file)!;
+ let it = iter(file);
+ return iter_lookup(&it, name);
+};
+
+// Looks up a slice of addresses given an [[iterator]] to /etc/hosts.
+export fn iter_lookup(it: *iterator, name: const str) ([]ip::addr | error) = {
+ let addrs: []ip::addr = [];
+ for (true) match(next(it)?) {
+ case void => break;
+ case let h: host =>
+ defer finish(h);
+ for (let i = 0z; i < len(h.names); i += 1) {
+ if (h.names[i] == name) {
+ append(addrs, h.addr);
+ };
+ };
+ };
+ return addrs;
+};
+
+// Frees resources associated with a [[host]].
+export fn finish(host: host) void = {
+ for (let i = 0z; i < len(host.names); i += 1) {
+ free(host.names[i]);
+ };
+ free(host.names);
+};
diff --git a/unix/hosts/lookup.ha b/unix/hosts/lookup.ha
@@ -1,57 +0,0 @@
-// License: MPL-2.0
-// (c) 2022 Alexey Yerin <yyp@disroot.org>
-// (c) 2021 Drew DeVault <sir@cmpwn.com>
-// (c) 2021 Ember Sawady <ecs@d2evs.net>
-use bufio;
-use io;
-use net::ip;
-use os;
-use strings;
-
-// Looks up a host from /etc/hosts. Aborts the program if the file does not
-// exist, is written in an invalid format, or if any other error occurs.
-export fn lookup(name: str) []ip::addr = {
- // XXX: Would be cool if we could do this without allocating anything
- // XXX: Would be cool to have meaningful error handling(?)
- const file = os::open(PATH)!;
- defer io::close(file)!;
-
- let addrs: []ip::addr = [];
- for (true) {
- const line = match (bufio::scanline(file)) {
- case io::EOF =>
- break;
- case let line: []u8 =>
- yield line;
- };
- defer free(line);
- if (len(line) == 0 || line[0] == '#') {
- continue;
- };
-
- const scanner = bufio::fixed(line, io::mode::READ);
- const tok = match (bufio::scantok(&scanner, ' ', '\t')!) {
- case io::EOF =>
- break;
- case let tok: []u8 =>
- yield tok;
- };
- defer free(tok);
- const addr = ip::parse(strings::fromutf8(tok)!)!;
-
- for (true) {
- const tok = match (bufio::scantok(&scanner, ' ', '\t')!) {
- case io::EOF =>
- break;
- case let tok: []u8 =>
- yield tok;
- };
- defer free(tok);
-
- if (strings::fromutf8(tok)! == name) {
- append(addrs, addr);
- };
- };
- };
- return addrs;
-};
diff --git a/unix/hosts/test+test.ha b/unix/hosts/test+test.ha
@@ -0,0 +1,69 @@
+// License: MPL-2.0
+// (c) 2023 Dmitry Matveyev <public@greenfork.me>
+use bufio;
+use io;
+use net::ip;
+use strings;
+
+def HOSTS_FILE = `
+127.0.0.1 localhost
+
+# The following lines are desirable for IPv6 capable hosts
+::1 ip6-localhost ip6-loopback
+
+10.10.10.10 other.localdomain
+10.10.20.20 other.localdomain
+`;
+
+@test fn next() void = {
+ let buf = bufio::fixed(strings::toutf8(HOSTS_FILE), io::mode::READ);
+ let it = iter(&buf);
+
+ const h = next(&it) as host;
+ defer finish(h);
+ assert(ip::equal(h.addr, ip::LOCAL_V4));
+ assert(len(h.names) == 1);
+ assert(h.names[0] == "localhost");
+
+ const h = next(&it) as host;
+ defer finish(h);
+ assert(ip::equal(h.addr, ip::LOCAL_V6));
+ assert(len(h.names) == 2);
+ assert(h.names[0] == "ip6-localhost");
+ assert(h.names[1] == "ip6-loopback");
+
+ const h = next(&it) as host;
+ defer finish(h);
+ assert(ip::equal(h.addr, [10, 10, 10, 10]: ip::addr4));
+ assert(len(h.names) == 1);
+ assert(h.names[0] == "other.localdomain");
+
+ const h = next(&it) as host;
+ defer finish(h);
+ assert(ip::equal(h.addr, [10, 10, 20, 20]: ip::addr4));
+ assert(len(h.names) == 1);
+ assert(h.names[0] == "other.localdomain");
+
+ const h = next(&it);
+ assert(h is void);
+ const h = next(&it);
+ assert(h is void);
+};
+
+@test fn errors() void = {
+ const s = "127";
+ assert(next(&iter(&bufio::fixed(strings::toutf8(s), io::mode::READ)))
+ is ip::invalid);
+ const s = "127.0.0.1";
+ assert(next(&iter(&bufio::fixed(strings::toutf8(s), io::mode::READ)))
+ is invalid);
+};
+
+@test fn lookup() void = {
+ let buf = bufio::fixed(strings::toutf8(HOSTS_FILE), io::mode::READ);
+ let it = iter(&buf);
+ const addrs = iter_lookup(&it, "other.localdomain") as []ip::addr;
+ assert(len(addrs) == 2);
+ assert(ip::equal(addrs[0], [10, 10, 10, 10]: ip::addr4));
+ assert(ip::equal(addrs[1], [10, 10, 20, 20]: ip::addr4));
+};