src/form.bend
emerging-ezhttp@0.21.0.0 · 343 lines · 43 definitions
1
2
3
4
5
6
7
8
9import Base
10import bend-kit-bytes@0.3.2.0/bytes.bend as Bytes
11import emerging-ezhttp@0.21.0.0/src/body.bend as Body
12import emerging-ezhttp@0.21.0.0/src/pct.bend as Pct
13
14
15
16type Upload is Type:
17 UField{name: String, value: String}
18 UFile{name: String, filename: String, ctype: String, bytes: Bytes.Bytes}
19
20
21
22type Part is Data:
23 Field{name: String, value: String}
24 File{name: String, filename: String, ctype: String, clash: Bool}
25
26
27type Seg is Data:
28 Run{octets: List<&2, U32>}
29 Blob{}
30
31
32
33type Plan is Data:
34 NoPlan{why: String}
35 Plan{ctype: String, segs: List<&2, Seg>}
36
37
38type Form is Type:
39 NoForm{why: String}
40 Form{ctype: String, body: Body.Body}
41
42
43def form.field(name: String, value: String) -> Upload:
44 UField{name, value}
45
46
47def form.file(name: String, filename: String, ctype: String,
48 bytes: Bytes.Bytes) -> Upload:
49 UFile{name, filename, ctype, bytes}
50
51
52
53
54def octets.onto(s: String, rest: List<&2, U32>) -> List<&2, U32>:
55 match s:
56 case SNil{}:
57 rest
58 case SCon{Chr{+cp}, tail}:
59 List.append(&2, U32, Pct.utf8(cp), octets.onto(tail, rest))
60
61
62def all.step(ok: Bool, rest: Unit -> Bool) -> Bool:
63 match ok:
64 case False{}:
65 False{}
66 case True{}:
67 rest(Unit{})
68
69
70def any.step(hit: Bool, rest: Unit -> Bool) -> Bool:
71 match hit:
72 case True{}:
73 True{}
74 case False{}:
75 rest(Unit{})
76
77
78def octets.starts(xs: List<&2, U32>, pat: List<&2, U32>) -> Bool:
79 match xs pat:
80 case Nil{} Nil{}:
81 True{}
82 case Nil{} Con{p, q}:
83 False{}
84 case Con{x, t} Nil{}:
85 True{}
86 case Con{x, t} Con{p, q}:
87 all.step(U32.is_eq(x, p), _u => octets.starts(t, q))
88
89
90def octets.has(xs: List<&2, U32>, +pat: List<&2, U32>) -> Bool:
91 match xs:
92 case []:
93 octets.starts([], pat)
94 case +x <> +t:
95 any.step(octets.starts(x <> t, pat), _u => octets.has(t, pat))
96
97
98
99
100
101def form.bchar(+cp: U32) -> Bool:
102 Bool.or(Bool.and(U32.is_le(65, cp), U32.is_le(cp, 90)),
103 Bool.or(Bool.and(U32.is_le(97, cp), U32.is_le(cp, 122)),
104 Bool.or(Bool.and(U32.is_le(48, cp), U32.is_le(cp, 57)),
105 Bool.or(U32.is_eq(cp, 45),
106 Bool.or(U32.is_eq(cp, 95), U32.is_eq(cp, 46))))))
107
108
109def form.bchars(s: String) -> Bool:
110 match s:
111 case SNil{}:
112 True{}
113 case SCon{Chr{+cp}, tail}:
114 all.step(form.bchar(cp), _u => form.bchars(tail))
115
116
117def form.boundary.ok(+b: String) -> Bool:
118 Bool.and(Bool.and(Bool.not(String.is_empty(b)),
119 Nat.is_le(String.length(b), 70n)), form.bchars(b))
120
121
122def form.delim(b: String) -> List<&2, U32>:
123 octets.onto("--" ++ b, [])
124
125
126
127
128def form.one.lf(+cp: U32, rest: String, lf: Bool) -> String:
129 match lf:
130 case True{}:
131 "%0A" ++ rest
132 case False{}:
133 SCon{Chr{cp}, rest}
134
135
136def form.one.cr(+cp: U32, rest: String, cr: Bool) -> String:
137 match cr:
138 case True{}:
139 "%0D" ++ rest
140 case False{}:
141 form.one.lf(cp, rest, U32.is_eq(cp, 10))
142
143
144def form.one.dq(+cp: U32, rest: String, dq: Bool) -> String:
145 match dq:
146 case True{}:
147 "%22" ++ rest
148 case False{}:
149 form.one.cr(cp, rest, U32.is_eq(cp, 13))
150
151
152def form.one(+cp: U32, rest: String) -> String:
153 form.one.dq(cp, rest, U32.is_eq(cp, 34))
154
155
156
157def form.quote(s: String) -> String:
158 match s:
159 case SNil{}:
160 SNil{}
161 case SCon{Chr{+cp}, tail}:
162 form.one(cp, form.quote(tail))
163
164
165
166
167def form.disposition(name: String) -> String:
168 "Content-Disposition: form-data; name=\"" ++ form.quote(name) ++ "\""
169
170
171def form.head(p: Part) -> String:
172 match p:
173 case Field{name, value}:
174 form.disposition(name) ++ "\r\n"
175 case File{name, filename, ctype, clash}:
176 form.disposition(name) ++ "; filename=\"" ++ form.quote(filename) ++ "\"\r\n"
177 ++ "Content-Type: " ++ ctype ++ "\r\n"
178
179
180
181def form.content(p: Part, rest: List<&2, Seg>) -> List<&2, Seg>:
182 match p:
183 case Field{name, value}:
184 Run{octets.onto(value, [])} <> rest
185 case File{name, filename, ctype, clash}:
186 Blob{} <> rest
187
188
189
190def form.part(+b: String, +p: Part, rest: List<&2, Seg>) -> List<&2, Seg>:
191 Run{octets.onto("--" ++ b ++ "\r\n" ++ form.head(p) ++ "\r\n", [])}
192 <> form.content(p, Run{octets.onto("\r\n", [])} <> rest)
193
194
195def form.parts(+b: String, ps: List<&2, Part>) -> List<&2, Seg>:
196 match ps:
197 case []:
198 [Run{octets.onto("--" ++ b ++ "--\r\n", [])}]
199 case p <> t:
200 form.part(b, p, form.parts(b, t))
201
202
203
204
205
206def form.hit(+b: String, p: Part) -> Bool:
207 match p:
208 case Field{name, value}:
209 octets.has(octets.onto(value, []), form.delim(b))
210 case File{name, filename, ctype, clash}:
211 clash
212
213
214def form.clash(+b: String, ps: List<&2, Part>) -> Bool:
215 match ps:
216 case []:
217 False{}
218 case p <> t:
219 any.step(form.hit(b, p), _u => form.clash(b, t))
220
221
222def form.badtype.one(p: Part) -> Bool:
223 match p:
224 case Field{name, value}:
225 False{}
226 case File{name, filename, +ctype, clash}:
227 Bool.or(String.contains(ctype, "\r"), String.contains(ctype, "\n"))
228
229
230def form.badtype(ps: List<&2, Part>) -> Bool:
231 match ps:
232 case []:
233 False{}
234 case p <> t:
235 any.step(form.badtype.one(p), _u => form.badtype(t))
236
237
238def form.type(b: String) -> String:
239 "multipart/form-data; boundary=" ++ b
240
241
242def form.check.type(+b: String, ps: List<&2, Part>, bad: Bool) -> Plan:
243 match bad:
244 case True{}:
245 NoPlan{"a file part's content type holds a CR or LF"}
246 case False{}:
247 Plan{form.type(b), form.parts(b, ps)}
248
249
250def form.check.clash(+b: String, +ps: List<&2, Part>, hit: Bool) -> Plan:
251 match hit:
252 case True{}:
253 NoPlan{"the boundary occurs in a part's content"}
254 case False{}:
255 form.check.type(b, ps, form.badtype(ps))
256
257
258def form.check(+b: String, +ps: List<&2, Part>, ok: Bool) -> Plan:
259 match ok:
260 case False{}:
261 NoPlan{"the boundary must be 1 to 70 of A-Z a-z 0-9 - _ ."}
262 case True{}:
263 form.check.clash(b, ps, form.clash(b, ps))
264
265
266
267def form.plan(+b: String, ps: List<&2, Part>) -> Plan:
268 form.check(b, ps, form.boundary.ok(b))
269
270
271
272
273def form.found(r: Bytes.Bytes & Maybe<&2, U32>) -> Bytes.Bytes & Bool:
274 (bs, m) = r
275 match m:
276 case None{}:
277 (bs, False{})
278 case Some{i}:
279 (bs, True{})
280
281
282
283def form.split.file(name: String, filename: String, ctype: String,
284 r: Bytes.Bytes & Bool, rest: List<&2, Part> & List<&1, Bytes.Bytes>)
285 -> List<&2, Part> & List<&1, Bytes.Bytes>:
286 (bs, hit) = r
287 (ps, fs) = rest
288 (File{name, filename, ctype, hit} <> ps, bs <> fs)
289
290
291def form.split.field(name: String, value: String,
292 rest: List<&2, Part> & List<&1, Bytes.Bytes>)
293 -> List<&2, Part> & List<&1, Bytes.Bytes>:
294 (ps, fs) = rest
295 (Field{name, value} <> ps, fs)
296
297
298
299def form.split(+d: String, us: List<&1, Upload>)
300 -> List<&2, Part> & List<&1, Bytes.Bytes>:
301 match us:
302 case []:
303 ([], [])
304 case u <> t:
305 match u:
306 case UField{name, value}:
307 form.split.field(name, value, form.split(d, t))
308 case UFile{name, filename, ctype, bytes}:
309 form.split.file(name, filename, ctype,
310 form.found(Bytes.find(bytes, d)), form.split(d, t))
311
312
313def form.runs(segs: List<&2, Seg>, fs: List<&1, Bytes.Bytes>)
314 -> List<&1, Bytes.Bytes>:
315 match segs:
316 case []:
317 []
318 case Run{octets} <> t:
319 Bytes.from_string(Body.octets.chars(octets)) <> form.runs(t, fs)
320 case Blob{} <> t:
321 match fs:
322 case []:
323 form.runs(t, [])
324 case f <> rest:
325 f <> form.runs(t, rest)
326
327
328def form.build(p: Plan, fs: List<&1, Bytes.Bytes>) -> Form:
329 match p:
330 case NoPlan{why}:
331 NoForm{why}
332 case Plan{ctype, segs}:
333 Form{ctype, Body.Octets{Bytes.concat(form.runs(segs, fs))}}
334
335
336def form.built(+b: String, r: List<&2, Part> & List<&1, Bytes.Bytes>) -> Form:
337 (ps, fs) = r
338 form.build(form.plan(b, ps), fs)
339
340
341
342def form.body(+b: String, us: List<&1, Upload>) -> Form:
343 form.built(b, form.split(Body.octets.chars(form.delim(b)), us))