hare

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

commit d11b0f794ec768acf0468440dba1f74f3311c486
parent f27664d115053f4f325050e4d53bfa4c8110ddff
Author: Mykyta Holubakha <hilobakho@gmail.com>
Date:   Sun, 28 Mar 2021 19:42:55 +0300

net/ip: new module

Diffstat:
Anet/ip/+test.ha | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anet/ip/ip.ha | 328+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mscripts/gen-stdlib | 19+++++++++++++++++++
Mstdlib.mk | 27+++++++++++++++++++++++++++
4 files changed, 457 insertions(+), 0 deletions(-)

diff --git a/net/ip/+test.ha b/net/ip/+test.ha @@ -0,0 +1,83 @@ +use strings; +use fmt; + +fn ip_test(s: str, expected: (addr|invalid)) void = { + let pr = parse(s); + let ip = if (pr is invalid) { + assert(expected is invalid); + return; + } else { + assert(expected is addr); + assert(equal(pr as addr, expected as addr)); + pr as addr; + }; + let fmted = string(ip); + let iprp = parse(fmted); + assert(iprp is addr); + let ipr = iprp as addr; + assert(equal(ip, ipr)); + if (ip is addr4) { + assert(fmted == s); + } else { + assert(strings::dup(fmted) == string(ipr)); + }; +}; + +@test fn parse_ip() void = { + let tests: [](str, (addr|invalid)) = [ + ("127.0.0.1", [127, 0, 0, 1]: addr4), + ("192.168.18.1", [192, 168, 18, 1]: addr4), + ("-127.0.0.1", invalid), + ("127.-0.0.1", invalid), + ("::", [0...]: addr6), + ("::1", [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1]: addr6), + ("::FFFF:FFFF", [0,0,0,0,0,0,0,0,0,0,0,0,0xFF,0xFF,0xFF,0xFF]: addr6), + ("::FFFF", [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0xFF,0xFF]: addr6), + (":FFFF", invalid), + ("::1:1", [0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1]: addr6), + ("1::1", [0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1]: addr6), + (":::1:1", invalid), + (":::", invalid), + ("::1::1", invalid), + ("1::::1", invalid), + ("FFFF::FFFF::1", invalid), + ("::127.0.0.1", [0,0,0,0,0,0,0,0,0,0,0,0,127,0,0,1]: addr6), + ("FFFF:FFFF", invalid), + ("DEAD::BEef", [0xDE, 0xAD, 0,0,0,0,0,0,0,0,0,0,0,0, 0xBE, 0xEF]: addr6), + ("DEAD::BEef:A12D", [0xDE, 0xAD, 0,0,0,0,0,0,0,0,0,0, 0xBE, 0xEF, 0xA1, 0x2D]: addr6), + ("DEAD:BEef::0102:A12D", [0xDE, 0xAD,0xBE,0xEF,0,0,0,0,0,0,0,0,0x01,0x02,0xA1,0x2D]: addr6), + ("DEAD:BEef:::A12D", invalid), + ("1980:cafe:a:babe::1", [0x19, 0x80, 0xca, 0xfe, 0x0, 0xa, 0xba, 0xbe, 0, 0, 0, 0, 0, 0, 0, 1]: addr6), + ("a1:a2:a3:a4::b1:b2:b3:b4", invalid), + ("", invalid), + ]; + for (let i = 0z; i < len(tests); i += 1) { + ip_test(tests[i].0, tests[i].1); + }; +}; + +fn subnet_test_simple(s: str) void = { + let net = match (parsecidr(s)) { + a: subnet => a, + * => return, + }; + let fmted = stringsubnet(net); + assert(fmted == s); + let netrp = parsecidr(fmted); + assert(netrp is subnet); + let netr = netrp as subnet; + assert(equal(net.addr, netr.addr)); + assert(equal(net.mask, netr.mask)); +}; + +@test fn parse_subnet() void = { + let subnet_tests: []str = [ + "192.168.1.0/0", + "192.168.1.0/23", + "192.168.1.0/24", + "192.168.1.0/32", + ]; + for (let i = 0z; i < len(subnet_tests); i += 1) { + subnet_test_simple(subnet_tests[i]); + }; +}; diff --git a/net/ip/ip.ha b/net/ip/ip.ha @@ -0,0 +1,328 @@ +use io; +use strio; +use fmt; +use bytes; +use rt; +use strconv; +use strings; + +// An IPv4 address. +export type addr4 = [4]u8; + +// An IPv6 address. +export type addr6 = [16]u8; + +// An IP address. +export type addr = (addr4 | addr6); + +// An IP subnet. +export type subnet = struct { + addr: addr, + mask: addr, +}; + +// Invalid parse result. +export type invalid = void!; + +// Test if two [addr]s are equal. +export fn equal(l: addr, r: addr) bool = { + return match (l) { + l: addr4 => { + if (!(r is addr4)) { + return false; + }; + let r = r as addr4; + bytes::equal(l, r); + }, + l: addr6 => { + if (!(r is addr6)) { + return false; + }; + let r = r as addr6; + bytes::equal(l, r); + }, + }; +}; + +export fn parsev4(st: str) (addr4 | invalid) = { + let ret: addr4 = [0...]; + let tok = strings::tokenize(st, "."); + let i = 0z; + for (i < 4; i += 1) { + let s = wanttoken(&tok)?; + ret[i] = match (strconv::stou8(s)) { + term: u8 => term, + * => return invalid + }; + }; + if (i < 4 || !(strings::next_token(&tok) is void)) { + return invalid; + }; + return ret; +}; + +fn parsev6(st: str) (addr6 | invalid) = { + let ret: addr6 = [0...]; + let tok = strings::tokenize(st, ":"); + if (st == "::") { + return ret; + }; + let ells = -1; + if (strings::has_prefix(st, "::")) { + wanttoken(&tok)?; + wanttoken(&tok)?; + ells = 0; + } else if (strings::has_prefix(st, ":")) { + return invalid; + }; + let i = 0; + for (i < 16) { + let s = match (strings::next_token(&tok)) { + s: str => s, + void => break, + }; + if (s == "") { + if (ells != -1) { + return invalid; + }; + ells = i; + continue; + }; + let val = strconv::stou16b(s, 16); + if (val is u16) { + let v = val as u16; + ret[i] = (v >> 8): u8; + i += 1; + ret[i] = v: u8; + i += 1; + continue; + } else { + let v4 = parsev4(s)?; + rt::memcpy(&ret[i], &v4, 4); + i += 4; + break; + }; + return invalid; + }; + if (!(strings::next_token(&tok) is void)) { + return invalid; + }; + if (ells >= 0) { + if (i >= 15) + return invalid; + rt::memcpy( + &ret[16 - (i - ells)], + &ret[ells], (i - ells): size); + rt::memset(&ret[ells], 0, (i - ells): size); + } else { + if (i != 16) + return invalid; + }; + + return ret; +}; + + +// Parses an IP address. +export fn parse(s: str) (addr | invalid) = { + match(parsev4(s)) { + v4: addr4 => return v4, + }; + match(parsev6(s)) { + v6: addr6 => return v6, + }; + return invalid; +}; + +fn fmtv4(a: addr4, s: *io::stream) (io::error | size) = { + let ret = 0z; + for (let i = 0; i < 4; i += 1) { + if (i > 0) { + ret += fmt::fprintf(s, ".")?; + }; + ret += fmt::fprintf(s, "{}", a[i])?; + }; + return ret; +}; + +fn fmtv6(a: addr6, s: *io::stream) (io::error | size) = { + let ret = 0z; + let zstart: int = -1; + let zend: int = -1; + for (let i = 0; i < 16; i += 2) { + let j = i; + for (j < 16 && a[j] == 0 && a[j + 1] == 0) { + j += 2; + }; + + if (j > i && j - i > zend - zstart) { + zstart = i; + zend = j; + i = j; + }; + }; + + if (zend - zstart <= 2) { + zstart = -1; + zend = -1; + }; + + for (let i = 0; i < 16; i += 2) { + if (i == zstart) { + ret += fmt::fprintf(s, "::")?; + i = zend; + if (i >= 16) + break; + } else if (i > 0) { + ret += fmt::fprintf(s, ":")?; + }; + let term = (a[i]: u16) << 8 | a[i + 1]; + ret += fmt::fprintf(s, "{:x}", term)?; + }; + return ret; +}; + +// Formats an IP address +export fn fmt(addr: addr, s: *io::stream) (io::error | size) = { + return match (addr) { + v4: addr4 => fmtv4(v4, s)?, + v6: addr6 => fmtv6(v6, s)?, + }; +}; + +// Formats an [addr] as a string. The return value is statically +// allocated and will be overwritten on subsequent calls; see +// [strings::dup] to extend its lifetime. +export fn string(addr: addr) str = { + // Maximum length of an IPv6 address is 45 chars + static let buf: [46]u8 = [0...]; + let stream = strio::fixed(buf); + fmt(addr, stream) as size; + return strio::string(stream); +}; + +// Fills a netmask according to the CIDR value +// e.g. 23 -> [0xFF, 0xFF, 0xFD, 0x00] +fn fillmask(mask: []u8, val: u8) void = { + rt::memset(&mask[0], 0xFF, len(mask)); + let i: int = len(mask): int - 1; + val = 32 - val; + for (val >= 8) { + mask[i] = 0x00; + val -= 8; + i -= 1; + }; + if (i >= 0) { + mask[i] = ~((1 << val) - 1); + }; +}; + +// Returns an addr representing a netmask +fn cidrmask(addr: addr, val: u8) (addr | invalid) = { + let a_len: u8 = match (addr) { + addr4 => 4, + addr6 => 16, + }; + + if (val > 8 * a_len) + return invalid; + if (a_len == 4) { + let ret: addr4 = [0...]; + fillmask(ret[..], val); + return ret; + }; + if (a_len == 16) { + let ret: addr6 = [0...]; + fillmask(ret[..], val); + return ret; + }; + return invalid; +}; + +// Parse an IP subnet in CIDR notation e.g. 192.168.1.0/24 +export fn parsecidr(st: str) (subnet | invalid) = { + let tok = strings::tokenize(st, "/"); + let ips = wanttoken(&tok)?; + let addr = parse(ips)?; + let masks = wanttoken(&tok)?; + let val = match (strconv::stou8(masks)) { + x: u8 => x, + * => return invalid, + }; + if (!(strings::next_token(&tok) is void)) { + return invalid; + }; + return subnet { + addr = addr, + mask = cidrmask(addr, val)? + }; +}; + +fn masklen(addr: []u8) (void | size) = { + let n = 0z; + for (let i = 0z; i < len(addr); i += 1) { + if (addr[i] == 0xff) { + n += 8; + continue; + }; + let val = addr[i]; + for (val & 0x80 != 0) { + n += 1; + val <<= 1; + }; + if (val != 0) + return; + for (let j = i + 1; j < len(addr); j += 1) { + if (addr[j] != 0) + return; + }; + break; + }; + return n; +}; + +fn fmtmask(mask: addr, s: *io::stream) (io::error | size) = { + let ret = 0z; + let slice = match (mask) { + v4: addr4 => v4[..], + v6: addr6 => v6[..], + }; + match (masklen(slice)) { + // format as hex, if zero runs are not contiguous + // (like golang does) + void => { + for (let i = 0z; i < len(slice); i += 1) { + ret += fmt::fprintf(s, "{:x}", slice[i])?; + }; + }, + // standard CIDR integer + n: size => ret += fmt::fprintf(s, "{}", n)?, + }; + return ret; +}; + +// Format an IP subnet +export fn fmtsubnet(subnet: subnet, s: *io::stream) (io::error | size) = { + let ret = 0z; + ret += fmt(subnet.addr, s)?; + ret += fmt::fprintf(s, "/")?; + ret += fmtmask(subnet.mask, s)?; + return ret; +}; + +// Formats a [subnet] as a string. The return value is statically +// allocated and will be overwritten on subsequent calls; see +// [strings::dup] to extend its lifetime. +export fn stringsubnet(subnet: subnet) str = { + static let buf: [66]u8 = [0...]; + let stream = strio::fixed(buf); + fmtsubnet(subnet, stream) as size; + return strio::string(stream); +}; + +fn wanttoken(tok: *strings::tokenizer) (str | invalid) = { + return match (strings::next_token(tok)) { + s: str => s, + void => invalid + }; +}; diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib @@ -394,6 +394,24 @@ linux_vdso() { gen_ssa linux::vdso linux strings format::elf } +gensrcs_net_ip() { + gen_srcs net::ip \ + ip.ha \ + $* +} + +net_ip() { + printf '# net::ip\n' + if [ $testing -eq 0 ] + then + gensrcs_net_ip + else + gensrcs_net_ip \ + +test.ha + fi + gen_ssa net::ip bytes io strconv strings strio +} + math_random() { printf '# math::random\n' gen_srcs math::random \ @@ -588,6 +606,7 @@ hash_fnv io linux linux_vdso +net_ip math_random os os_exec diff --git a/stdlib.mk b/stdlib.mk @@ -147,6 +147,9 @@ hare_stdlib_deps+=$(stdlib_linux) stdlib_linux_vdso=$(HARECACHE)/linux/vdso/linux_vdso.o hare_stdlib_deps+=$(stdlib_linux_vdso) +stdlib_net_ip=$(HARECACHE)/net/ip/net_ip.o +hare_stdlib_deps+=$(stdlib_net_ip) + stdlib_math_random=$(HARECACHE)/math/random/math_random.o hare_stdlib_deps+=$(stdlib_math_random) @@ -499,6 +502,16 @@ $(HARECACHE)/linux/vdso/linux_vdso.ssa: $(stdlib_linux_vdso_srcs) $(stdlib_rt) $ @HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Nlinux::vdso \ -t$(HARECACHE)/linux/vdso/linux_vdso.td $(stdlib_linux_vdso_srcs) +# net::ip +stdlib_net_ip_srcs= \ + $(STDLIB)/net/ip/ip.ha + +$(HARECACHE)/net/ip/net_ip.ssa: $(stdlib_net_ip_srcs) $(stdlib_rt) $(stdlib_bytes) $(stdlib_io) $(stdlib_strconv) $(stdlib_strings) $(stdlib_strio) + @printf 'HAREC \t$@\n' + @mkdir -p $(HARECACHE)/net/ip + @HARECACHE=$(HARECACHE) $(HAREC) $(HAREFLAGS) -o $@ -Nnet::ip \ + -t$(HARECACHE)/net/ip/net_ip.td $(stdlib_net_ip_srcs) + # math::random stdlib_math_random_srcs= \ $(STDLIB)/math/random/random.ha @@ -837,6 +850,9 @@ hare_testlib_deps+=$(testlib_linux) testlib_linux_vdso=$(TESTCACHE)/linux/vdso/linux_vdso.o hare_testlib_deps+=$(testlib_linux_vdso) +testlib_net_ip=$(TESTCACHE)/net/ip/net_ip.o +hare_testlib_deps+=$(testlib_net_ip) + testlib_math_random=$(TESTCACHE)/math/random/math_random.o hare_testlib_deps+=$(testlib_math_random) @@ -1197,6 +1213,17 @@ $(TESTCACHE)/linux/vdso/linux_vdso.ssa: $(testlib_linux_vdso_srcs) $(testlib_rt) @HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nlinux::vdso \ -t$(TESTCACHE)/linux/vdso/linux_vdso.td $(testlib_linux_vdso_srcs) +# net::ip +testlib_net_ip_srcs= \ + $(STDLIB)/net/ip/ip.ha \ + $(STDLIB)/net/ip/+test.ha + +$(TESTCACHE)/net/ip/net_ip.ssa: $(testlib_net_ip_srcs) $(testlib_rt) $(testlib_bytes) $(testlib_io) $(testlib_strconv) $(testlib_strings) $(testlib_strio) + @printf 'HAREC \t$@\n' + @mkdir -p $(TESTCACHE)/net/ip + @HARECACHE=$(TESTCACHE) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nnet::ip \ + -t$(TESTCACHE)/net/ip/net_ip.td $(testlib_net_ip_srcs) + # math::random testlib_math_random_srcs= \ $(STDLIB)/math/random/random.ha