code wiki / (root) / content_type.nx

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}