content_type.nx source
↩ module page · 167 lines · 5376 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
31import "syscalls.nx"
32
33const CT_ERR_FORMAT: i64 = -1
34
35struct ContentType {
36 type_off: i64, type_len: i64,
37 subtype_off: i64, subtype_len: i64,
38 // Everything after the first ';' (inclusive of leading ; -- no,
39 // we strip the semicolon; params_len = 0 if none).
40 params_off: i64, params_len: i64,
41}
42
43func ct_is_ws(b: i64) -> i64 {
44 if b == 0x20 { return 1 }
45 if b == 0x09 { return 1 }
46 return 0
47}
48
49func ct_ltrim(buf: *u8, off: i64, end: i64) -> i64 {
50 var i: i64 = off
51 while i < end {
52 if ct_is_ws(buf[i]) == 0 { return i }
53 i = i + 1
54 }
55 return end
56}
57
58func ct_rtrim(buf: *u8, start: i64, end: i64) -> i64 {
59 var e: i64 = end
60 while e > start {
61 if ct_is_ws(buf[e - 1]) == 0 { return e }
62 e = e - 1
63 }
64 return e
65}
66
67// Forward declaration (NishiLang requires decl before use).
68func if_neg_else(a: i64, b: i64) -> i64;
69
70// Parse a single media-type value. Returns 0 on success.
71func content_type_parse(buf: *u8, n: i64, ct: *ContentType) -> i64 {
72 let start: i64 = ct_ltrim(buf, 0, n)
73 let end: i64 = ct_rtrim(buf, start, n)
74
75 // Find '/'.
76 var slash: i64 = -1
77 var i: i64 = start
78 while i < end {
79 if buf[i] == 0x2F {
80 slash = i
81 break
82 }
83 i = i + 1
84 }
85 if slash < 0 { return CT_ERR_FORMAT }
86
87 // Find ';' (optional).
88 var semi: i64 = -1
89 i = slash + 1
90 while i < end {
91 if buf[i] == 0x3B {
92 semi = i
93 break
94 }
95 i = i + 1
96 }
97
98 let subtype_end: i64 = if_neg_else(semi, end)
99 ct.type_off = start
100 ct.type_len = ct_rtrim(buf, start, slash) - start
101 ct.subtype_off = slash + 1
102 ct.subtype_len = ct_rtrim(buf, slash + 1, subtype_end) - (slash + 1)
103 if semi < 0 {
104 ct.params_off = 0
105 ct.params_len = 0
106 } else {
107 let params_start: i64 = ct_ltrim(buf, semi + 1, end)
108 ct.params_off = params_start
109 ct.params_len = end - params_start
110 }
111 return 0
112}
113
114// Helper: return a if a >= 0 else b. NishiLang doesn't have
115// short-circuit tertiary so we inline.
116func if_neg_else(a: i64, b: i64) -> i64 {
117 if a < 0 { return b }
118 return a
119}
120
121// Byte-exact compare of the parsed type/subtype against a literal.
122// Returns 1 on match. Caller is responsible for lowercasing if
123// they want case-insensitive match (HTTP media types are
124// case-insensitive per RFC but most servers emit lowercase).
125func content_type_is(buf: *u8, ct: *ContentType,
126 type_lit: *u8, type_len: i64,
127 sub_lit: *u8, sub_len: i64) -> i64 {
128 if ct.type_len != type_len { return 0 }
129 if ct.subtype_len != sub_len { return 0 }
130 var i: i64 = 0
131 while i < type_len {
132 if buf[ct.type_off + i] != type_lit[i] { return 0 }
133 i = i + 1
134 }
135 i = 0
136 while i < sub_len {
137 if buf[ct.subtype_off + i] != sub_lit[i] { return 0 }
138 i = i + 1
139 }
140 return 1
141}
142
143// Compile-only smoke.
144func main() -> i64 {
145 let ct_raw: *u8 = sys_mmap(64)
146 let ct: *ContentType = ct_raw as *ContentType
147
148 // Simple: "text/html"
149 if content_type_parse("text/html", 9, ct) != 0 { return 1 }
150 if ct.type_len != 4 { return 2 }
151 if ct.subtype_len != 4 { return 3 }
152 if content_type_is("text/html", ct, "text", 4, "html", 4) != 1 { return 4 }
153 if content_type_is("text/html", ct, "text", 4, "xml", 3) != 0 { return 5 }
154
155 // With parameters: "text/html; charset=utf-8"
156 if content_type_parse("text/html; charset=utf-8", 24, ct) != 0 { return 6 }
157 if ct.type_len != 4 { return 7 }
158 if ct.subtype_len != 4 { return 8 }
159 if ct.params_len != 14 { return 9 } // "charset=utf-8"
160 // Verify params starts at 'c' of charset.
161 let src: *u8 = "text/html; charset=utf-8"
162 if src[ct.params_off] != 0x63 { return 10 } // 'c'
163
164 // Missing slash -> error.
165 if content_type_parse("textwtf", 7, ct) != CT_ERR_FORMAT { return 11 }
166 return 0
167}