feat: multipart, etc.. (#180)

This commit is contained in:
Yusuke Sakurai 2019-02-11 08:49:48 +09:00 committed by Ryan Dahl
parent 88ddd5677d
commit fda9c98d05
19 changed files with 1112 additions and 14 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.DS_Store
.idea
tsconfig.json
deno.d.ts

60
bytes/bytes.ts Normal file
View File

@ -0,0 +1,60 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
/** Find first index of binary pattern from a. If not found, then return -1 **/
export function bytesFindIndex(a: Uint8Array, pat: Uint8Array): number {
const s = pat[0];
for (let i = 0; i < a.length; i++) {
if (a[i] !== s) continue;
const pin = i;
let matched = 1;
while (matched < pat.length) {
i++;
if (a[i] !== pat[i - pin]) {
break;
}
matched++;
}
if (matched === pat.length) {
return pin;
}
}
return -1;
}
/** Find last index of binary pattern from a. If not found, then return -1 **/
export function bytesFindLastIndex(a: Uint8Array, pat: Uint8Array) {
const e = pat[pat.length - 1];
for (let i = a.length - 1; i >= 0; i--) {
if (a[i] !== e) continue;
const pin = i;
let matched = 1;
while (matched < pat.length) {
i--;
if (a[i] !== pat[pat.length - 1 - (pin - i)]) {
break;
}
matched++;
}
if (matched === pat.length) {
return pin - pat.length + 1;
}
}
return -1;
}
/** Check whether binary arrays are equal to each other **/
export function bytesEqual(a: Uint8Array, match: Uint8Array): boolean {
if (a.length !== match.length) return false;
for (let i = 0; i < match.length; i++) {
if (a[i] !== match[i]) return false;
}
return true;
}
/** Check whether binary array has binary prefix **/
export function bytesHasPrefix(a: Uint8Array, prefix: Uint8Array): boolean {
for (let i = 0, max = prefix.length; i < max; i++) {
if (a[i] !== prefix[i]) return false;
}
return true;
}

36
bytes/bytes_test.ts Normal file
View File

@ -0,0 +1,36 @@
import {
bytesFindIndex,
bytesFindLastIndex,
bytesEqual,
bytesHasPrefix
} from "./bytes.ts";
import { assertEqual, test } from "./deps.ts";
test(function bytesBytesFindIndex() {
const i = bytesFindIndex(
new Uint8Array([1, 2, 0, 1, 2, 0, 1, 2, 0, 1, 3]),
new Uint8Array([0, 1, 2])
);
assertEqual(i, 2);
});
test(function bytesBytesFindLastIndex1() {
const i = bytesFindLastIndex(
new Uint8Array([0, 1, 2, 0, 1, 2, 0, 1, 3]),
new Uint8Array([0, 1, 2])
);
assertEqual(i, 3);
});
test(function bytesBytesBytesEqual() {
const v = bytesEqual(
new Uint8Array([0, 1, 2, 3]),
new Uint8Array([0, 1, 2, 3])
);
assertEqual(v, true);
});
test(function bytesBytesHasPrefix() {
const v = bytesHasPrefix(new Uint8Array([0, 1, 2]), new Uint8Array([0, 1]));
assertEqual(v, true);
});

View File

@ -30,7 +30,7 @@ test(async function bufioReaderSimple() {
const data = "hello world";
const b = new BufReader(stringsReader(data));
const s = await readBytes(b);
assertEqual(s, data);
assert.equal(s, data);
});
type ReadMaker = { name: string; fn: (r: Reader) => Reader };

View File

@ -1,27 +1,55 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { BufReader } from "./bufio.ts";
import { Reader, Writer } from "deno";
import { assert } from "../testing/mod.ts";
/* Read big endian 16bit short from BufReader */
/** copy N size at the most. If read size is lesser than N, then returns nread */
export async function copyN(
dest: Writer,
r: Reader,
size: number
): Promise<number> {
let bytesRead = 0;
let buf = new Uint8Array(1024);
while (bytesRead < size) {
if (size - bytesRead < 1024) {
buf = new Uint8Array(size - bytesRead);
}
const { nread, eof } = await r.read(buf);
bytesRead += nread;
if (nread > 0) {
const n = await dest.write(buf.slice(0, nread));
assert.assert(n === nread, "could not write");
}
if (eof) {
break;
}
}
return bytesRead;
}
/** Read big endian 16bit short from BufReader */
export async function readShort(buf: BufReader): Promise<number> {
const [high, low] = [await buf.readByte(), await buf.readByte()];
return (high << 8) | low;
}
/* Read big endian 32bit integer from BufReader */
/** Read big endian 32bit integer from BufReader */
export async function readInt(buf: BufReader): Promise<number> {
const [high, low] = [await readShort(buf), await readShort(buf)];
return (high << 16) | low;
}
const BIT32 = 0xffffffff;
/* Read big endian 64bit long from BufReader */
/** Read big endian 64bit long from BufReader */
export async function readLong(buf: BufReader): Promise<number> {
const [high, low] = [await readInt(buf), await readInt(buf)];
// ECMAScript doesn't support 64bit bit ops.
return high ? high * (BIT32 + 1) + low : low;
}
/* Slice number into 64bit big endian byte array */
/** Slice number into 64bit big endian byte array */
export function sliceLongToBytes(d: number, dest = new Array(8)): number[] {
let mask = 0xff;
let low = (d << 32) >>> 32;

View File

@ -1,8 +1,15 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { Reader, ReadResult } from "deno";
import { assertEqual, test } from "../testing/mod.ts";
import { readInt, readLong, readShort, sliceLongToBytes } from "./ioutil.ts";
import { Buffer, Reader, ReadResult } from "deno";
import { assert, assertEqual, runTests, test } from "../testing/mod.ts";
import {
copyN,
readInt,
readLong,
readShort,
sliceLongToBytes
} from "./ioutil.ts";
import { BufReader } from "./bufio.ts";
import { stringsReader } from "./util.ts";
class BinaryReader implements Reader {
index = 0;
@ -61,3 +68,19 @@ test(async function testSliceLongToBytes2() {
const arr = sliceLongToBytes(0x12345678);
assertEqual(arr, [0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78]);
});
test(async function testCopyN1() {
const w = new Buffer();
const r = stringsReader("abcdefghij");
const n = await copyN(w, r, 3);
assert.equal(n, 3);
assert.equal(w.toString(), "abc");
});
test(async function testCopyN2() {
const w = new Buffer();
const r = stringsReader("abcdefghij");
const n = await copyN(w, r, 11);
assert.equal(n, 10);
assert.equal(w.toString(), "abcdefghij");
});

38
io/readers.ts Normal file
View File

@ -0,0 +1,38 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { Reader, ReadResult } from "deno";
import { encode } from "../strings/strings.ts";
/** Reader utility for strings */
export class StringReader implements Reader {
private offs = 0;
private buf = new Uint8Array(encode(this.s));
constructor(private readonly s: string) {}
async read(p: Uint8Array): Promise<ReadResult> {
const n = Math.min(p.byteLength, this.buf.byteLength - this.offs);
p.set(this.buf.slice(this.offs, this.offs + n));
this.offs += n;
return { nread: n, eof: this.offs === this.buf.byteLength };
}
}
/** Reader utility for combining multiple readers */
export class MultiReader implements Reader {
private readonly readers: Reader[];
private currentIndex = 0;
constructor(...readers: Reader[]) {
this.readers = readers;
}
async read(p: Uint8Array): Promise<ReadResult> {
const r = this.readers[this.currentIndex];
if (!r) return { nread: 0, eof: true };
const { nread, eof } = await r.read(p);
if (eof) {
this.currentIndex++;
}
return { nread, eof: false };
}
}

36
io/readers_test.ts Normal file
View File

@ -0,0 +1,36 @@
import { assert, test } from "../testing/mod.ts";
import { MultiReader, StringReader } from "./readers.ts";
import { StringWriter } from "./writers.ts";
import { copy } from "deno";
import { copyN } from "./ioutil.ts";
import { decode } from "../strings/strings.ts";
test(async function ioStringReader() {
const r = new StringReader("abcdef");
const { nread, eof } = await r.read(new Uint8Array(6));
assert.equal(nread, 6);
assert.equal(eof, true);
});
test(async function ioStringReader() {
const r = new StringReader("abcdef");
const buf = new Uint8Array(3);
let res1 = await r.read(buf);
assert.equal(res1.nread, 3);
assert.equal(res1.eof, false);
assert.equal(decode(buf), "abc");
let res2 = await r.read(buf);
assert.equal(res2.nread, 3);
assert.equal(res2.eof, true);
assert.equal(decode(buf), "def");
});
test(async function ioMultiReader() {
const r = new MultiReader(new StringReader("abc"), new StringReader("def"));
const w = new StringWriter();
const n = await copyN(w, r, 4);
assert.equal(n, 4);
assert.equal(w.toString(), "abcd");
await copy(w, r);
assert.equal(w.toString(), "abcdef");
});

View File

@ -1,6 +1,7 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { Buffer, Reader } from "deno";
import { Buffer, File, mkdir, open, Reader } from "deno";
import { encode } from "../strings/strings.ts";
import * as path from "../fs/path.ts";
// `off` is the offset into `dst` where it will at which to begin writing values
// from `src`.
// Returns the number of bytes copied.
@ -18,8 +19,23 @@ export function charCode(s: string): number {
return s.charCodeAt(0);
}
const encoder = new TextEncoder();
export function stringsReader(s: string): Reader {
const ui8 = encoder.encode(s);
return new Buffer(ui8.buffer as ArrayBuffer);
return new Buffer(encode(s).buffer);
}
/** Create or open a temporal file at specified directory with prefix and postfix */
export async function tempFile(
dir: string,
opts: {
prefix?: string;
postfix?: string;
} = { prefix: "", postfix: "" }
): Promise<{ file: File; filepath: string }> {
const r = Math.floor(Math.random() * 1000000);
const filepath = path.resolve(
`${dir}/${opts.prefix || ""}${r}${opts.postfix || ""}`
);
await mkdir(path.dirname(filepath), true);
const file = await open(filepath, "a");
return { file, filepath };
}

View File

@ -1,6 +1,8 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { test, assert } from "../testing/mod.ts";
import { copyBytes } from "./util.ts";
import { copyBytes, tempFile } from "./util.ts";
import { remove } from "deno";
import * as path from "../fs/path.ts";
test(function testCopyBytes() {
let dst = new Uint8Array(4);
@ -35,3 +37,14 @@ test(function testCopyBytes() {
assert(len === 2);
assert.equal(dst, Uint8Array.of(3, 4, 0, 0));
});
test(async function ioTempfile() {
const f = await tempFile(".", {
prefix: "prefix-",
postfix: "-postfix"
});
console.log(f.file, f.filepath);
const base = path.basename(f.filepath);
assert.assert(!!base.match(/^prefix-.+?-postfix$/));
await remove(f.filepath);
});

38
io/writers.ts Normal file
View File

@ -0,0 +1,38 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { Writer } from "deno";
import { decode, encode } from "../strings/strings.ts";
/** Writer utility for buffering string chunks */
export class StringWriter implements Writer {
private chunks: Uint8Array[] = [];
private byteLength: number = 0;
constructor(private base: string = "") {
const c = encode(base);
this.chunks.push(c);
this.byteLength += c.byteLength;
}
async write(p: Uint8Array): Promise<number> {
this.chunks.push(p);
this.byteLength += p.byteLength;
this.cache = null;
return p.byteLength;
}
private cache: string;
toString(): string {
if (this.cache) {
return this.cache;
}
const buf = new Uint8Array(this.byteLength);
let offs = 0;
for (const chunk of this.chunks) {
buf.set(chunk, offs);
offs += chunk.byteLength;
}
this.cache = decode(buf);
return this.cache;
}
}

14
io/writers_test.ts Normal file
View File

@ -0,0 +1,14 @@
import { assert, test } from "../testing/mod.ts";
import { StringWriter } from "./writers.ts";
import { StringReader } from "./readers.ts";
import { copyN } from "./ioutil.ts";
import { copy } from "deno";
test(async function ioStringWriter() {
const w = new StringWriter("base");
const r = new StringReader("0123456789");
const n = await copyN(w, r, 4);
assert.equal(w.toString(), "base0123");
await copy(w, r);
assert.equal(w.toString(), "base0123456789");
});

View File

@ -0,0 +1,27 @@
----------------------------434049563556637648550474
content-disposition: form-data; name="foo"
content-type: application/octet-stream
foo
----------------------------434049563556637648550474
content-disposition: form-data; name="bar"
content-type: application/octet-stream
bar
----------------------------434049563556637648550474
content-disposition: form-data; name="file"; filename="tsconfig.json"
content-type: application/octet-stream
{
"compilerOptions": {
"target": "es2018",
"baseUrl": ".",
"paths": {
"deno": ["./deno.d.ts"],
"https://*": ["../../.deno/deps/https/*"],
"http://*": ["../../.deno/deps/http/*"]
}
}
}
----------------------------434049563556637648550474--

24
multipart/formfile.ts Normal file
View File

@ -0,0 +1,24 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
/** FormFile object */
export type 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 */
export function isFormFile(x): x is FormFile {
return (
typeof x === "object" &&
x.hasOwnProperty("filename") &&
x.hasOwnProperty("type")
);
}

View File

@ -0,0 +1,19 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { assert, test } from "../testing/mod.ts";
import { isFormFile } from "./formfile.ts";
test(function multipartIsFormFile() {
assert.equal(
isFormFile({
filename: "foo",
type: "application/json"
}),
true
);
assert.equal(
isFormFile({
filename: "foo"
}),
false
);
});

492
multipart/multipart.ts Normal file
View File

@ -0,0 +1,492 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { Buffer, Closer, copy, Reader, ReadResult, remove, Writer } from "deno";
import { FormFile } from "./formfile.ts";
import {
bytesFindIndex,
bytesFindLastIndex,
bytesHasPrefix,
bytesEqual
} from "../bytes/bytes.ts";
import { copyN } from "../io/ioutil.ts";
import { MultiReader } from "../io/readers.ts";
import { tempFile } from "../io/util.ts";
import { BufReader, BufState, BufWriter } from "../io/bufio.ts";
import { TextProtoReader } from "../textproto/mod.ts";
import { encoder } from "../strings/strings.ts";
import * as path from "../fs/path.ts";
function randomBoundary() {
let boundary = "--------------------------";
for (let i = 0; i < 24; i++) {
boundary += Math.floor(Math.random() * 10).toString(16);
}
return boundary;
}
/** 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(private reader: 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 */
async readForm(
maxMemory: number
): Promise<{ [key: string]: string | FormFile }> {
const result = Object.create(null);
let maxValueBytes = maxMemory + (10 << 20);
const buf = new Buffer(new Uint8Array(maxValueBytes));
for (;;) {
const p = await this.nextPart();
if (!p) {
break;
}
if (p.formName === "") {
continue;
}
buf.reset();
if (!p.fileName) {
// value
const n = await copyN(buf, p, maxValueBytes);
maxValueBytes -= n;
if (maxValueBytes < 0) {
throw new RangeError("message too large");
}
const value = buf.toString();
result[p.formName] = value;
continue;
}
// file
let formFile: FormFile;
const n = await copy(buf, p);
if (n > maxMemory) {
// too big, write to disk and flush buffer
const ext = path.extname(p.fileName);
const { file, filepath } = await tempFile(".", {
prefix: "multipart-",
postfix: ext
});
try {
const size = await copyN(
file,
new MultiReader(buf, p),
maxValueBytes
);
file.close();
formFile = {
filename: p.fileName,
type: p.headers.get("content-type"),
tempfile: filepath,
size
};
} catch (e) {
await remove(filepath);
}
} else {
formFile = {
filename: p.fileName,
type: p.headers.get("content-type"),
content: buf.bytes(),
size: buf.bytes().byteLength
};
maxMemory -= n;
maxValueBytes -= n;
}
result[p.formName] = formFile;
}
return result;
}
private currentPart: PartReader;
private partsRead: number;
private async nextPart(): Promise<PartReader> {
if (this.currentPart) {
this.currentPart.close();
}
if (bytesEqual(this.dashBoundary, encoder.encode("--"))) {
throw new Error("boundary is empty");
}
let expectNewPart = false;
for (;;) {
const [line, state] = await this.bufReader.readSlice("\n".charCodeAt(0));
if (state === "EOF" && this.isFinalBoundary(line)) {
break;
}
if (state) {
throw new Error("aa" + state.toString());
}
if (this.isBoundaryDelimiterLine(line)) {
this.partsRead++;
const r = new TextProtoReader(this.bufReader);
const [headers, state] = await r.readMIMEHeader();
if (state) {
throw state;
}
const np = new PartReader(this, headers);
this.currentPart = np;
return np;
}
if (this.isFinalBoundary(line)) {
break;
}
if (expectNewPart) {
throw new Error(`expecting a new Part; got line ${line}`);
}
if (this.partsRead === 0) {
continue;
}
if (bytesEqual(line, this.newLine)) {
expectNewPart = true;
continue;
}
throw new Error(`unexpected line in next(): ${line}`);
}
}
private isFinalBoundary(line: Uint8Array) {
if (!bytesHasPrefix(line, this.dashBoundaryDash)) {
return false;
}
let rest = line.slice(this.dashBoundaryDash.length, line.length);
return rest.length === 0 || bytesEqual(skipLWSPChar(rest), this.newLine);
}
private isBoundaryDelimiterLine(line: Uint8Array) {
if (!bytesHasPrefix(line, this.dashBoundary)) {
return false;
}
const rest = line.slice(this.dashBoundary.length);
return bytesEqual(skipLWSPChar(rest), this.newLine);
}
}
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);
}
let i = 0;
class PartReader implements Reader, Closer {
n: number = 0;
total: number = 0;
bufState: BufState = null;
index = i++;
constructor(private mr: MultipartReader, public readonly headers: Headers) {}
async read(p: Uint8Array): Promise<ReadResult> {
const br = this.mr.bufReader;
const returnResult = (nread: number, bufState: BufState): ReadResult => {
if (bufState && bufState !== "EOF") {
throw bufState;
}
return { nread, eof: bufState === "EOF" };
};
if (this.n === 0 && !this.bufState) {
const [peek] = await br.peek(br.buffered());
const [n, state] = scanUntilBoundary(
peek,
this.mr.dashBoundary,
this.mr.newLineDashBoundary,
this.total,
this.bufState
);
this.n = n;
this.bufState = state;
if (this.n === 0 && !this.bufState) {
const [_, state] = await br.peek(peek.length + 1);
this.bufState = state;
if (this.bufState === "EOF") {
this.bufState = new RangeError("unexpected eof");
}
}
}
if (this.n === 0) {
return returnResult(0, this.bufState);
}
let n = 0;
if (p.byteLength > this.n) {
n = this.n;
}
const buf = p.slice(0, n);
const [nread] = await this.mr.bufReader.readFull(buf);
p.set(buf);
this.total += nread;
this.n -= nread;
if (this.n === 0) {
return returnResult(n, this.bufState);
}
return returnResult(n, null);
}
close(): void {}
private contentDisposition: string;
private contentDispositionParams: { [key: string]: string };
private getContentDispositionParams() {
if (this.contentDispositionParams) return this.contentDispositionParams;
const cd = this.headers.get("content-disposition");
const params = {};
const comps = cd.split(";");
this.contentDisposition = comps[0];
comps
.slice(1)
.map(v => v.trim())
.map(kv => {
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 "";
}
}
export function scanUntilBoundary(
buf: Uint8Array,
dashBoundary: Uint8Array,
newLineDashBoundary: Uint8Array,
total: number,
state: BufState
): [number, BufState] {
if (total === 0) {
if (bytesHasPrefix(buf, dashBoundary)) {
switch (matchAfterPrefix(buf, dashBoundary, state)) {
case -1:
return [dashBoundary.length, null];
case 0:
return [0, null];
case 1:
return [0, "EOF"];
}
if (bytesHasPrefix(dashBoundary, buf)) {
return [0, state];
}
}
}
const i = bytesFindIndex(buf, newLineDashBoundary);
if (i >= 0) {
switch (matchAfterPrefix(buf.slice(i), newLineDashBoundary, state)) {
case -1:
return [i + newLineDashBoundary.length, null];
case 0:
return [i, null];
case 1:
return [i, "EOF"];
}
}
if (bytesHasPrefix(newLineDashBoundary, buf)) {
return [0, state];
}
const j = bytesFindLastIndex(buf, newLineDashBoundary.slice(0, 1));
if (j >= 0 && bytesHasPrefix(newLineDashBoundary, buf.slice(j))) {
return [j, null];
}
return [buf.length, state];
}
export function matchAfterPrefix(
a: Uint8Array,
prefix: Uint8Array,
bufState: BufState
): number {
if (a.length === prefix.length) {
if (bufState) {
return 1;
}
return 0;
}
const c = a[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;
}
class PartWriter implements Writer {
closed = false;
private readonly partHeader: string;
private headersWritten: boolean = false;
constructor(
private writer: 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) {
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() {
return this._boundary;
}
private lastPart: PartWriter;
private bufWriter: BufWriter;
private isClosed: boolean = false;
constructor(private readonly writer: 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): 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): 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): 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) {
const f = await this.createFormField(field);
await f.write(encoder.encode(value));
}
async writeFile(field: string, filename: string, file: Reader) {
const f = await this.createFormFile(field, filename);
await copy(f, file);
}
private flush(): Promise<BufState> {
return this.bufWriter.flush();
}
/** Close writer. No additional data can be writen to stream */
async close() {
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;
}
}

208
multipart/multipart_test.ts Normal file
View File

@ -0,0 +1,208 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { assert, test } from "../testing/mod.ts";
import {
matchAfterPrefix,
MultipartReader,
MultipartWriter,
scanUntilBoundary
} from "./multipart.ts";
import { Buffer, copy, open, remove } from "deno";
import * as path from "../fs/path.ts";
import { FormFile, isFormFile } from "./formfile.ts";
import { StringWriter } from "../io/writers.ts";
const e = new TextEncoder();
const d = new TextDecoder();
const boundary = "--abcde";
const dashBoundary = e.encode("--" + boundary);
const nlDashBoundary = e.encode("\r\n--" + boundary);
test(function multipartScanUntilBoundary1() {
const data = `--${boundary}`;
const [n, err] = scanUntilBoundary(
e.encode(data),
dashBoundary,
nlDashBoundary,
0,
"EOF"
);
assert.equal(n, 0);
assert.equal(err, "EOF");
});
test(function multipartScanUntilBoundary2() {
const data = `foo\r\n--${boundary}`;
const [n, err] = scanUntilBoundary(
e.encode(data),
dashBoundary,
nlDashBoundary,
0,
"EOF"
);
assert.equal(n, 3);
assert.equal(err, "EOF");
});
test(function multipartScanUntilBoundary4() {
const data = `foo\r\n--`;
const [n, err] = scanUntilBoundary(
e.encode(data),
dashBoundary,
nlDashBoundary,
0,
null
);
assert.equal(n, 3);
assert.equal(err, null);
});
test(function multipartScanUntilBoundary3() {
const data = `foobar`;
const [n, err] = scanUntilBoundary(
e.encode(data),
dashBoundary,
nlDashBoundary,
0,
null
);
assert.equal(n, data.length);
assert.equal(err, null);
});
test(function multipartMatchAfterPrefix1() {
const data = `${boundary}\r`;
const v = matchAfterPrefix(e.encode(data), e.encode(boundary), null);
assert.equal(v, 1);
});
test(function multipartMatchAfterPrefix2() {
const data = `${boundary}hoge`;
const v = matchAfterPrefix(e.encode(data), e.encode(boundary), null);
assert.equal(v, -1);
});
test(function multipartMatchAfterPrefix3() {
const data = `${boundary}`;
const v = matchAfterPrefix(e.encode(data), e.encode(boundary), null);
assert.equal(v, 0);
});
test(async function multipartMultipartWriter() {
const buf = new Buffer();
const mw = new MultipartWriter(buf);
await mw.writeField("foo", "foo");
await mw.writeField("bar", "bar");
const f = await open(path.resolve("./multipart/fixtures/sample.txt"), "r");
await mw.writeFile("file", "sample.txt", f);
await mw.close();
});
test(function multipartMultipartWriter2() {
const w = new StringWriter();
assert.throws(
() => new MultipartWriter(w, ""),
Error,
"invalid boundary length"
);
assert.throws(
() =>
new MultipartWriter(
w,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
),
Error,
"invalid boundary length"
);
assert.throws(
() => new MultipartWriter(w, "aaa aaa"),
Error,
"invalid boundary character"
);
assert.throws(
() => new MultipartWriter(w, "boundary¥¥"),
Error,
"invalid boundary character"
);
});
test(async function multipartMultipartWriter3() {
const w = new StringWriter();
const mw = new MultipartWriter(w);
await mw.writeField("foo", "foo");
await mw.close();
await assert.throwsAsync(
async () => {
await mw.close();
},
Error,
"closed"
);
await assert.throwsAsync(
async () => {
await mw.writeFile("bar", "file", null);
},
Error,
"closed"
);
await assert.throwsAsync(
async () => {
await mw.writeField("bar", "bar");
},
Error,
"closed"
);
assert.throws(
() => {
mw.createFormField("bar");
},
Error,
"closed"
);
assert.throws(
() => {
mw.createFormFile("bar", "file");
},
Error,
"closed"
);
});
test(async function multipartMultipartReader() {
// FIXME: path resolution
const o = await open(path.resolve("./multipart/fixtures/sample.txt"));
const mr = new MultipartReader(
o,
"--------------------------434049563556637648550474"
);
const form = await mr.readForm(10 << 20);
assert.equal(form["foo"], "foo");
assert.equal(form["bar"], "bar");
const file = form["file"] as FormFile;
assert.equal(isFormFile(file), true);
assert.assert(file.content !== void 0);
});
test(async function multipartMultipartReader2() {
const o = await open(path.resolve("./multipart/fixtures/sample.txt"));
const mr = new MultipartReader(
o,
"--------------------------434049563556637648550474"
);
const form = await mr.readForm(20); //
try {
assert.equal(form["foo"], "foo");
assert.equal(form["bar"], "bar");
const file = form["file"] as FormFile;
assert.equal(file.type, "application/octet-stream");
const f = await open(file.tempfile);
const w = new StringWriter();
await copy(w, f);
const json = JSON.parse(w.toString());
assert.equal(json["compilerOptions"]["target"], "es2018");
f.close();
} finally {
const file = form["file"] as FormFile;
await remove(file.tempfile);
}
});

15
strings/strings.ts Normal file
View File

@ -0,0 +1,15 @@
/** A default TextEncoder instance */
export const encoder = new TextEncoder();
/** Shorthand for new TextEncoder().encode() */
export function encode(input?: string): Uint8Array {
return encoder.encode(input);
}
/** A default TextDecoder instance */
export const decoder = new TextDecoder();
/** Shorthand for new TextDecoder().decode() */
export function decode(input?: Uint8Array): string {
return decoder.decode(input);
}

View File

@ -4,12 +4,19 @@ import "colors/test.ts";
import "datetime/test.ts";
import "examples/test.ts";
import "flags/test.ts";
import "io/bufio_test.ts";
import "io/ioutil_test.ts";
import "io/util_test.ts";
import "io/writers_test.ts";
import "io/readers_test.ts";
import "fs/path/test.ts";
import "io/test.ts";
import "http/server_test.ts";
import "http/file_server_test.ts";
import "log/test.ts";
import "media_types/test.ts";
import "multipart/formfile_test.ts";
import "multipart/multipart_test.ts";
import "prettier/main_test.ts";
import "testing/test.ts";
import "textproto/test.ts";