code wiki / (root) / nx_content_type.nx

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}