fmt.ha (3120B)
1 // SPDX-License-Identifier: MPL-2.0 2 // (c) Hare authors <https://harelang.org> 3 4 use ascii; 5 use encoding::utf8; 6 use fmt; 7 use io; 8 use memio; 9 use net::ip; 10 use strings; 11 12 13 // Extract from RFC3986 ABNF 14 // URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] 15 // unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 16 // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "=" 17 // reg-name = *( unreserved / pct-encoded / sub-delims ) 18 // host = IP-literal / IPv4address / reg-name 19 // pchar = unreserved / pct-encoded / sub-delims / ":" / "@" 20 // query = *( pchar / "/" / "?" ) 21 // fragment = *( pchar / "/" / "?" ) 22 23 def unres_host: str = "-._~!$&'()*+,;="; 24 def unres_query_frag: str = "-._~!$&'()*+,;=:@/?"; 25 def unres_path: str = "-._~!$&'()*+,;=:@/"; 26 27 // Writes a formatted [[uri]] to an [[io::handle]]. Returns the number of bytes 28 // written. 29 export fn fmt(out: io::handle, u: *const uri) (size | io::error) = { 30 let n = 0z; 31 let slashes_w = false; 32 let has_host = false; 33 if (u.scheme != "") { 34 n += fmt::fprintf(out, "{}:", u.scheme)?; 35 }; 36 if (len(u.userinfo) > 0) { 37 assert(!(u.host is str) || len(u.host as str) > 0); 38 n += fmt::fprintf(out, "//{}@", u.userinfo)?; 39 slashes_w = true; 40 }; 41 match (u.host) { 42 case let host: str => 43 // file scheme is allowed an empty host 44 if (len(host) > 0 || u.scheme == "file") { 45 has_host = true; 46 if (!slashes_w) { 47 n += fmt::fprint(out, "//")?; 48 }; 49 let unres = if(u.scheme == "file") { 50 yield unres_path; 51 } else { 52 yield unres_host; 53 }; 54 n += percent_encode(out, host, unres)?; 55 }; 56 case let addr: ip::addr => 57 has_host = true; 58 if (!slashes_w) { 59 n += fmt::fprint(out, "//")?; 60 }; 61 n += fmtaddr(out, addr)?; 62 }; 63 if (u.port != 0) { 64 n += fmt::fprintf(out, ":{}", u.port)?; 65 }; 66 if (has_host && len(u.path) > 0 && !strings::hasprefix(u.path, '/')) { 67 n += fmt::fprint(out, "/")?; 68 }; 69 n += percent_encode(out, u.path, unres_path)?; 70 if (len(u.query) > 0) { 71 // Always percent-encoded, see parse and encodequery/decodequery 72 n += fmt::fprintf(out, "?{}", u.query)?; 73 }; 74 if (len(u.fragment) > 0) { 75 n += fmt::fprint(out, "#")?; 76 n += percent_encode(out, u.fragment, unres_query_frag)?; 77 }; 78 79 return n; 80 }; 81 82 fn fmtaddr(out: io::handle, addr: ip::addr) (size | io::error) = { 83 let n = 0z; 84 match (addr) { 85 case let addr: ip::addr4 => 86 n += ip::fmt(out, addr)?; 87 case let addr: ip::addr6 => 88 n += fmt::fprintf(out, "[")?; 89 n += ip::fmt(out, addr)?; 90 n += fmt::fprintf(out, "]")?; 91 }; 92 return n; 93 }; 94 95 fn percent_encode(out: io::handle, src: str, allowed: str) (size | io::error) = { 96 let iter = strings::iter(src); 97 let n = 0z; 98 for (let r => strings::next(&iter)) { 99 if (ascii::isalnum(r) || strings::contains(allowed, r)) { 100 n += fmt::fprint(out, r)?; 101 } else { 102 const en = utf8::encoderune(r)!; 103 for (let elem .. en) { 104 n += fmt::fprintf(out, "%{:X}", elem)?; 105 }; 106 }; 107 }; 108 return n; 109 }; 110 111 // Formats a [[uri]] into a string. The result must be freed by the caller. 112 export fn string(u: *const uri) str = { 113 const st = memio::dynamic(); 114 fmt(&st, u)!; 115 return memio::string(&st)!; 116 };