nx_content_type.nx source
↩ module page · 173 lines · 5463 B
1// content_type.nx -- parse a Content-Type / Accept header value.
2//
3// RFC 7231 ยง3.1.1.1. A media-type value is:
4// type "/" subtype *( ";" param "=" value )
5//
6// Examples:
7// text/html
8// text/html; charset=utf-8
9// multipart/form-data; boundary=----WebKitFormBoundary
10// application/vnd.nishi.page+json; profile=blog
11//
12// Parses this into (type_off, type_len, subtype_off, subtype_len,
13// params_off, params_len) triplets. Parameter list is left as a
14// raw byte range -- caller can feed it into cookie.nx or query.nx
15// style splitters if it needs per-param access. For most
16// routing, knowing the type + subtype is enough.
17//
18// The Accept header uses the same grammar with an additional
19// "q=" parameter for quality (0..1). We surface the parsed
20// param range so caller can extract "q" on demand.
21//
22// Invariants:
23// CT1 Type + subtype trimmed of surrounding whitespace.
24// CT2 Type + subtype lowercased-in-place is CALLER's job
25// (we're zero-alloc; won't mutate the buffer).
26// CT3 Missing '/' returns CT_ERR_FORMAT; media types always
27// have a slash.
28// CT4 "*/*", "text/*", "application/*": accepted as-is; caller
29// inspects for '*' to know whether it's a wildcard.
30
31// nx_safety_envelope:
32// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
33// sil_target: SIL1
34// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
35// verdict: NOT_YET_EVALUATED
36
37import "nx_syscalls.nx"
38
39const CT_ERR_FORMAT: i64 = -1
40
41struct ContentType {
42 type_off: i64, type_len: i64,
43 subtype_off: i64, subtype_len: i64,
44 // Everything after the first ';' (inclusive of leading ; -- no,
45 // we strip the semicolon; params_len = 0 if none).
46 params_off: i64, params_len: i64,
47}
48
49func ct_is_ws(b: i64) -> i64 {
50 if b == 0x20 { return 1 }
51 if b == 0x09 { return 1 }
52 return 0
53}
54
55func ct_ltrim(buf: *u8, off: i64, end: i64) -> i64 {
56 var i: i64 = off
57 while i < end {
58 if ct_is_ws(buf[i]) == 0 { return i }
59 i = i + 1
60 }
61 return end
62}
63
64func ct_rtrim(buf: *u8, start: i64, end: i64) -> i64 {
65 var e: i64 = end
66 while e > start {
67 if ct_is_ws(buf[e - 1]) == 0 { return e }
68 e = e - 1
69 }
70 return e
71}
72
73// Forward declaration (NishiLang requires decl before use).
74func if_neg_else(a: i64, b: i64) -> i64;
75
76// Parse a single media-type value. Returns 0 on success.
77func content_type_parse(buf: *u8, n: i64, ct: *ContentType) -> i64 {
78 let start: i64 = ct_ltrim(buf, 0, n)
79 let end: i64 = ct_rtrim(buf, start, n)
80
81 // Find '/'.
82 var slash: i64 = -1
83 var i: i64 = start
84 while i < end {
85 if buf[i] == 0x2F {
86 slash = i
87 break
88 }
89 i = i + 1
90 }
91 if slash < 0 { return CT_ERR_FORMAT }
92
93 // Find ';' (optional).
94 var semi: i64 = -1
95 i = slash + 1
96 while i < end {
97 if buf[i] == 0x3B {
98 semi = i
99 break
100 }
101 i = i + 1
102 }
103
104 let subtype_end: i64 = if_neg_else(semi, end)
105 ct.type_off = start
106 ct.type_len = ct_rtrim(buf, start, slash) - start
107 ct.subtype_off = slash + 1
108 ct.subtype_len = ct_rtrim(buf, slash + 1, subtype_end) - (slash + 1)
109 if semi < 0 {
110 ct.params_off = 0
111 ct.params_len = 0
112 } else {
113 let params_start: i64 = ct_ltrim(buf, semi + 1, end)
114 ct.params_off = params_start
115 ct.params_len = end - params_start
116 }
117 return 0
118}
119
120// Helper: return a if a >= 0 else b. NishiLang doesn't have
121// short-circuit tertiary so we inline.
122func if_neg_else(a: i64, b: i64) -> i64 {
123 if a < 0 { return b }
124 return a
125}
126
127// Byte-exact compare of the parsed type/subtype against a literal.
128// Returns 1 on match. Caller is responsible for lowercasing if
129// they want case-insensitive match (HTTP media types are
130// case-insensitive per RFC but most servers emit lowercase).
131func content_type_is(buf: *u8, ct: *ContentType,
132 type_lit: *u8, type_len: i64,
133 sub_lit: *u8, sub_len: i64) -> i64 {
134 if ct.type_len != type_len { return 0 }
135 if ct.subtype_len != sub_len { return 0 }
136 var i: i64 = 0
137 while i < type_len {
138 if buf[ct.type_off + i] != type_lit[i] { return 0 }
139 i = i + 1
140 }
141 i = 0
142 while i < sub_len {
143 if buf[ct.subtype_off + i] != sub_lit[i] { return 0 }
144 i = i + 1
145 }
146 return 1
147}
148
149// Compile-only smoke.
150func main() -> i64 {
151 let ct_raw: *u8 = sys_mmap(64)
152 let ct: *ContentType = ct_raw as *ContentType
153
154 // Simple: "text/html"
155 if content_type_parse("text/html", 9, ct) != 0 { return 1 }
156 if ct.type_len != 4 { return 2 }
157 if ct.subtype_len != 4 { return 3 }
158 if content_type_is("text/html", ct, "text", 4, "html", 4) != 1 { return 4 }
159 if content_type_is("text/html", ct, "text", 4, "xml", 3) != 0 { return 5 }
160
161 // With parameters: "text/html; charset=utf-8"
162 if content_type_parse("text/html; charset=utf-8", 24, ct) != 0 { return 6 }
163 if ct.type_len != 4 { return 7 }
164 if ct.subtype_len != 4 { return 8 }
165 if ct.params_len != 14 { return 9 } // "charset=utf-8"
166 // Verify params starts at 'c' of charset.
167 let src: *u8 = "text/html; charset=utf-8"
168 if src[ct.params_off] != 0x63 { return 10 } // 'c'
169
170 // Missing slash -> error.
171 if content_type_parse("textwtf", 7, ct) != CT_ERR_FORMAT { return 11 }
172 return 0
173}