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}