char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport FormData and multipart extension to typescriptc013baf

main
8.7 KiB234 linesraw
1import {
2  ArrayPrototypePush as push,
3  RegExpPrototypeExec as exec,
4  String,
5  TypedArrayPrototypeGetBuffer as buffer,
6  TypedArrayPrototypeGetByteLength as length,
7  TypedArrayPrototypeGetByteOffset as offset,
8  TypeError,
9  Uint8Array,
10  uncurryThis,
11  WeakMapPrototypeGet as get,
12} from "ext:core/primordials";
13import { op_form_data_boundary, op_form_data_urlencoded } from "ext:core/ops";
14import { array } from "ext:web/idl.js";
15import { blobs, build, copyRange } from "ext:blob/blob.js";
16import { fileState, parsedFile } from "ext:blob/file.js";
17import { TextDecoder, TextEncoder } from "ext:encoding/main.js";
18import { forms, fromEntries } from "./form_data.js";
19
20import type { Part } from "ext:blob/blob.js";
21import type { FormData, FormEntry } from "./form_data.js";
22
23type Bytes = Uint8Array<ArrayBuffer>;
24
25const charCode = uncurryThis(String.prototype.charCodeAt),
26  strSlice = uncurryThis(String.prototype.slice),
27  strIndex = uncurryThis(String.prototype.indexOf),
28  lower = uncurryThis(String.prototype.toLowerCase);
29const indexOf = uncurryThis(Uint8Array.prototype.indexOf);
30const decode = uncurryThis(TextDecoder.prototype.decode),
31  encode = uncurryThis(TextEncoder.prototype.encode);
32const decoder = new TextDecoder("utf-8", { __proto__: null, ignoreBOM: true }),
33  encoder = new TextEncoder();
34const token = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
35function crlf(value: string): string {
36  let result = "", start = 0;
37  for (let i = 0; i < value.length; i++) {
38    const code = charCode(value, i);
39    if (code === 10 || code === 13) {
40      result += strSlice(value, start, i) + "\r\n";
41      if (code === 13 && charCode(value, i + 1) === 10) i++;
42      start = i + 1;
43    }
44  }
45  return result + strSlice(value, start);
46}
47function escape(value: string): string {
48  let result = "", start = 0;
49  for (let i = 0; i < value.length; i++) {
50    const code = charCode(value, i);
51    if (code === 10 || code === 13 || code === 34) {
52      result += strSlice(value, start, i) +
53        (code === 10 ? "%0A" : code === 13 ? "%0D" : "%22");
54      start = i + 1;
55    }
56  }
57  return result + strSlice(value, start);
58}
59function serializeFormData(value: unknown): { bytes: Bytes; type: string } | null {
60  const data = get(forms, value);
61  if (!data) return null;
62  const boundary = op_form_data_boundary(null), parts = array<Part>();
63  for (let i = 0; i < data.list.length; i++) {
64    const e = data.list[i];
65    let header = "--" + boundary +
66      '\r\nContent-Disposition: form-data; name="' + escape(crlf(e.name)) + '"';
67    if (typeof e.value === "string") {
68      push(parts, {
69        kind: "string",
70        value: header + "\r\n\r\n" + crlf(e.value) + "\r\n",
71      });
72    } else {
73      const f = fileState(e.value)!, blob = get(blobs, e.value)!;
74      header += '; filename="' + escape(f.name) + '"\r\nContent-Type: ' +
75        (blob.type || "application/octet-stream") + "\r\n\r\n";
76      push(parts, { kind: "string", value: header }, {
77        kind: "blob",
78        value: blob,
79      }, { kind: "string", value: "\r\n" });
80    }
81  }
82  push(parts, { kind: "string", value: "--" + boundary + "--\r\n" });
83  const type = "multipart/form-data; boundary=" + boundary;
84  const body = build(parts, { __proto__: null, endings: "transparent", type });
85  return { bytes: copyRange(body, 0, body.size), type };
86}
87function invalid(): never {
88  throw new TypeError("Invalid multipart/form-data body");
89}
90function view(bytes: Bytes, start: number, end: number): Bytes {
91  return new Uint8Array(buffer(bytes), offset(bytes) + start, end - start);
92}
93function ows(value: string): string {
94  let start = 0, end = value.length;
95  while (charCode(value, start) === 9 || charCode(value, start) === 32) start++;
96  while (
97    end > start &&
98    (charCode(value, end - 1) === 9 || charCode(value, end - 1) === 32)
99  ) end--;
100  return strSlice(value, start, end);
101}
102function disposition(value: string): { name: string; filename: string | undefined } {
103  let position = strIndex(value, ";");
104  const kind = ows(position < 0 ? value : strSlice(value, 0, position));
105  if (lower(kind) !== "form-data") invalid();
106  if (position < 0) invalid();
107  let name: string | undefined, filename: string | undefined;
108  while (position < value.length) {
109    position++;
110    while (
111      charCode(value, position) === 9 || charCode(value, position) === 32
112    ) position++;
113    if (position === value.length) break;
114    const start = position;
115    while (
116      position < value.length && value[position] !== "=" &&
117      value[position] !== ";"
118    ) position++;
119    const rawKey = ows(strSlice(value, start, position));
120    if (!exec(token, rawKey) || value[position] !== "=") invalid();
121    const key = lower(rawKey);
122    position++;
123    while (
124      charCode(value, position) === 9 || charCode(value, position) === 32
125    ) position++;
126    let parameter;
127    if (value[position] === '"') {
128      const start = ++position;
129      while (position < value.length && value[position] !== '"') position++;
130      if (position === value.length) invalid();
131      parameter = strSlice(value, start, position++);
132      while (
133        charCode(value, position) === 9 || charCode(value, position) === 32
134      ) position++;
135      if (position < value.length && value[position] !== ";") invalid();
136    } else {
137      const start = position;
138      while (position < value.length && value[position] !== ";") position++;
139      parameter = ows(strSlice(value, start, position));
140      if (!exec(token, parameter)) invalid();
141    }
142    if (key === "name") name = parameter;
143    else if (key === "filename") filename = parameter;
144  }
145  if (name === undefined) invalid();
146  return { name, filename };
147}
148function parseMultipart(bytes: Bytes, boundary: unknown): FormData {
149  if (typeof boundary !== "string" || !boundary.length) invalid();
150  for (let i = 0; i < boundary.length; i++) {
151    if (charCode(boundary, i) < 32 || charCode(boundary, i) > 126) invalid();
152  }
153  const prefix = encode(encoder, "--" + boundary),
154    size = length(bytes),
155    prefixSize = length(prefix);
156  function matches(position: number): boolean {
157    if (position + prefixSize > size) return false;
158    for (let i = 0; i < prefixSize; i++) {
159      if (bytes[position + i] !== prefix[i]) return false;
160    }
161    return true;
162  }
163  function delimiter(start: number): { closed: boolean; position: number } {
164    let p = start + prefixSize, closed = false;
165    if (bytes[p] === 45 && bytes[p + 1] === 45) {
166      closed = true;
167      p += 2;
168    }
169    while (bytes[p] === 32 || bytes[p] === 9) p++;
170    if (closed && p === size) return { closed, position: p };
171    if (bytes[p] !== 13 || bytes[p + 1] !== 10) invalid();
172    return { closed, position: p + 2 };
173  }
174  function find(from: number): number {
175    let p = from;
176    while ((p = indexOf(bytes, 13, p)) !== -1) {
177      if (bytes[p + 1] === 10 && matches(p + 2)) return p;
178      p++;
179    }
180    return -1;
181  }
182  let start = matches(0) ? 0 : find(0);
183  if (start < 0) invalid();
184  if (start !== 0 || !matches(0)) start += 2;
185  let d = delimiter(start);
186  const list = array<FormEntry>();
187  while (!d.closed) {
188    let p = d.position, contentDisposition: string | undefined, contentType = "text/plain";
189    const headerStart = p;
190    while (true) {
191      const end = indexOf(bytes, 13, p), lf = indexOf(bytes, 10, p);
192      if (end < 0 || lf !== end + 1) invalid();
193      if (end === p) break;
194      p = end + 2;
195    }
196    const headers = decode(decoder, view(bytes, headerStart, p));
197    p += 2;
198    let lineStart = 0;
199    while (lineStart < headers.length) {
200      const lineEnd = strIndex(headers, "\r\n", lineStart);
201      const line = strSlice(headers, lineStart, lineEnd);
202      lineStart = lineEnd + 2;
203      if (charCode(line, 0) === 9 || charCode(line, 0) === 32) invalid();
204      const colon = strIndex(line, ":");
205      if (colon < 0) invalid();
206      const rawKey = ows(strSlice(line, 0, colon)),
207        value = ows(strSlice(line, colon + 1));
208      if (!exec(token, rawKey)) invalid();
209      const key = lower(rawKey);
210      if (key === "content-disposition") contentDisposition = value;
211      else if (key === "content-type") contentType = value;
212    }
213    if (contentDisposition === undefined) invalid();
214    const metadata = disposition(contentDisposition), end = find(p);
215    if (end < 0) invalid();
216    const content = view(bytes, p, end);
217    push(list, {
218      name: metadata.name,
219      value: metadata.filename === undefined
220        ? decode(decoder, content)
221        : parsedFile(new Uint8Array(content), metadata.filename, contentType),
222    });
223    d = delimiter(end + 2);
224  }
225  return fromEntries(list);
226}
227function parseUrlencoded(bytes: Bytes): FormData {
228  const pairs = op_form_data_urlencoded(bytes), list = array<FormEntry>();
229  for (let i = 0; i < pairs.length; i++) {
230    push(list, { name: pairs[i][0], value: pairs[i][1] });
231  }
232  return fromEntries(list);
233}
234export { parseMultipart, parseUrlencoded, serializeFormData };