mirror of
https://github.com/denoland/deno.git
synced 2024-11-29 16:30:56 -05:00
parent
ed20bda6ec
commit
33f62789cd
19 changed files with 1112 additions and 14 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
tsconfig.json
|
||||||
|
deno.d.ts
|
60
bytes/bytes.ts
Normal file
60
bytes/bytes.ts
Normal 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
36
bytes/bytes_test.ts
Normal 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);
|
||||||
|
});
|
|
@ -30,7 +30,7 @@ test(async function bufioReaderSimple() {
|
||||||
const data = "hello world";
|
const data = "hello world";
|
||||||
const b = new BufReader(stringsReader(data));
|
const b = new BufReader(stringsReader(data));
|
||||||
const s = await readBytes(b);
|
const s = await readBytes(b);
|
||||||
assertEqual(s, data);
|
assert.equal(s, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
type ReadMaker = { name: string; fn: (r: Reader) => Reader };
|
type ReadMaker = { name: string; fn: (r: Reader) => Reader };
|
||||||
|
|
36
io/ioutil.ts
36
io/ioutil.ts
|
@ -1,27 +1,55 @@
|
||||||
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
||||||
import { BufReader } from "./bufio.ts";
|
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> {
|
export async function readShort(buf: BufReader): Promise<number> {
|
||||||
const [high, low] = [await buf.readByte(), await buf.readByte()];
|
const [high, low] = [await buf.readByte(), await buf.readByte()];
|
||||||
return (high << 8) | low;
|
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> {
|
export async function readInt(buf: BufReader): Promise<number> {
|
||||||
const [high, low] = [await readShort(buf), await readShort(buf)];
|
const [high, low] = [await readShort(buf), await readShort(buf)];
|
||||||
return (high << 16) | low;
|
return (high << 16) | low;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BIT32 = 0xffffffff;
|
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> {
|
export async function readLong(buf: BufReader): Promise<number> {
|
||||||
const [high, low] = [await readInt(buf), await readInt(buf)];
|
const [high, low] = [await readInt(buf), await readInt(buf)];
|
||||||
// ECMAScript doesn't support 64bit bit ops.
|
// ECMAScript doesn't support 64bit bit ops.
|
||||||
return high ? high * (BIT32 + 1) + low : low;
|
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[] {
|
export function sliceLongToBytes(d: number, dest = new Array(8)): number[] {
|
||||||
let mask = 0xff;
|
let mask = 0xff;
|
||||||
let low = (d << 32) >>> 32;
|
let low = (d << 32) >>> 32;
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
||||||
import { Reader, ReadResult } from "deno";
|
import { Buffer, Reader, ReadResult } from "deno";
|
||||||
import { assertEqual, test } from "../testing/mod.ts";
|
import { assert, assertEqual, runTests, test } from "../testing/mod.ts";
|
||||||
import { readInt, readLong, readShort, sliceLongToBytes } from "./ioutil.ts";
|
import {
|
||||||
|
copyN,
|
||||||
|
readInt,
|
||||||
|
readLong,
|
||||||
|
readShort,
|
||||||
|
sliceLongToBytes
|
||||||
|
} from "./ioutil.ts";
|
||||||
import { BufReader } from "./bufio.ts";
|
import { BufReader } from "./bufio.ts";
|
||||||
|
import { stringsReader } from "./util.ts";
|
||||||
|
|
||||||
class BinaryReader implements Reader {
|
class BinaryReader implements Reader {
|
||||||
index = 0;
|
index = 0;
|
||||||
|
@ -61,3 +68,19 @@ test(async function testSliceLongToBytes2() {
|
||||||
const arr = sliceLongToBytes(0x12345678);
|
const arr = sliceLongToBytes(0x12345678);
|
||||||
assertEqual(arr, [0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78]);
|
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
38
io/readers.ts
Normal 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
36
io/readers_test.ts
Normal 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");
|
||||||
|
});
|
26
io/util.ts
26
io/util.ts
|
@ -1,6 +1,7 @@
|
||||||
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
// 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
|
// `off` is the offset into `dst` where it will at which to begin writing values
|
||||||
// from `src`.
|
// from `src`.
|
||||||
// Returns the number of bytes copied.
|
// Returns the number of bytes copied.
|
||||||
|
@ -18,8 +19,23 @@ export function charCode(s: string): number {
|
||||||
return s.charCodeAt(0);
|
return s.charCodeAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const encoder = new TextEncoder();
|
|
||||||
export function stringsReader(s: string): Reader {
|
export function stringsReader(s: string): Reader {
|
||||||
const ui8 = encoder.encode(s);
|
return new Buffer(encode(s).buffer);
|
||||||
return new Buffer(ui8.buffer as ArrayBuffer);
|
}
|
||||||
|
|
||||||
|
/** 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 };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
|
||||||
import { test, assert } from "../testing/mod.ts";
|
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() {
|
test(function testCopyBytes() {
|
||||||
let dst = new Uint8Array(4);
|
let dst = new Uint8Array(4);
|
||||||
|
@ -35,3 +37,14 @@ test(function testCopyBytes() {
|
||||||
assert(len === 2);
|
assert(len === 2);
|
||||||
assert.equal(dst, Uint8Array.of(3, 4, 0, 0));
|
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
38
io/writers.ts
Normal 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
14
io/writers_test.ts
Normal 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");
|
||||||
|
});
|
27
multipart/fixtures/sample.txt
Normal file
27
multipart/fixtures/sample.txt
Normal 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
24
multipart/formfile.ts
Normal 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")
|
||||||
|
);
|
||||||
|
}
|
19
multipart/formfile_test.ts
Normal file
19
multipart/formfile_test.ts
Normal 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
492
multipart/multipart.ts
Normal 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
208
multipart/multipart_test.ts
Normal 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
15
strings/strings.ts
Normal 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);
|
||||||
|
}
|
7
test.ts
7
test.ts
|
@ -4,12 +4,19 @@ import "colors/test.ts";
|
||||||
import "datetime/test.ts";
|
import "datetime/test.ts";
|
||||||
import "examples/test.ts";
|
import "examples/test.ts";
|
||||||
import "flags/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 "fs/path/test.ts";
|
||||||
import "io/test.ts";
|
import "io/test.ts";
|
||||||
import "http/server_test.ts";
|
import "http/server_test.ts";
|
||||||
import "http/file_server_test.ts";
|
import "http/file_server_test.ts";
|
||||||
import "log/test.ts";
|
import "log/test.ts";
|
||||||
import "media_types/test.ts";
|
import "media_types/test.ts";
|
||||||
|
import "multipart/formfile_test.ts";
|
||||||
|
import "multipart/multipart_test.ts";
|
||||||
import "prettier/main_test.ts";
|
import "prettier/main_test.ts";
|
||||||
import "testing/test.ts";
|
import "testing/test.ts";
|
||||||
import "textproto/test.ts";
|
import "textproto/test.ts";
|
||||||
|
|
Loading…
Reference in a new issue