diff options
author | Casper Beyer <caspervonb@pm.me> | 2021-02-02 19:05:46 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-02 12:05:46 +0100 |
commit | 6abf126c2a7a451cded8c6b5e6ddf1b69c84055d (patch) | |
tree | fd94c013a19fcb38954844085821ec1601c20e18 /std/mime/multipart.ts | |
parent | a2b5d44f1aa9d64f448a2a3cc2001272e2f60b98 (diff) |
chore: remove std directory (#9361)
This removes the std folder from the tree.
Various parts of the tests are pretty tightly dependent
on std (47 direct imports and 75 indirect imports, not
counting the cli tests that use them as fixtures) so I've
added std as a submodule for now.
Diffstat (limited to 'std/mime/multipart.ts')
-rw-r--r-- | std/mime/multipart.ts | 609 |
1 files changed, 0 insertions, 609 deletions
diff --git a/std/mime/multipart.ts b/std/mime/multipart.ts deleted file mode 100644 index b0d03bbc6..000000000 --- a/std/mime/multipart.ts +++ /dev/null @@ -1,609 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { equals, indexOf, lastIndexOf, startsWith } from "../bytes/mod.ts"; -import { copyN } from "../io/ioutil.ts"; -import { MultiReader } from "../io/readers.ts"; -import { extname } from "../path/mod.ts"; -import { BufReader, BufWriter } from "../io/bufio.ts"; -import { encoder } from "../encoding/utf8.ts"; -import { assert } from "../_util/assert.ts"; -import { TextProtoReader } from "../textproto/mod.ts"; -import { hasOwnProperty } from "../_util/has_own_property.ts"; - -/** FormFile object */ -export interface FormFile { - /** filename */ - filename: string; - /** content-type header value of file */ - type: string; - /** byte size of file */ - size: number; - /** in-memory content of file. Either content or tempfile is set */ - content?: Uint8Array; - /** temporal file path. - * Set if file size is bigger than specified max-memory size at reading form - * */ - tempfile?: string; -} - -/** Type guard for FormFile */ -// deno-lint-ignore no-explicit-any -export function isFormFile(x: any): x is FormFile { - return hasOwnProperty(x, "filename") && hasOwnProperty(x, "type"); -} - -function randomBoundary(): string { - let boundary = "--------------------------"; - for (let i = 0; i < 24; i++) { - boundary += Math.floor(Math.random() * 16).toString(16); - } - return boundary; -} - -/** - * Checks whether `buf` should be considered to match the boundary. - * - * The prefix is "--boundary" or "\r\n--boundary" or "\n--boundary", and the - * caller has verified already that `hasPrefix(buf, prefix)` is true. - * - * `matchAfterPrefix()` returns `1` if the buffer does match the boundary, - * meaning the prefix is followed by a dash, space, tab, cr, nl, or EOF. - * - * It returns `-1` if the buffer definitely does NOT match the boundary, - * meaning the prefix is followed by some other character. - * For example, "--foobar" does not match "--foo". - * - * It returns `0` more input needs to be read to make the decision, - * meaning that `buf.length` and `prefix.length` are the same. - */ -export function matchAfterPrefix( - buf: Uint8Array, - prefix: Uint8Array, - eof: boolean, -): -1 | 0 | 1 { - if (buf.length === prefix.length) { - return eof ? 1 : 0; - } - const c = buf[prefix.length]; - if ( - c === " ".charCodeAt(0) || - c === "\t".charCodeAt(0) || - c === "\r".charCodeAt(0) || - c === "\n".charCodeAt(0) || - c === "-".charCodeAt(0) - ) { - return 1; - } - return -1; -} - -/** - * Scans `buf` to identify how much of it can be safely returned as part of the - * `PartReader` body. - * - * @param buf - The buffer to search for boundaries. - * @param dashBoundary - Is "--boundary". - * @param newLineDashBoundary - Is "\r\n--boundary" or "\n--boundary", depending - * on what mode we are in. The comments below (and the name) assume - * "\n--boundary", but either is accepted. - * @param total - The number of bytes read out so far. If total == 0, then a - * leading "--boundary" is recognized. - * @param eof - Whether `buf` contains the final bytes in the stream before EOF. - * If `eof` is false, more bytes are expected to follow. - * @returns The number of data bytes from buf that can be returned as part of - * the `PartReader` body. - */ -export function scanUntilBoundary( - buf: Uint8Array, - dashBoundary: Uint8Array, - newLineDashBoundary: Uint8Array, - total: number, - eof: boolean, -): number | null { - if (total === 0) { - // At beginning of body, allow dashBoundary. - if (startsWith(buf, dashBoundary)) { - switch (matchAfterPrefix(buf, dashBoundary, eof)) { - case -1: - return dashBoundary.length; - case 0: - return 0; - case 1: - return null; - } - } - if (startsWith(dashBoundary, buf)) { - return 0; - } - } - - // Search for "\n--boundary". - const i = indexOf(buf, newLineDashBoundary); - if (i >= 0) { - switch (matchAfterPrefix(buf.slice(i), newLineDashBoundary, eof)) { - case -1: - return i + newLineDashBoundary.length; - case 0: - return i; - case 1: - return i > 0 ? i : null; - } - } - if (startsWith(newLineDashBoundary, buf)) { - return 0; - } - - // Otherwise, anything up to the final \n is not part of the boundary and so - // must be part of the body. Also, if the section from the final \n onward is - // not a prefix of the boundary, it too must be part of the body. - const j = lastIndexOf(buf, newLineDashBoundary.slice(0, 1)); - if (j >= 0 && startsWith(newLineDashBoundary, buf.slice(j))) { - return j; - } - - return buf.length; -} - -class PartReader implements Deno.Reader, Deno.Closer { - n: number | null = 0; - total = 0; - - constructor(private mr: MultipartReader, public readonly headers: Headers) {} - - async read(p: Uint8Array): Promise<number | null> { - const br = this.mr.bufReader; - - // Read into buffer until we identify some data to return, - // or we find a reason to stop (boundary or EOF). - let peekLength = 1; - while (this.n === 0) { - peekLength = Math.max(peekLength, br.buffered()); - const peekBuf = await br.peek(peekLength); - if (peekBuf === null) { - throw new Deno.errors.UnexpectedEof(); - } - const eof = peekBuf.length < peekLength; - this.n = scanUntilBoundary( - peekBuf, - this.mr.dashBoundary, - this.mr.newLineDashBoundary, - this.total, - eof, - ); - if (this.n === 0) { - // Force buffered I/O to read more into buffer. - assert(eof === false); - peekLength++; - } - } - - if (this.n === null) { - return null; - } - - const nread = Math.min(p.length, this.n); - const buf = p.subarray(0, nread); - const r = await br.readFull(buf); - assert(r === buf); - this.n -= nread; - this.total += nread; - return nread; - } - - close(): void {} - - private contentDisposition!: string; - private contentDispositionParams!: { [key: string]: string }; - - private getContentDispositionParams(): { [key: string]: string } { - if (this.contentDispositionParams) return this.contentDispositionParams; - const cd = this.headers.get("content-disposition"); - const params: { [key: string]: string } = {}; - assert(cd != null, "content-disposition must be set"); - const comps = decodeURI(cd).split(";"); - this.contentDisposition = comps[0]; - comps - .slice(1) - .map((v: string): string => v.trim()) - .map((kv: string): void => { - const [k, v] = kv.split("="); - if (v) { - const s = v.charAt(0); - const e = v.charAt(v.length - 1); - if ((s === e && s === '"') || s === "'") { - params[k] = v.substr(1, v.length - 2); - } else { - params[k] = v; - } - } - }); - return (this.contentDispositionParams = params); - } - - get fileName(): string { - return this.getContentDispositionParams()["filename"]; - } - - get formName(): string { - const p = this.getContentDispositionParams(); - if (this.contentDisposition === "form-data") { - return p["name"]; - } - return ""; - } -} - -function skipLWSPChar(u: Uint8Array): Uint8Array { - const ret = new Uint8Array(u.length); - const sp = " ".charCodeAt(0); - const ht = "\t".charCodeAt(0); - let j = 0; - for (let i = 0; i < u.length; i++) { - if (u[i] === sp || u[i] === ht) continue; - ret[j++] = u[i]; - } - return ret.slice(0, j); -} - -export interface MultipartFormData { - file(key: string): FormFile | FormFile[] | undefined; - value(key: string): string | undefined; - entries(): IterableIterator< - [string, string | FormFile | FormFile[] | undefined] - >; - [Symbol.iterator](): IterableIterator< - [string, string | FormFile | FormFile[] | undefined] - >; - /** Remove all tempfiles */ - removeAll(): Promise<void>; -} - -/** Reader for parsing multipart/form-data */ -export class MultipartReader { - readonly newLine = encoder.encode("\r\n"); - readonly newLineDashBoundary = encoder.encode(`\r\n--${this.boundary}`); - readonly dashBoundaryDash = encoder.encode(`--${this.boundary}--`); - readonly dashBoundary = encoder.encode(`--${this.boundary}`); - readonly bufReader: BufReader; - - constructor(reader: Deno.Reader, private boundary: string) { - this.bufReader = new BufReader(reader); - } - - /** Read all form data from stream. - * If total size of stored data in memory exceed maxMemory, - * overflowed file data will be written to temporal files. - * String field values are never written to files. - * null value means parsing or writing to file was failed in some reason. - * @param maxMemory maximum memory size to store file in memory. bytes. @default 10485760 (10MB) - * */ - async readForm(maxMemory = 10 << 20): Promise<MultipartFormData> { - const fileMap = new Map<string, FormFile | FormFile[]>(); - const valueMap = new Map<string, string>(); - let maxValueBytes = maxMemory + (10 << 20); - const buf = new Deno.Buffer(new Uint8Array(maxValueBytes)); - for (;;) { - const p = await this.nextPart(); - if (p === null) { - break; - } - if (p.formName === "") { - continue; - } - buf.reset(); - if (!p.fileName) { - // value - const n = await copyN(p, buf, maxValueBytes); - maxValueBytes -= n; - if (maxValueBytes < 0) { - throw new RangeError("message too large"); - } - const value = new TextDecoder().decode(buf.bytes()); - valueMap.set(p.formName, value); - continue; - } - // file - let formFile: FormFile | FormFile[] | undefined; - const n = await copyN(p, buf, maxValueBytes); - const contentType = p.headers.get("content-type"); - assert(contentType != null, "content-type must be set"); - if (n > maxMemory) { - // too big, write to disk and flush buffer - const ext = extname(p.fileName); - const filepath = await Deno.makeTempFile({ - dir: ".", - prefix: "multipart-", - suffix: ext, - }); - - const file = await Deno.open(filepath, { write: true }); - - try { - const size = await Deno.copy(new MultiReader(buf, p), file); - - file.close(); - formFile = { - filename: p.fileName, - type: contentType, - tempfile: filepath, - size, - }; - } catch (e) { - await Deno.remove(filepath); - throw e; - } - } else { - formFile = { - filename: p.fileName, - type: contentType, - content: buf.bytes(), - size: buf.length, - }; - maxMemory -= n; - maxValueBytes -= n; - } - if (formFile) { - const mapVal = fileMap.get(p.formName); - if (mapVal !== undefined) { - if (Array.isArray(mapVal)) { - mapVal.push(formFile); - } else { - fileMap.set(p.formName, [mapVal, formFile]); - } - } else { - fileMap.set(p.formName, formFile); - } - } - } - return multipartFormData(fileMap, valueMap); - } - - private currentPart: PartReader | undefined; - private partsRead = 0; - - private async nextPart(): Promise<PartReader | null> { - if (this.currentPart) { - this.currentPart.close(); - } - if (equals(this.dashBoundary, encoder.encode("--"))) { - throw new Error("boundary is empty"); - } - let expectNewPart = false; - for (;;) { - const line = await this.bufReader.readSlice("\n".charCodeAt(0)); - if (line === null) { - throw new Deno.errors.UnexpectedEof(); - } - if (this.isBoundaryDelimiterLine(line)) { - this.partsRead++; - const r = new TextProtoReader(this.bufReader); - const headers = await r.readMIMEHeader(); - if (headers === null) { - throw new Deno.errors.UnexpectedEof(); - } - const np = new PartReader(this, headers); - this.currentPart = np; - return np; - } - if (this.isFinalBoundary(line)) { - return null; - } - if (expectNewPart) { - throw new Error(`expecting a new Part; got line ${line}`); - } - if (this.partsRead === 0) { - continue; - } - if (equals(line, this.newLine)) { - expectNewPart = true; - continue; - } - throw new Error(`unexpected line in nextPart(): ${line}`); - } - } - - private isFinalBoundary(line: Uint8Array): boolean { - if (!startsWith(line, this.dashBoundaryDash)) { - return false; - } - const rest = line.slice(this.dashBoundaryDash.length, line.length); - return rest.length === 0 || equals(skipLWSPChar(rest), this.newLine); - } - - private isBoundaryDelimiterLine(line: Uint8Array): boolean { - if (!startsWith(line, this.dashBoundary)) { - return false; - } - const rest = line.slice(this.dashBoundary.length); - return equals(skipLWSPChar(rest), this.newLine); - } -} - -function multipartFormData( - fileMap: Map<string, FormFile | FormFile[]>, - valueMap: Map<string, string>, -): MultipartFormData { - function file(key: string): FormFile | FormFile[] | undefined { - return fileMap.get(key); - } - function value(key: string): string | undefined { - return valueMap.get(key); - } - function* entries(): IterableIterator< - [string, string | FormFile | FormFile[] | undefined] - > { - yield* fileMap; - yield* valueMap; - } - async function removeAll(): Promise<void> { - const promises: Array<Promise<void>> = []; - for (const val of fileMap.values()) { - if (Array.isArray(val)) { - for (const subVal of val) { - if (!subVal.tempfile) continue; - promises.push(Deno.remove(subVal.tempfile)); - } - } else { - if (!val.tempfile) continue; - promises.push(Deno.remove(val.tempfile)); - } - } - await Promise.all(promises); - } - return { - file, - value, - entries, - removeAll, - [Symbol.iterator](): IterableIterator< - [string, string | FormFile | FormFile[] | undefined] - > { - return entries(); - }, - }; -} - -class PartWriter implements Deno.Writer { - closed = false; - private readonly partHeader: string; - private headersWritten = false; - - constructor( - private writer: Deno.Writer, - readonly boundary: string, - public headers: Headers, - isFirstBoundary: boolean, - ) { - let buf = ""; - if (isFirstBoundary) { - buf += `--${boundary}\r\n`; - } else { - buf += `\r\n--${boundary}\r\n`; - } - for (const [key, value] of headers.entries()) { - buf += `${key}: ${value}\r\n`; - } - buf += `\r\n`; - this.partHeader = buf; - } - - close(): void { - this.closed = true; - } - - async write(p: Uint8Array): Promise<number> { - if (this.closed) { - throw new Error("part is closed"); - } - if (!this.headersWritten) { - await this.writer.write(encoder.encode(this.partHeader)); - this.headersWritten = true; - } - return this.writer.write(p); - } -} - -function checkBoundary(b: string): string { - if (b.length < 1 || b.length > 70) { - throw new Error(`invalid boundary length: ${b.length}`); - } - const end = b.length - 1; - for (let i = 0; i < end; i++) { - const c = b.charAt(i); - if (!c.match(/[a-zA-Z0-9'()+_,\-./:=?]/) || (c === " " && i !== end)) { - throw new Error("invalid boundary character: " + c); - } - } - return b; -} - -/** Writer for creating multipart/form-data */ -export class MultipartWriter { - private readonly _boundary: string; - - get boundary(): string { - return this._boundary; - } - - private lastPart: PartWriter | undefined; - private bufWriter: BufWriter; - private isClosed = false; - - constructor(private readonly writer: Deno.Writer, boundary?: string) { - if (boundary !== void 0) { - this._boundary = checkBoundary(boundary); - } else { - this._boundary = randomBoundary(); - } - this.bufWriter = new BufWriter(writer); - } - - formDataContentType(): string { - return `multipart/form-data; boundary=${this.boundary}`; - } - - private createPart(headers: Headers): Deno.Writer { - if (this.isClosed) { - throw new Error("multipart: writer is closed"); - } - if (this.lastPart) { - this.lastPart.close(); - } - const part = new PartWriter( - this.writer, - this.boundary, - headers, - !this.lastPart, - ); - this.lastPart = part; - return part; - } - - createFormFile(field: string, filename: string): Deno.Writer { - const h = new Headers(); - h.set( - "Content-Disposition", - `form-data; name="${field}"; filename="${filename}"`, - ); - h.set("Content-Type", "application/octet-stream"); - return this.createPart(h); - } - - createFormField(field: string): Deno.Writer { - const h = new Headers(); - h.set("Content-Disposition", `form-data; name="${field}"`); - h.set("Content-Type", "application/octet-stream"); - return this.createPart(h); - } - - async writeField(field: string, value: string): Promise<void> { - const f = await this.createFormField(field); - await f.write(encoder.encode(value)); - } - - async writeFile( - field: string, - filename: string, - file: Deno.Reader, - ): Promise<void> { - const f = await this.createFormFile(field, filename); - await Deno.copy(file, f); - } - - private flush(): Promise<void> { - return this.bufWriter.flush(); - } - - /** Close writer. No additional data can be written to stream */ - async close(): Promise<void> { - if (this.isClosed) { - throw new Error("multipart: writer is closed"); - } - if (this.lastPart) { - this.lastPart.close(); - this.lastPart = void 0; - } - await this.writer.write(encoder.encode(`\r\n--${this.boundary}--\r\n`)); - await this.flush(); - this.isClosed = true; - } -} |