1 # Checked typed inputs; bodies are consumed once, without caching or decompression. 2 import Base 3 import bend-kit-http@0.38.0.0/http.bend as Http 4 import bend-kit-router@0.2.0.0/target.bend as Target 5 import bend-kit-json@0.7.0.0/json.bend as Json 6 import bend-kit-bytes@0.3.3.0/bytes.bend as Bytes 7 import bend-kit-url@0.4.1.0/url.bend as Url 8 import ./response.bend as Response 9 10 # Expected HTTP status, not an exception; the dispatch executor maps it once. 11 def checked(~I: Type, value: Maybe<&1, I>) -> Result<&2, &1, U32, I>: 12 match value: 13 case None{}: Fail{400} 14 case Some{input}: Done{input} 15 16 def decimal.step(ok: Bool, +acc: U32, digit: U32, next: U32 -> Maybe<&2, U32>) -> Maybe<&2, U32>: 17 match ok: 18 case False{}: None{} 19 case True{}: next((acc * 10 + digit : U32)) 20 21 def decimal.go(text: String, +acc: U32) -> Maybe<&2, U32>: 22 match text: 23 case SNil{}: Some{acc} 24 case SCon{Chr{+c}, rest}: 25 +digit = (c - 48 : U32) 26 decimal.step(U32.is_ge(c, 48) && U32.is_le(c, 57) && (U32.is_lt(acc, 429496729) || (U32.is_eq(acc, 429496729) && U32.is_le(digit, 5))), acc, digit, n => decimal.go(rest, n)) 27 28 # Nonempty ASCII decimal only; arithmetic happens only after the overflow guard. 29 def u32(text: String) -> Maybe<&2, U32>: 30 match text: 31 case SNil{}: None{} 32 case SCon{c, rest}: decimal.go(SCon{c, rest}, 0) 33 34 def scalar.convert(~I: Data, value: Maybe<&2, I>) -> Result<&2, &1, U32, I>: 35 match value: 36 case None{}: Fail{400} 37 case Some{input}: Done{input} 38 39 def path.value(~I: Data, ~convert: String -> Maybe<&2, I>, pair: Map<&2, String> & String) -> Result<&2, &1, U32, I>: 40 (params, value) = pair 41 scalar.convert(~I, convert(value)) 42 43 def path.present(~I: Data, ~convert: String -> Maybe<&2, I>, present: Bool, params: Map<&2, String>, key: String) -> Result<&2, &1, U32, I>: 44 match present: 45 case False{}: Fail{400} 46 case True{}: 47 path.value(~I, ~convert, Map.get(String, "", params, key)) 48 49 def path.found(~I: Data, ~convert: String -> Maybe<&2, I>, key: String, pair: Map<&2, String> & Bool) -> Result<&2, &1, U32, I>: 50 (params, present) = pair 51 path.present(~I, ~convert, present, params, key) 52 53 def path(~I: Data, ~convert: String -> Maybe<&2, I>, params: Map<&2, String>, +key: String) -> Result<&2, &1, U32, I>: 54 path.found(~I, ~convert, key, Map.has(&2, String, params, key)) 55 56 57 def query.keep(same: Bool, value: String, rest: List<&2, String>) -> List<&2, String>: 58 match same: 59 case True{}: value <> rest 60 case False{}: rest 61 def query.values(xs: List<&2, Target.Query>, +key: String) -> List<&2, String>: 62 match xs: 63 case Nil{}: Nil{} 64 case Target.Query{+name, value} <> rest: 65 query.keep(String.eq(name, key), value, query.values(rest, key)) 66 67 68 def scalar.values(~I: Data, ~convert: String -> Maybe<&2, I>, values: List<&2, String>) -> Result<&2, &1, U32, I>: 69 match values: 70 case value <> Nil{}: scalar.convert(~I, convert(value)) 71 case _: Fail{400} 72 73 def query.scalar(~I: Data, ~convert: String -> Maybe<&2, I>, query: List<&2, Target.Query>, key: String) -> Result<&2, &1, U32, I>: 74 scalar.values(~I, ~convert, query.values(query, key)) 75 76 def list.values(~I: Data, ~convert: String -> Maybe<&2, I>, values: List<&2, String>) -> Result<&2, &1, U32, List<&2, I>>: 77 match values: 78 case Nil{}: Done{Nil{}} 79 case value <> rest: 80 do Result<&2, &1, U32, List<&2, I>>: 81 head : I <- scalar.convert(~I, convert(value)) 82 tail : List<&2, I> <- list.values(~I, ~convert, rest) 83 return head <> tail 84 85 def query.list(~I: Data, ~convert: String -> Maybe<&2, I>, query: List<&2, Target.Query>, key: String) -> Result<&2, &1, U32, List<&2, I>>: 86 list.values(~I, ~convert, query.values(query, key)) 87 88 # Reuse Target's RFC 3629 state machine directly over the owned packed array. 89 def utf8.step(pair: Array & U32, state: Target.Utf) -> Array & Maybe<&2, Target.Utf>: 90 (bytes, byte) = pair 91 Target.Utf{+need, lo, hi, cp, out} = state 92 (bytes, Target.utf.step(U32.is_eq(need, 0), byte, need, lo, hi, cp, out)) 93 94 def utf8.go(fuel: Nat, pair: Array & Maybe<&2, Target.Utf>, +index: U32) -> Array & Maybe<&2, String>: 95 match fuel: 96 case 0n: 97 (bytes, state) = pair 98 (bytes, Target.utf.finish(state)) 99 case 1n+rest: 100 (bytes, state) = pair 101 match state: 102 case None{}: (bytes, None{}) 103 case Some{state}: utf8.go(rest, utf8.step(Bytes.peek(bytes, index), state), (index + 1 : U32)) 104 105 def utf8.borrowed(len: U32, pair: Array & Maybe<&2, String>) -> Http.Body & Maybe<&2, String>: 106 (buffer, text) = pair 107 (Bytes.Bytes{len, buffer}, text) 108 109 def utf8.borrow(bytes: Http.Body) -> Http.Body & Maybe<&2, String>: 110 Bytes.Bytes{+len, buffer} = bytes 111 utf8.borrowed(len, utf8.go(U32.to_nat(len), (buffer, Some{Target.Utf{0, 128, 191, 0, ""}}), 0)) 112 113 def utf8.text(pair: Http.Body & Maybe<&2, String>) -> Maybe<&2, String>: 114 (bytes, text) = pair 115 text 116 117 def utf8(bytes: Http.Body) -> Maybe<&2, String>: 118 utf8.text(utf8.borrow(bytes)) 119 120 # Split only unquoted semicolons, retaining quoted parameter syntax for checks. 121 def media.part(split: Bool, +c: U32, +quoted: Bool, +escaped: Bool, acc: String, parts: List<&2, String>) -> Bool & Bool & String & List<&2, String>: 122 match split: 123 case True{}: (False{}, False{}, "", String.reverse(acc) <> parts) 124 case False{}: 125 (Bool.pick(Bool, U32.is_eq(c, 34) && Bool.not(escaped), Bool.not(quoted), quoted), quoted && U32.is_eq(c, 92) && Bool.not(escaped), SCon{Chr{c}, acc}, parts) 126 127 def media.end(invalid: Bool, acc: String, parts: List<&2, String>) -> List<&2, String>: 128 match invalid: 129 case True{}: ["invalid"] 130 case False{}: List.reverse(&2, String, String.reverse(acc) <> parts) 131 132 def media.parts(text: String, state: Bool & Bool & String & List<&2, String>) -> List<&2, String>: 133 match text: 134 case SNil{}: 135 (quoted, escaped, acc, parts) = state 136 media.end(quoted || escaped, acc, parts) 137 case SCon{Chr{+c}, rest}: 138 (+quoted, escaped, acc, parts) = state 139 media.parts(rest, media.part(U32.is_eq(c, 59) && Bool.not(quoted), c, quoted, escaped, acc, parts)) 140 141 # Compare the decoded quoted-string charset without allocating a second value. 142 def media.charset.quoted(text: String, expected: String, escaped: Bool) -> Bool: 143 match text: 144 case SNil{}: False{} 145 case SCon{Chr{+c}, rest}: 146 match c: 147 case 92: 148 match expected: 149 case SNil{}: False{} 150 case SCon{Chr{want}, next}: 151 match escaped: 152 case True{}: False{} 153 case False{}: media.charset.quoted(rest, SCon{Chr{want}, next}, True{}) 154 case _: 155 match expected: 156 case SNil{}: U32.is_eq(c, 34) && Bool.not(escaped) && String.eq(rest, "") 157 case SCon{Chr{want}, next}: U32.is_eq(c, want) && media.charset.quoted(rest, next, False{}) 158 159 def media.charset.start(quoted: Bool, c: U32, rest: String) -> Bool: 160 match quoted: 161 case True{}: media.charset.quoted(rest, "utf-8", False{}) 162 case False{}: String.eq(SCon{Chr{c}, rest}, "utf-8") 163 164 def media.charset.value(value: String) -> Bool: 165 match value: 166 case SNil{}: False{} 167 case SCon{Chr{+c}, rest}: media.charset.start(U32.is_eq(c, 34), c, rest) 168 169 def media.parameter(pair: String & String) -> Bool: 170 (key, value) = pair 171 +name = String.to_lower(Http.trim(key)) 172 +charset = String.to_lower(Http.trim(value)) 173 Bool.not(String.eq(name, "charset")) || media.charset.value(charset) 174 175 def media.charset(parts: List<&2, String>) -> Bool: 176 match parts: 177 case Nil{}: True{} 178 case part <> rest: media.parameter(Url.take_to(part, 61)) && media.charset(rest) 179 180 def media.type(json: Bool, +name: String) -> Bool: 181 match json: 182 case True{}: 183 String.eq(name, "application/json") || (String.starts_with(name, "application/") && String.ends_with(name, "+json") && Nat.is_gt(String.length(name), 17n) && Response.name(String.drop(name, 12n))) 184 case False{}: 185 String.starts_with(name, "text/") && Response.name(String.drop(name, 5n)) 186 187 def media.content(json: Bool, parts: List<&2, String>) -> Bool: 188 match parts: 189 case Nil{}: False{} 190 case name <> rest: media.type(json, String.to_lower(Http.trim(name))) && media.charset(rest) 191 192 def media.encoding(values: List<&2, String>) -> Bool: 193 match values: 194 case Nil{}: True{} 195 case value <> rest: String.eq(value, "identity") && media.encoding(rest) 196 197 def media.headers(json: Bool, types: List<&2, String>, encodings: List<&2, String>) -> Bool: 198 match types: 199 case value <> Nil{}: media.content(json, media.parts(value, (False{}, False{}, "", Nil{}))) && media.encoding(Http.ce.split(encodings)) 200 case _: False{} 201 202 def media(json: Bool, +headers: Map<&2, List<&2, String>>) -> Bool: 203 media.headers(json, Http.fields(headers, "content-type"), Http.fields(headers, "content-encoding")) 204 205 def raw(body: Http.Body) -> Result<&2, &1, U32, Http.Body>: 206 Done{body} 207 208 def text.accepted(accepted: Bool, body: Http.Body) -> Result<&2, &1, U32, String>: 209 match accepted: 210 case False{}: Fail{415} 211 case True{}: scalar.convert(~String, utf8(body)) 212 213 def text(headers: Map<&2, List<&2, String>>, body: Http.Body) -> Result<&2, &1, U32, String>: 214 text.accepted(media(False{}, headers), body) 215 216 # An explicit stack avoids mutually recursive tree walkers. Each decoded object 217 # has its own key set; escaped and literal spellings compare as the same text. 218 type Parent is Type: 219 Object{key: Bytes.Bytes, rest: List<&1, Bytes.Bytes & Json.Val>, acc: List<&1, Bytes.Bytes & Json.Val>, seen: Set()} 220 Array{rest: List<&1, Json.Val>, acc: List<&1, Json.Val>} 221 222 type Walk is Type: 223 Value{value: Json.Val, parents: List<&1, Parent>} 224 Fields{fields: List<&1, Bytes.Bytes & Json.Val>, acc: List<&1, Bytes.Bytes & Json.Val>, seen: Set(), parents: List<&1, Parent>} 225 Items{values: List<&1, Json.Val>, acc: List<&1, Json.Val>, parents: List<&1, Parent>} 226 Returned{value: Json.Val, parents: List<&1, Parent>} 227 228 def unique.field(repeated: Bool, key: Bytes.Bytes, value: Json.Val, rest: List<&1, Bytes.Bytes & Json.Val>, acc: List<&1, Bytes.Bytes & Json.Val>, seen: Set(), parents: List<&1, Parent>) -> Maybe<&1, Walk>: 229 match repeated: 230 case True{}: None{} 231 case False{}: Some{Value{value, Object{key, rest, acc, seen} <> parents}} 232 233 def unique.seen(pair: Set() & Bool, name: String, key: Bytes.Bytes, value: Json.Val, rest: List<&1, Bytes.Bytes & Json.Val>, acc: List<&1, Bytes.Bytes & Json.Val>, parents: List<&1, Parent>) -> Maybe<&1, Walk>: 234 (seen, repeated) = pair 235 unique.field(repeated, key, value, rest, acc, Set.add(seen, name), parents) 236 237 def unique.key(text: Maybe<&2, String>, key: Bytes.Bytes, value: Json.Val, rest: List<&1, Bytes.Bytes & Json.Val>, acc: List<&1, Bytes.Bytes & Json.Val>, seen: Set(), parents: List<&1, Parent>) -> Maybe<&1, Walk>: 238 match text: 239 case None{}: None{} 240 case Some{+name}: 241 unique.seen(Set.has(seen, name), name, key, value, rest, acc, parents) 242 243 def unique.borrowed(pair: Http.Body & Maybe<&2, String>, value: Json.Val, rest: List<&1, Bytes.Bytes & Json.Val>, acc: List<&1, Bytes.Bytes & Json.Val>, seen: Set(), parents: List<&1, Parent>) -> Maybe<&1, Walk>: 244 (key, text) = pair 245 unique.key(text, key, value, rest, acc, seen, parents) 246 247 def unique.step(step: Walk) -> Maybe<&1, Walk>: 248 match step: 249 case Value{value, parents}: 250 match value: 251 case Json.Obj{fields}: Some{Fields{fields, Nil{}, Set.new(), parents}} 252 case Json.Arr{values}: Some{Items{values, Nil{}, parents}} 253 case Json.Null{}: Some{Returned{Json.Null{}, parents}} 254 case Json.Flag{on}: Some{Returned{Json.Flag{on}, parents}} 255 case Json.Num{number}: Some{Returned{Json.Num{number}, parents}} 256 case Json.Str{text}: Some{Returned{Json.Str{text}, parents}} 257 case Fields{fields, acc, seen, parents}: 258 match fields: 259 case Nil{}: Some{Returned{Json.Obj{List.reverse(&1, Bytes.Bytes & Json.Val, acc)}, parents}} 260 case (key, value) <> rest: 261 unique.borrowed(utf8.borrow(key), value, rest, acc, seen, parents) 262 case Items{values, acc, parents}: 263 match values: 264 case Nil{}: Some{Returned{Json.Arr{List.reverse(&1, Json.Val, acc)}, parents}} 265 case value <> rest: Some{Value{value, Array{rest, acc} <> parents}} 266 case Returned{value, parents}: 267 match parents: 268 case Nil{}: Some{Returned{value, Nil{}}} 269 case Object{key, rest, acc, seen} <> parents: Some{Fields{rest, (key, value) <> acc, seen, parents}} 270 case Array{rest, acc} <> parents: Some{Items{rest, value <> acc, parents}} 271 272 def unique(fuel: Nat, step: Maybe<&1, Walk>) -> Maybe<&1, Json.Val>: 273 match fuel: 274 case 0n: None{} 275 case 1n+rest: 276 match step: 277 case None{}: None{} 278 case Some{Returned{value, Nil{}}}: Some{value} 279 case Some{state}: unique(rest, unique.step(state)) 280 281 def json.parsed(fuel: Nat, value: Maybe<&1, Json.Val>) -> Result<&2, &1, U32, Json.Val>: 282 match value: 283 case None{}: Fail{400} 284 case Some{value}: checked(~Json.Val, unique(fuel, Some{Value{value, Nil{}}})) 285 286 def json.accepted(accepted: Bool, cap: U32, body: Http.Body) -> Result<&2, &1, U32, Json.Val>: 287 match accepted: 288 case False{}: Fail{415} 289 case True{}: 290 Bytes.Bytes{+len, buffer} = body 291 json.parsed(Nat.add(Nat.mul(U32.to_nat(len), 4n), 8n), Json.parse.strict.bytes(Bytes.Bytes{len, buffer}, cap)) 292 293 def json.with_depth(headers: Map<&2, List<&2, String>>, body: Http.Body, cap: U32) -> Result<&2, &1, U32, Json.Val>: 294 json.accepted(media(True{}, headers), cap, body) 295 296 def json(headers: Map<&2, List<&2, String>>, body: Http.Body) -> Result<&2, &1, U32, Json.Val>: 297 json.with_depth(headers, body, 64) 298 299 # Exact number text remains packed until this explicit checked conversion. 300 def number.u32(value: Json.Val) -> Result<&2, &1, U32, U32>: 301 match value: 302 case Json.Num{bytes}: scalar.convert(~U32, u32(Bytes.to_string(bytes))) 303 case _: Fail{400}