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