code wiki / (root) / url.nx

url.nx source

↩ module page · 149 lines · 5412 B

1// url.nx -- URL percent-encoding + decoding (RFC 3986). 2// 3// Used by: HTTP query strings, path segments, form-urlencoded 4// POST bodies, JWT segments. Complements base64.nx / hex.nx for 5// the URL-safe text-encoding niche. 6// 7// Encoding rules (RFC 3986 §2.3): 8// Unreserved: A-Z a-z 0-9 - . _ ~ (encoded as-is) 9// Reserved: : / ? # [ ] @ ! $ & ' ( ) * + , ; = 10// -- reserved chars DEPEND on context. We offer two encoders: 11// url_encode_component -- encodes everything reserved 12// url_encode_path -- preserves / for path segments 13// All other bytes: encoded as %HH (two uppercase hex digits). 14// 15// Invariants: 16// UR1 Output is deterministic: same input -> same bytes. 17// UR2 Decoder rejects malformed sequences (%X or %XY where Y 18// isn't hex) with a negative return. No silent skip. 19// UR3 Decoder accepts both upper and lower case hex digits. 20// UR4 Round-trip exact for any byte sequence: 21// decode(encode(x)) == x 22// UR5 Encoder never outputs non-ASCII; caller always gets 23// 7-bit safe text. 24 25import "syscalls.nx" 26 27const URL_ERR_BAD_ESCAPE: i64 = -1 28const URL_ERR_TRUNCATED: i64 = -2 29 30// Is byte an unreserved RFC 3986 char (never needs encoding)? 31func url_unreserved(c: i64) -> i64 { 32 if c >= 0x41 { if c <= 0x5A { return 1 } } // A-Z 33 if c >= 0x61 { if c <= 0x7A { return 1 } } // a-z 34 if c >= 0x30 { if c <= 0x39 { return 1 } } // 0-9 35 if c == 0x2D { return 1 } // - 36 if c == 0x2E { return 1 } // . 37 if c == 0x5F { return 1 } // _ 38 if c == 0x7E { return 1 } // ~ 39 return 0 40} 41 42// Hex digit of a 0..15 value (uppercase, for URL convention). 43func url_hex_upper(n: i64) -> i64 { 44 if n < 10 { return 0x30 + n } // 0-9 45 return 0x41 + n - 10 // A-F 46} 47 48// Parse one hex digit ASCII -> 0..15, or -1 if invalid. 49func url_hex_val(c: i64) -> i64 { 50 if c >= 0x30 { if c <= 0x39 { return c - 0x30 } } 51 if c >= 0x41 { if c <= 0x46 { return c - 0x41 + 10 } } 52 if c >= 0x61 { if c <= 0x66 { return c - 0x61 + 10 } } 53 return -1 54} 55 56// Full encode: every non-unreserved byte percent-encoded. Use for 57// query-string values, path segments that might contain reserved 58// chars, form-urlencoded body (with plus-for-space handled separately). 59func url_encode_component(in_bytes: *u8, n: i64, out: *u8) -> i64 { 60 var i: i64 = 0 61 var out_pos: i64 = 0 62 while i < n { 63 let b: i64 = in_bytes[i] 64 if url_unreserved(b) == 1 { 65 out[out_pos] = b 66 out_pos = out_pos + 1 67 } else { 68 out[out_pos] = 0x25 // '%' 69 out[out_pos + 1] = url_hex_upper((b >> 4) & 0xF) 70 out[out_pos + 2] = url_hex_upper(b & 0xF) 71 out_pos = out_pos + 3 72 } 73 i = i + 1 74 } 75 return out_pos 76} 77 78// Path encoder: preserves '/' as a structural separator per 79// RFC 3986 §3.3. Every other non-unreserved byte gets encoded. 80func url_encode_path(in_bytes: *u8, n: i64, out: *u8) -> i64 { 81 var i: i64 = 0 82 var out_pos: i64 = 0 83 while i < n { 84 let b: i64 = in_bytes[i] 85 if url_unreserved(b) == 1 { 86 out[out_pos] = b 87 out_pos = out_pos + 1 88 } else { 89 if b == 0x2F { // '/' 90 out[out_pos] = b 91 out_pos = out_pos + 1 92 } else { 93 out[out_pos] = 0x25 94 out[out_pos + 1] = url_hex_upper((b >> 4) & 0xF) 95 out[out_pos + 2] = url_hex_upper(b & 0xF) 96 out_pos = out_pos + 3 97 } 98 } 99 i = i + 1 100 } 101 return out_pos 102} 103 104// Decoder: %HH sequences become their byte value; + is preserved 105// (caller handles form-urlencoded + -> space separately if needed). 106// Returns bytes written or a negative URL_ERR_*. 107func url_decode(in_bytes: *u8, n: i64, out: *u8) -> i64 { 108 var i: i64 = 0 109 var out_pos: i64 = 0 110 while i < n { 111 let b: i64 = in_bytes[i] 112 if b == 0x25 { // '%' 113 if i + 3 > n { return URL_ERR_TRUNCATED } 114 let h1: i64 = url_hex_val(in_bytes[i + 1]) 115 if h1 < 0 { return URL_ERR_BAD_ESCAPE } 116 let h2: i64 = url_hex_val(in_bytes[i + 2]) 117 if h2 < 0 { return URL_ERR_BAD_ESCAPE } 118 out[out_pos] = ((h1 << 4) | h2) & 0xFF 119 out_pos = out_pos + 1 120 i = i + 3 121 } else { 122 out[out_pos] = b 123 out_pos = out_pos + 1 124 i = i + 1 125 } 126 } 127 return out_pos 128} 129 130// Compile-only smoke. Expected: encode("Hello World!") -> 131// "Hello%20World%21" (12 chars -> 16 chars). 132func main() -> i64 { 133 let input: *u8 = "Hello World!" 134 let encoded: *u8 = sys_mmap(64) 135 let n_enc: i64 = url_encode_component(input, 12, encoded) 136 if n_enc != 16 { return 1 } 137 // Position 5 should be '%'. 138 if encoded[5] != 0x25 { return 2 } 139 // Position 6-7 should be '2' '0' (space encoded). 140 if encoded[6] != 0x32 { return 3 } 141 if encoded[7] != 0x30 { return 4 } 142 143 let decoded: *u8 = sys_mmap(32) 144 let n_dec: i64 = url_decode(encoded, n_enc, decoded) 145 if n_dec != 12 { return 5 } 146 if decoded[5] != 0x20 { return 6 } // space restored 147 if decoded[11] != 0x21 { return 7 } // '!' restored 148 return 0 149}