hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

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:
Mnet/dial/registry.ha | 6+++++-
Mnet/dial/resolve.ha | 2+-
Mscripts/gen-stdlib | 37++++++++++++++++++++++++++++---------
Mstdlib.mk | 18++++++++++--------
Aunix/hosts/hosts.ha | 129+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dunix/hosts/lookup.ha | 57---------------------------------------------------------
Aunix/hosts/test+test.ha | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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)); +};