import { ArrayPrototypePush as push, RegExpPrototypeExec as exec, String, TypedArrayPrototypeGetBuffer as buffer, TypedArrayPrototypeGetByteLength as length, TypedArrayPrototypeGetByteOffset as offset, TypeError, Uint8Array, uncurryThis, WeakMapPrototypeGet as get, } from "ext:core/primordials"; import { op_form_data_boundary, op_form_data_urlencoded } from "ext:core/ops"; import { array } from "ext:web/idl.js"; import { blobs, build, copyRange } from "ext:blob/blob.js"; import { fileState, parsedFile } from "ext:blob/file.js"; import { TextDecoder, TextEncoder } from "ext:encoding/main.js"; import { forms, fromEntries } from "./form_data.js"; import type { Part } from "ext:blob/blob.js"; import type { FormData, FormEntry } from "./form_data.js"; type Bytes = Uint8Array; const charCode = uncurryThis(String.prototype.charCodeAt), strSlice = uncurryThis(String.prototype.slice), strIndex = uncurryThis(String.prototype.indexOf), lower = uncurryThis(String.prototype.toLowerCase); const indexOf = uncurryThis(Uint8Array.prototype.indexOf); const decode = uncurryThis(TextDecoder.prototype.decode), encode = uncurryThis(TextEncoder.prototype.encode); const decoder = new TextDecoder("utf-8", { __proto__: null, ignoreBOM: true }), encoder = new TextEncoder(); const token = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/; function crlf(value: string): string { let result = "", start = 0; for (let i = 0; i < value.length; i++) { const code = charCode(value, i); if (code === 10 || code === 13) { result += strSlice(value, start, i) + "\r\n"; if (code === 13 && charCode(value, i + 1) === 10) i++; start = i + 1; } } return result + strSlice(value, start); } function escape(value: string): string { let result = "", start = 0; for (let i = 0; i < value.length; i++) { const code = charCode(value, i); if (code === 10 || code === 13 || code === 34) { result += strSlice(value, start, i) + (code === 10 ? "%0A" : code === 13 ? "%0D" : "%22"); start = i + 1; } } return result + strSlice(value, start); } function serializeFormData(value: unknown): { bytes: Bytes; type: string } | null { const data = get(forms, value); if (!data) return null; const boundary = op_form_data_boundary(null), parts = array(); for (let i = 0; i < data.list.length; i++) { const e = data.list[i]; let header = "--" + boundary + '\r\nContent-Disposition: form-data; name="' + escape(crlf(e.name)) + '"'; if (typeof e.value === "string") { push(parts, { kind: "string", value: header + "\r\n\r\n" + crlf(e.value) + "\r\n", }); } else { const f = fileState(e.value)!, blob = get(blobs, e.value)!; header += '; filename="' + escape(f.name) + '"\r\nContent-Type: ' + (blob.type || "application/octet-stream") + "\r\n\r\n"; push(parts, { kind: "string", value: header }, { kind: "blob", value: blob, }, { kind: "string", value: "\r\n" }); } } push(parts, { kind: "string", value: "--" + boundary + "--\r\n" }); const type = "multipart/form-data; boundary=" + boundary; const body = build(parts, { __proto__: null, endings: "transparent", type }); return { bytes: copyRange(body, 0, body.size), type }; } function invalid(): never { throw new TypeError("Invalid multipart/form-data body"); } function view(bytes: Bytes, start: number, end: number): Bytes { return new Uint8Array(buffer(bytes), offset(bytes) + start, end - start); } function ows(value: string): string { let start = 0, end = value.length; while (charCode(value, start) === 9 || charCode(value, start) === 32) start++; while ( end > start && (charCode(value, end - 1) === 9 || charCode(value, end - 1) === 32) ) end--; return strSlice(value, start, end); } function disposition(value: string): { name: string; filename: string | undefined } { let position = strIndex(value, ";"); const kind = ows(position < 0 ? value : strSlice(value, 0, position)); if (lower(kind) !== "form-data") invalid(); if (position < 0) invalid(); let name: string | undefined, filename: string | undefined; while (position < value.length) { position++; while ( charCode(value, position) === 9 || charCode(value, position) === 32 ) position++; if (position === value.length) break; const start = position; while ( position < value.length && value[position] !== "=" && value[position] !== ";" ) position++; const rawKey = ows(strSlice(value, start, position)); if (!exec(token, rawKey) || value[position] !== "=") invalid(); const key = lower(rawKey); position++; while ( charCode(value, position) === 9 || charCode(value, position) === 32 ) position++; let parameter; if (value[position] === '"') { const start = ++position; while (position < value.length && value[position] !== '"') position++; if (position === value.length) invalid(); parameter = strSlice(value, start, position++); while ( charCode(value, position) === 9 || charCode(value, position) === 32 ) position++; if (position < value.length && value[position] !== ";") invalid(); } else { const start = position; while (position < value.length && value[position] !== ";") position++; parameter = ows(strSlice(value, start, position)); if (!exec(token, parameter)) invalid(); } if (key === "name") name = parameter; else if (key === "filename") filename = parameter; } if (name === undefined) invalid(); return { name, filename }; } function parseMultipart(bytes: Bytes, boundary: unknown): FormData { if (typeof boundary !== "string" || !boundary.length) invalid(); for (let i = 0; i < boundary.length; i++) { if (charCode(boundary, i) < 32 || charCode(boundary, i) > 126) invalid(); } const prefix = encode(encoder, "--" + boundary), size = length(bytes), prefixSize = length(prefix); function matches(position: number): boolean { if (position + prefixSize > size) return false; for (let i = 0; i < prefixSize; i++) { if (bytes[position + i] !== prefix[i]) return false; } return true; } function delimiter(start: number): { closed: boolean; position: number } { let p = start + prefixSize, closed = false; if (bytes[p] === 45 && bytes[p + 1] === 45) { closed = true; p += 2; } while (bytes[p] === 32 || bytes[p] === 9) p++; if (closed && p === size) return { closed, position: p }; if (bytes[p] !== 13 || bytes[p + 1] !== 10) invalid(); return { closed, position: p + 2 }; } function find(from: number): number { let p = from; while ((p = indexOf(bytes, 13, p)) !== -1) { if (bytes[p + 1] === 10 && matches(p + 2)) return p; p++; } return -1; } let start = matches(0) ? 0 : find(0); if (start < 0) invalid(); if (start !== 0 || !matches(0)) start += 2; let d = delimiter(start); const list = array(); while (!d.closed) { let p = d.position, contentDisposition: string | undefined, contentType = "text/plain"; const headerStart = p; while (true) { const end = indexOf(bytes, 13, p), lf = indexOf(bytes, 10, p); if (end < 0 || lf !== end + 1) invalid(); if (end === p) break; p = end + 2; } const headers = decode(decoder, view(bytes, headerStart, p)); p += 2; let lineStart = 0; while (lineStart < headers.length) { const lineEnd = strIndex(headers, "\r\n", lineStart); const line = strSlice(headers, lineStart, lineEnd); lineStart = lineEnd + 2; if (charCode(line, 0) === 9 || charCode(line, 0) === 32) invalid(); const colon = strIndex(line, ":"); if (colon < 0) invalid(); const rawKey = ows(strSlice(line, 0, colon)), value = ows(strSlice(line, colon + 1)); if (!exec(token, rawKey)) invalid(); const key = lower(rawKey); if (key === "content-disposition") contentDisposition = value; else if (key === "content-type") contentType = value; } if (contentDisposition === undefined) invalid(); const metadata = disposition(contentDisposition), end = find(p); if (end < 0) invalid(); const content = view(bytes, p, end); push(list, { name: metadata.name, value: metadata.filename === undefined ? decode(decoder, content) : parsedFile(new Uint8Array(content), metadata.filename, contentType), }); d = delimiter(end + 2); } return fromEntries(list); } function parseUrlencoded(bytes: Bytes): FormData { const pairs = op_form_data_urlencoded(bytes), list = array(); for (let i = 0; i < pairs.length; i++) { push(list, { name: pairs[i][0], value: pairs[i][1] }); } return fromEntries(list); } export { parseMultipart, parseUrlencoded, serializeFormData };