From 0ee6334b698072b50c6f5ac8d42d34dc4c94b948 Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Thu, 23 May 2019 19:04:06 -0700 Subject: [PATCH] io: refactor BufReader/Writer interfaces to be more idiomatic (#444) Thanks Vincent Le Goff (@zekth) for porting over the CSV reader implementation. Fixes: #436 --- encoding/csv.ts | 47 ++--- encoding/csv_test.ts | 35 ++-- http/file_server_test.ts | 8 +- http/racing_server_test.ts | 14 +- http/server.ts | 142 ++++++++------ http/server_test.ts | 151 ++++++++------ io/bufio.ts | 392 ++++++++++++++++++++----------------- io/bufio_test.ts | 112 ++++++----- mime/multipart.ts | 216 +++++++++++--------- mime/multipart_test.ts | 45 +++-- textproto/mod.ts | 58 +++--- textproto/reader_test.ts | 59 +++--- ws/mod.ts | 106 +++++----- ws/test.ts | 10 +- 14 files changed, 761 insertions(+), 634 deletions(-) diff --git a/encoding/csv.ts b/encoding/csv.ts index 3d50180cc872..191961ace284 100644 --- a/encoding/csv.ts +++ b/encoding/csv.ts @@ -2,7 +2,7 @@ // https://github.com/golang/go/blob/go1.12.5/src/encoding/csv/ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. -import { BufReader, BufState } from "../io/bufio.ts"; +import { BufReader, EOF } from "../io/bufio.ts"; import { TextProtoReader } from "../textproto/mod.ts"; const INVALID_RUNE = ["\r", "\n", '"']; @@ -25,30 +25,29 @@ export interface ParseOptions { fieldsPerRecord?: number; } -function chkOptions(opt: ParseOptions): Error | null { +function chkOptions(opt: ParseOptions): void { if ( INVALID_RUNE.includes(opt.comma) || INVALID_RUNE.includes(opt.comment) || opt.comma === opt.comment ) { - return Error("Invalid Delimiter"); + throw new Error("Invalid Delimiter"); } - return null; } export async function read( Startline: number, reader: BufReader, opt: ParseOptions = { comma: ",", comment: "#", trimLeadingSpace: false } -): Promise<[string[], BufState]> { +): Promise { const tp = new TextProtoReader(reader); - let err: BufState; let line: string; let result: string[] = []; let lineIndex = Startline; - [line, err] = await tp.readLine(); - + const r = await tp.readLine(); + if (r === EOF) return EOF; + line = r; // Normalize \r\n to \n on all input lines. if ( line.length >= 2 && @@ -61,12 +60,12 @@ export async function read( const trimmedLine = line.trimLeft(); if (trimmedLine.length === 0) { - return [[], err]; + return []; } // line starting with comment character is ignored if (opt.comment && trimmedLine[0] === opt.comment) { - return [result, err]; + return []; } result = line.split(opt.comma); @@ -92,12 +91,9 @@ export async function read( } ); if (quoteError) { - return [ - [], - new ParseError(Startline, lineIndex, 'bare " in non-quoted-field') - ]; + throw new ParseError(Startline, lineIndex, 'bare " in non-quoted-field'); } - return [result, err]; + return result; } export async function readAll( @@ -107,19 +103,18 @@ export async function readAll( trimLeadingSpace: false, lazyQuotes: false } -): Promise<[string[][], BufState]> { +): Promise { const result: string[][] = []; let _nbFields: number; - let err: BufState; let lineResult: string[]; let first = true; let lineIndex = 0; - err = chkOptions(opt); - if (err) return [result, err]; + chkOptions(opt); for (;;) { - [lineResult, err] = await read(lineIndex, reader, opt); - if (err) break; + const r = await read(lineIndex, reader, opt); + if (r === EOF) break; + lineResult = r; lineIndex++; // If fieldsPerRecord is 0, Read sets it to // the number of fields in the first record @@ -136,16 +131,10 @@ export async function readAll( if (lineResult.length > 0) { if (_nbFields && _nbFields !== lineResult.length) { - return [ - null, - new ParseError(lineIndex, lineIndex, "wrong number of fields") - ]; + throw new ParseError(lineIndex, lineIndex, "wrong number of fields"); } result.push(lineResult); } } - if (err !== "EOF") { - return [result, err]; - } - return [result, null]; + return result; } diff --git a/encoding/csv_test.ts b/encoding/csv_test.ts index 1ca68ea16234..40a2abcef9a9 100644 --- a/encoding/csv_test.ts +++ b/encoding/csv_test.ts @@ -437,20 +437,31 @@ for (const t of testCases) { if (t.LazyQuotes) { lazyquote = t.LazyQuotes; } - const actual = await readAll(new BufReader(new StringReader(t.Input)), { - comma: comma, - comment: comment, - trimLeadingSpace: trim, - fieldsPerRecord: fieldsPerRec, - lazyQuotes: lazyquote - }); + let actual; if (t.Error) { - assert(!!actual[1]); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const e: any = actual[1]; - assertEquals(e.message, t.Error); + let err; + try { + actual = await readAll(new BufReader(new StringReader(t.Input)), { + comma: comma, + comment: comment, + trimLeadingSpace: trim, + fieldsPerRecord: fieldsPerRec, + lazyQuotes: lazyquote + }); + } catch (e) { + err = e; + } + assert(err); + assertEquals(err.message, t.Error); } else { - const expected = [t.Output, null]; + actual = await readAll(new BufReader(new StringReader(t.Input)), { + comma: comma, + comment: comment, + trimLeadingSpace: trim, + fieldsPerRecord: fieldsPerRec, + lazyQuotes: lazyquote + }); + const expected = t.Output; assertEquals(actual, expected); } } diff --git a/http/file_server_test.ts b/http/file_server_test.ts index 578b0e6241e7..1e2d86c4da1c 100644 --- a/http/file_server_test.ts +++ b/http/file_server_test.ts @@ -3,7 +3,7 @@ const { readFile, run } = Deno; import { test } from "../testing/mod.ts"; import { assert, assertEquals } from "../testing/asserts.ts"; -import { BufReader } from "../io/bufio.ts"; +import { BufReader, EOF } from "../io/bufio.ts"; import { TextProtoReader } from "../textproto/mod.ts"; let fileServer; @@ -22,10 +22,10 @@ async function startFileServer(): Promise { }); // Once fileServer is ready it will write to its stdout. const r = new TextProtoReader(new BufReader(fileServer.stdout)); - const [s, err] = await r.readLine(); - assert(err == null); - assert(s.includes("server listening")); + const s = await r.readLine(); + assert(s !== EOF && s.includes("server listening")); } + function killFileServer(): void { fileServer.close(); fileServer.stdout.close(); diff --git a/http/racing_server_test.ts b/http/racing_server_test.ts index cdcdca1a74eb..f98072c16fa7 100644 --- a/http/racing_server_test.ts +++ b/http/racing_server_test.ts @@ -1,8 +1,8 @@ const { dial, run } = Deno; -import { test } from "../testing/mod.ts"; +import { test, runIfMain } from "../testing/mod.ts"; import { assert, assertEquals } from "../testing/asserts.ts"; -import { BufReader } from "../io/bufio.ts"; +import { BufReader, EOF } from "../io/bufio.ts"; import { TextProtoReader } from "../textproto/mod.ts"; let server; @@ -13,9 +13,8 @@ async function startServer(): Promise { }); // Once fileServer is ready it will write to its stdout. const r = new TextProtoReader(new BufReader(server.stdout)); - const [s, err] = await r.readLine(); - assert(err == null); - assert(s.includes("Racing server listening...")); + const s = await r.readLine(); + assert(s !== EOF && s.includes("Racing server listening...")); } function killServer(): void { server.close(); @@ -57,9 +56,10 @@ test(async function serverPipelineRace(): Promise { const outLines = output.split("\n"); // length - 1 to disregard last empty line for (let i = 0; i < outLines.length - 1; i++) { - const [s, err] = await r.readLine(); - assert(!err); + const s = await r.readLine(); assertEquals(s, outLines[i]); } killServer(); }); + +runIfMain(import.meta); diff --git a/http/server.ts b/http/server.ts index 68a9d8780ecd..bdf48fca348a 100644 --- a/http/server.ts +++ b/http/server.ts @@ -4,10 +4,10 @@ type Listener = Deno.Listener; type Conn = Deno.Conn; type Reader = Deno.Reader; type Writer = Deno.Writer; -import { BufReader, BufState, BufWriter } from "../io/bufio.ts"; +import { BufReader, BufWriter, EOF, UnexpectedEOFError } from "../io/bufio.ts"; import { TextProtoReader } from "../textproto/mod.ts"; import { STATUS_TEXT } from "./http_status.ts"; -import { assert, fail } from "../testing/asserts.ts"; +import { assert } from "../testing/asserts.ts"; import { collectUint8Arrays, deferred, @@ -134,7 +134,8 @@ export class ServerRequest { if (transferEncodings.includes("chunked")) { // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6 const tp = new TextProtoReader(this.r); - let [line] = await tp.readLine(); + let line = await tp.readLine(); + if (line === EOF) throw new UnexpectedEOFError(); // TODO: handle chunk extension let [chunkSizeString] = line.split(";"); let chunkSize = parseInt(chunkSizeString, 16); @@ -142,18 +143,18 @@ export class ServerRequest { throw new Error("Invalid chunk size"); } while (chunkSize > 0) { - let data = new Uint8Array(chunkSize); - let [nread] = await this.r.readFull(data); - if (nread !== chunkSize) { - throw new Error("Chunk data does not match size"); + const data = new Uint8Array(chunkSize); + if ((await this.r.readFull(data)) === EOF) { + throw new UnexpectedEOFError(); } yield data; await this.r.readLine(); // Consume \r\n - [line] = await tp.readLine(); + line = await tp.readLine(); + if (line === EOF) throw new UnexpectedEOFError(); chunkSize = parseInt(line, 16); } - const [entityHeaders, err] = await tp.readMIMEHeader(); - if (!err) { + const entityHeaders = await tp.readMIMEHeader(); + if (entityHeaders !== EOF) { for (let [k, v] of entityHeaders) { this.headers.set(k, v); } @@ -220,70 +221,78 @@ function fixLength(req: ServerRequest): void { // ParseHTTPVersion parses a HTTP version string. // "HTTP/1.0" returns (1, 0, true). // Ported from https://github.com/golang/go/blob/f5c43b9/src/net/http/request.go#L766-L792 -export function parseHTTPVersion(vers: string): [number, number, boolean] { - const Big = 1000000; // arbitrary upper bound - const digitReg = /^\d+$/; // test if string is only digit - let major: number; - let minor: number; - +export function parseHTTPVersion(vers: string): [number, number] { switch (vers) { case "HTTP/1.1": - return [1, 1, true]; + return [1, 1]; + case "HTTP/1.0": - return [1, 0, true]; - } + return [1, 0]; - if (!vers.startsWith("HTTP/")) { - return [0, 0, false]; - } + default: { + const Big = 1000000; // arbitrary upper bound + const digitReg = /^\d+$/; // test if string is only digit + let major: number; + let minor: number; - const dot = vers.indexOf("."); - if (dot < 0) { - return [0, 0, false]; - } + if (!vers.startsWith("HTTP/")) { + break; + } - let majorStr = vers.substring(vers.indexOf("/") + 1, dot); - major = parseInt(majorStr); - if (!digitReg.test(majorStr) || isNaN(major) || major < 0 || major > Big) { - return [0, 0, false]; - } + const dot = vers.indexOf("."); + if (dot < 0) { + break; + } + + let majorStr = vers.substring(vers.indexOf("/") + 1, dot); + major = parseInt(majorStr); + if ( + !digitReg.test(majorStr) || + isNaN(major) || + major < 0 || + major > Big + ) { + break; + } - let minorStr = vers.substring(dot + 1); - minor = parseInt(minorStr); - if (!digitReg.test(minorStr) || isNaN(minor) || minor < 0 || minor > Big) { - return [0, 0, false]; + let minorStr = vers.substring(dot + 1); + minor = parseInt(minorStr); + if ( + !digitReg.test(minorStr) || + isNaN(minor) || + minor < 0 || + minor > Big + ) { + break; + } + + return [major, minor]; + } } - return [major, minor, true]; + + throw new Error(`malformed HTTP version ${vers}`); } export async function readRequest( bufr: BufReader -): Promise<[ServerRequest, BufState]> { +): Promise { + const tp = new TextProtoReader(bufr); + const firstLine = await tp.readLine(); // e.g. GET /index.html HTTP/1.0 + if (firstLine === EOF) return EOF; + const headers = await tp.readMIMEHeader(); + if (headers === EOF) throw new UnexpectedEOFError(); + const req = new ServerRequest(); req.r = bufr; - const tp = new TextProtoReader(bufr); - let err: BufState; - // First line: GET /index.html HTTP/1.0 - let firstLine: string; - [firstLine, err] = await tp.readLine(); - if (err) { - return [null, err]; - } [req.method, req.url, req.proto] = firstLine.split(" ", 3); - - let ok: boolean; - [req.protoMinor, req.protoMajor, ok] = parseHTTPVersion(req.proto); - if (!ok) { - throw Error(`malformed HTTP version ${req.proto}`); - } - - [req.headers, err] = await tp.readMIMEHeader(); + [req.protoMinor, req.protoMajor] = parseHTTPVersion(req.proto); + req.headers = headers; fixLength(req); // TODO(zekth) : add parsing of headers eg: // rfc: https://tools.ietf.org/html/rfc7230#section-3.3.2 // A sender MUST NOT send a Content-Length header field in any message // that contains a Transfer-Encoding header field. - return [req, err]; + return req; } export class Server implements AsyncIterable { @@ -302,36 +311,39 @@ export class Server implements AsyncIterable { ): AsyncIterableIterator { const bufr = new BufReader(conn); const w = new BufWriter(conn); - let bufStateErr: BufState; - let req: ServerRequest; + let req: ServerRequest | EOF; + let err: Error | undefined; while (!this.closing) { try { - [req, bufStateErr] = await readRequest(bufr); - } catch (err) { - bufStateErr = err; + req = await readRequest(bufr); + } catch (e) { + err = e; + break; + } + if (req === EOF) { + break; } - if (bufStateErr) break; + req.w = w; yield req; + // Wait for the request to be processed before we accept a new request on // this connection. await req.done; } - if (bufStateErr === "EOF") { + if (req === EOF) { // The connection was gracefully closed. - } else if (bufStateErr instanceof Error) { + } else if (err) { // An error was thrown while parsing request headers. await writeResponse(req.w, { status: 400, - body: new TextEncoder().encode(`${bufStateErr.message}\r\n\r\n`) + body: new TextEncoder().encode(`${err.message}\r\n\r\n`) }); } else if (this.closing) { // There are more requests incoming but the server is closing. // TODO(ry): send a back a HTTP 503 Service Unavailable status. - } else { - fail(`unexpected BufState: ${bufStateErr}`); } conn.close(); diff --git a/http/server_test.ts b/http/server_test.ts index fbab0234f042..32f12cc4083c 100644 --- a/http/server_test.ts +++ b/http/server_test.ts @@ -7,7 +7,7 @@ const { Buffer } = Deno; import { test, runIfMain } from "../testing/mod.ts"; -import { assert, assertEquals } from "../testing/asserts.ts"; +import { assert, assertEquals, assertNotEquals } from "../testing/asserts.ts"; import { Response, ServerRequest, @@ -15,9 +15,20 @@ import { readRequest, parseHTTPVersion } from "./server.ts"; -import { BufReader, BufWriter } from "../io/bufio.ts"; +import { + BufReader, + BufWriter, + EOF, + ReadLineResult, + UnexpectedEOFError +} from "../io/bufio.ts"; import { StringReader } from "../io/readers.ts"; +function assertNotEOF(val: T | EOF): T { + assertNotEquals(val, EOF); + return val as T; +} + interface ResponseTest { response: Response; raw: string; @@ -247,21 +258,25 @@ test(async function writeUint8ArrayResponse(): Promise { const decoder = new TextDecoder("utf-8"); const reader = new BufReader(buf); - let line: Uint8Array; - line = (await reader.readLine())[0]; - assertEquals(decoder.decode(line), "HTTP/1.1 200 OK"); + let r: ReadLineResult; + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); + assertEquals(r.more, false); - line = (await reader.readLine())[0]; - assertEquals(decoder.decode(line), `content-length: ${shortText.length}`); + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), `content-length: ${shortText.length}`); + assertEquals(r.more, false); - line = (await reader.readLine())[0]; - assertEquals(line.byteLength, 0); + r = assertNotEOF(await reader.readLine()); + assertEquals(r.line.byteLength, 0); + assertEquals(r.more, false); - line = (await reader.readLine())[0]; - assertEquals(decoder.decode(line), shortText); + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), shortText); + assertEquals(r.more, false); - line = (await reader.readLine())[0]; - assertEquals(line.byteLength, 0); + const eof = await reader.readLine(); + assertEquals(eof, EOF); }); test(async function writeStringReaderResponse(): Promise { @@ -276,24 +291,30 @@ test(async function writeStringReaderResponse(): Promise { const decoder = new TextDecoder("utf-8"); const reader = new BufReader(buf); - let line: Uint8Array; - line = (await reader.readLine())[0]; - assertEquals(decoder.decode(line), "HTTP/1.1 200 OK"); + let r: ReadLineResult; + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK"); + assertEquals(r.more, false); - line = (await reader.readLine())[0]; - assertEquals(decoder.decode(line), "transfer-encoding: chunked"); + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), "transfer-encoding: chunked"); + assertEquals(r.more, false); - line = (await reader.readLine())[0]; - assertEquals(line.byteLength, 0); + r = assertNotEOF(await reader.readLine()); + assertEquals(r.line.byteLength, 0); + assertEquals(r.more, false); - line = (await reader.readLine())[0]; - assertEquals(decoder.decode(line), shortText.length.toString()); + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), shortText.length.toString()); + assertEquals(r.more, false); - line = (await reader.readLine())[0]; - assertEquals(decoder.decode(line), shortText); + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), shortText); + assertEquals(r.more, false); - line = (await reader.readLine())[0]; - assertEquals(decoder.decode(line), "0"); + r = assertNotEOF(await reader.readLine()); + assertEquals(decoder.decode(r.line), "0"); + assertEquals(r.more, false); }); test(async function readRequestError(): Promise { @@ -318,19 +339,20 @@ test(async function testReadRequestError(): Promise { const testCases = { 0: { in: "GET / HTTP/1.1\r\nheader: foo\r\n\r\n", - headers: [{ key: "header", value: "foo" }], - err: null + headers: [{ key: "header", value: "foo" }] }, - 1: { in: "GET / HTTP/1.1\r\nheader:foo\r\n", err: "EOF", headers: [] }, - 2: { in: "", err: "EOF", headers: [] }, + 1: { + in: "GET / HTTP/1.1\r\nheader:foo\r\n", + err: UnexpectedEOFError + }, + 2: { in: "", err: EOF }, 3: { in: "HEAD / HTTP/1.1\r\nContent-Length:4\r\n\r\n", err: "http: method cannot contain a Content-Length" }, 4: { in: "HEAD / HTTP/1.1\r\n\r\n", - headers: [], - err: null + headers: [] }, // Multiple Content-Length values should either be // deduplicated if same or reject otherwise @@ -348,7 +370,6 @@ test(async function testReadRequestError(): Promise { 7: { in: "PUT / HTTP/1.1\r\nContent-Length: 6 \r\nContent-Length: 6\r\nContent-Length:6\r\n\r\nGopher\r\n", - err: null, headers: [{ key: "Content-Length", value: "6" }] }, 8: { @@ -363,24 +384,28 @@ test(async function testReadRequestError(): Promise { // }, 10: { in: "HEAD / HTTP/1.1\r\nContent-Length:0\r\nContent-Length: 0\r\n\r\n", - headers: [{ key: "Content-Length", value: "0" }], - err: null + headers: [{ key: "Content-Length", value: "0" }] } }; for (const p in testCases) { const test = testCases[p]; const reader = new BufReader(new StringReader(test.in)); - let _err; - if (test.err && test.err != "EOF") { - try { - await readRequest(reader); - } catch (e) { - _err = e; - } - assertEquals(_err.message, test.err); + let err; + let req; + try { + req = await readRequest(reader); + } catch (e) { + err = e; + } + if (test.err === EOF) { + assertEquals(req, EOF); + } else if (typeof test.err === "string") { + assertEquals(err.message, test.err); + } else if (test.err) { + assert(err instanceof test.err); } else { - const [req, err] = await readRequest(reader); - assertEquals(test.err, err); + assertEquals(err, undefined); + assertNotEquals(req, EOF); for (const h of test.headers) { assertEquals(req.headers.get(h.key), h.value); } @@ -393,21 +418,31 @@ test({ name: "[http] parseHttpVersion", fn(): void { const testCases = [ - { in: "HTTP/0.9", want: [0, 9, true] }, - { in: "HTTP/1.0", want: [1, 0, true] }, - { in: "HTTP/1.1", want: [1, 1, true] }, - { in: "HTTP/3.14", want: [3, 14, true] }, - { in: "HTTP", want: [0, 0, false] }, - { in: "HTTP/one.one", want: [0, 0, false] }, - { in: "HTTP/1.1/", want: [0, 0, false] }, - { in: "HTTP/-1.0", want: [0, 0, false] }, - { in: "HTTP/0.-1", want: [0, 0, false] }, - { in: "HTTP/", want: [0, 0, false] }, - { in: "HTTP/1,0", want: [0, 0, false] } + { in: "HTTP/0.9", want: [0, 9] }, + { in: "HTTP/1.0", want: [1, 0] }, + { in: "HTTP/1.1", want: [1, 1] }, + { in: "HTTP/3.14", want: [3, 14] }, + { in: "HTTP", err: true }, + { in: "HTTP/one.one", err: true }, + { in: "HTTP/1.1/", err: true }, + { in: "HTTP/-1.0", err: true }, + { in: "HTTP/0.-1", err: true }, + { in: "HTTP/", err: true }, + { in: "HTTP/1,0", err: true } ]; for (const t of testCases) { - const r = parseHTTPVersion(t.in); - assertEquals(r, t.want, t.in); + let r, err; + try { + r = parseHTTPVersion(t.in); + } catch (e) { + err = e; + } + if (t.err) { + assert(err instanceof Error, t.in); + } else { + assertEquals(err, undefined); + assertEquals(r, t.want, t.in); + } } } }); diff --git a/io/bufio.ts b/io/bufio.ts index 749a7e8fafbe..815c94eed8cd 100644 --- a/io/bufio.ts +++ b/io/bufio.ts @@ -15,13 +15,28 @@ const MAX_CONSECUTIVE_EMPTY_READS = 100; const CR = charCode("\r"); const LF = charCode("\n"); -export type BufState = - | null - | "EOF" - | "BufferFull" - | "ShortWrite" - | "NoProgress" - | Error; +export class BufferFullError extends Error { + name = "BufferFullError"; + constructor(public partial: Uint8Array) { + super("Buffer full"); + } +} + +export class UnexpectedEOFError extends Error { + name = "UnexpectedEOFError"; + constructor() { + super("Unexpected EOF"); + } +} + +export const EOF: unique symbol = Symbol("EOF"); +export type EOF = typeof EOF; + +/** Result type returned by of BufReader.readLine(). */ +export interface ReadLineResult { + line: Uint8Array; + more: boolean; +} /** BufReader implements buffering for a Reader object. */ export class BufReader implements Reader { @@ -29,9 +44,9 @@ export class BufReader implements Reader { private rd: Reader; // Reader provided by caller. private r = 0; // buf read position. private w = 0; // buf write position. - private lastByte: number; - private lastCharSize: number; - private err: BufState; + private eof = false; + // private lastByte: number; + // private lastCharSize: number; /** return new BufReader unless r is BufReader */ static create(r: Reader, size = DEFAULT_BUF_SIZE): BufReader { @@ -54,12 +69,6 @@ export class BufReader implements Reader { return this.w - this.r; } - private _readErr(): BufState { - const err = this.err; - this.err = null; - return err; - } - // Reads a new chunk into the buffer. private async _fill(): Promise { // Slide existing data to beginning. @@ -75,24 +84,21 @@ export class BufReader implements Reader { // Read new data: try a limited number of times. for (let i = MAX_CONSECUTIVE_EMPTY_READS; i > 0; i--) { - let rr: ReadResult; - try { - rr = await this.rd.read(this.buf.subarray(this.w)); - } catch (e) { - this.err = e; - return; - } + let rr: ReadResult = await this.rd.read(this.buf.subarray(this.w)); assert(rr.nread >= 0, "negative read"); this.w += rr.nread; if (rr.eof) { - this.err = "EOF"; + this.eof = true; return; } if (rr.nread > 0) { return; } } - this.err = "NoProgress"; + + throw new Error( + `No progress after ${MAX_CONSECUTIVE_EMPTY_READS} read() calls` + ); } /** Discards any buffered data, resets all state, and switches @@ -105,108 +111,96 @@ export class BufReader implements Reader { private _reset(buf: Uint8Array, rd: Reader): void { this.buf = buf; this.rd = rd; - this.lastByte = -1; - // this.lastRuneSize = -1; + this.eof = false; + // this.lastByte = -1; + // this.lastCharSize = -1; } /** reads data into p. * It returns the number of bytes read into p. * The bytes are taken from at most one Read on the underlying Reader, * hence n may be less than len(p). - * At EOF, the count will be zero and err will be io.EOF. * To read exactly len(p) bytes, use io.ReadFull(b, p). */ async read(p: Uint8Array): Promise { let rr: ReadResult = { nread: p.byteLength, eof: false }; - if (rr.nread === 0) { - if (this.err) { - throw this._readErr(); - } - return rr; - } + if (p.byteLength === 0) return rr; if (this.r === this.w) { - if (this.err) { - throw this._readErr(); - } if (p.byteLength >= this.buf.byteLength) { // Large read, empty buffer. // Read directly into p to avoid copy. - rr = await this.rd.read(p); + const rr = await this.rd.read(p); assert(rr.nread >= 0, "negative read"); - if (rr.nread > 0) { - this.lastByte = p[rr.nread - 1]; - // this.lastRuneSize = -1; - } - if (this.err) { - throw this._readErr(); - } + // if (rr.nread > 0) { + // this.lastByte = p[rr.nread - 1]; + // this.lastCharSize = -1; + // } return rr; } + // One read. // Do not use this.fill, which will loop. this.r = 0; this.w = 0; - try { - rr = await this.rd.read(this.buf); - } catch (e) { - this.err = e; - } + rr = await this.rd.read(this.buf); assert(rr.nread >= 0, "negative read"); - if (rr.nread === 0) { - if (this.err) { - throw this._readErr(); - } - return rr; - } + if (rr.nread === 0) return rr; this.w += rr.nread; } // copy as much as we can - rr.nread = copyBytes(p as Uint8Array, this.buf.subarray(this.r, this.w), 0); + rr.nread = copyBytes(p, this.buf.subarray(this.r, this.w), 0); this.r += rr.nread; - this.lastByte = this.buf[this.r - 1]; - // this.lastRuneSize = -1; + // this.lastByte = this.buf[this.r - 1]; + // this.lastCharSize = -1; return rr; } - /** reads exactly len(p) bytes into p. + /** reads exactly `p.length` bytes into `p`. + * + * If successful, `p` is returned. + * + * If the end of the underlying stream has been reached, and there are no more + * bytes available in the buffer, `readFull()` returns `EOF` instead. + * + * An error is thrown if some bytes could be read, but not enough to fill `p` + * entirely before the underlying stream reported an error or EOF. Any error + * thrown will have a `partial` property that indicates the slice of the + * buffer that has been successfully filled with data. + * * Ported from https://golang.org/pkg/io/#ReadFull - * It returns the number of bytes copied and an error if fewer bytes were read. - * The error is EOF only if no bytes were read. - * If an EOF happens after reading some but not all the bytes, - * readFull returns ErrUnexpectedEOF. ("EOF" for current impl) - * On return, n == len(p) if and only if err == nil. - * If r returns an error having read at least len(buf) bytes, - * the error is dropped. */ - async readFull(p: Uint8Array): Promise<[number, BufState]> { - let rr = await this.read(p); - let nread = rr.nread; - if (rr.eof) { - return [nread, nread < p.length ? "EOF" : null]; - } - while (!rr.eof && nread < p.length) { - rr = await this.read(p.subarray(nread)); - nread += rr.nread; + async readFull(p: Uint8Array): Promise { + let bytesRead = 0; + while (bytesRead < p.length) { + try { + const rr = await this.read(p.subarray(bytesRead)); + bytesRead += rr.nread; + if (rr.eof) { + if (bytesRead === 0) { + return EOF; + } else { + throw new UnexpectedEOFError(); + } + } + } catch (err) { + err.partial = p.subarray(0, bytesRead); + throw err; + } } - return [nread, nread < p.length ? "EOF" : null]; + return p; } /** Returns the next byte [0, 255] or -1 if EOF. */ async readByte(): Promise { while (this.r === this.w) { + if (this.eof) return -1; await this._fill(); // buffer is empty. - if (this.err == "EOF") { - return -1; - } - if (this.err != null) { - throw this._readErr(); - } } const c = this.buf[this.r]; this.r++; - this.lastByte = c; + // this.lastByte = c; return c; } @@ -218,46 +212,73 @@ export class BufReader implements Reader { * delim. * For simple uses, a Scanner may be more convenient. */ - async readString(_delim: string): Promise { + async readString(_delim: string): Promise { throw new Error("Not implemented"); } - /** readLine() is a low-level line-reading primitive. Most callers should use - * readBytes('\n') or readString('\n') instead or use a Scanner. + /** `readLine()` is a low-level line-reading primitive. Most callers should + * use `readString('\n')` instead or use a Scanner. * - * readLine tries to return a single line, not including the end-of-line bytes. - * If the line was too long for the buffer then isPrefix is set and the + * `readLine()` tries to return a single line, not including the end-of-line + * bytes. If the line was too long for the buffer then `more` is set and the * beginning of the line is returned. The rest of the line will be returned - * from future calls. isPrefix will be false when returning the last fragment + * from future calls. `more` will be false when returning the last fragment * of the line. The returned buffer is only valid until the next call to - * ReadLine. ReadLine either returns a non-nil line or it returns an error, - * never both. + * `readLine()`. * - * The text returned from ReadLine does not include the line end ("\r\n" or "\n"). - * No indication or error is given if the input ends without a final line end. - * Calling UnreadByte after ReadLine will always unread the last byte read - * (possibly a character belonging to the line end) even if that byte is not - * part of the line returned by ReadLine. + * The text returned from ReadLine does not include the line end ("\r\n" or + * "\n"). + * + * When the end of the underlying stream is reached, the final bytes in the + * stream are returned. No indication or error is given if the input ends + * without a final line end. When there are no more trailing bytes to read, + * `readLine()` returns the `EOF` symbol. + * + * Calling `unreadByte()` after `readLine()` will always unread the last byte + * read (possibly a character belonging to the line end) even if that byte is + * not part of the line returned by `readLine()`. */ - async readLine(): Promise<[Uint8Array, boolean, BufState]> { - let [line, err] = await this.readSlice(LF); + async readLine(): Promise { + let line: Uint8Array | EOF; + + try { + line = await this.readSlice(LF); + } catch (err) { + let { partial } = err; + assert( + partial instanceof Uint8Array, + "bufio: caught error from `readSlice()` without `partial` property" + ); + + // Don't throw if `readSlice()` failed with `BufferFullError`, instead we + // just return whatever is available and set the `more` flag. + if (!(err instanceof BufferFullError)) { + throw err; + } - if (err === "BufferFull") { // Handle the case where "\r\n" straddles the buffer. - if (line.byteLength > 0 && line[line.byteLength - 1] === CR) { + if ( + !this.eof && + partial.byteLength > 0 && + partial[partial.byteLength - 1] === CR + ) { // Put the '\r' back on buf and drop it from line. // Let the next call to ReadLine check for "\r\n". assert(this.r > 0, "bufio: tried to rewind past start of buffer"); this.r--; - line = line.subarray(0, line.byteLength - 1); + partial = partial.subarray(0, partial.byteLength - 1); } - return [line, true, null]; + + return { line: partial, more: !this.eof }; + } + + if (line === EOF) { + return EOF; } if (line.byteLength === 0) { - return [line, false, err]; + return { line, more: false }; } - err = null; if (line[line.byteLength - 1] == LF) { let drop = 1; @@ -266,98 +287,112 @@ export class BufReader implements Reader { } line = line.subarray(0, line.byteLength - drop); } - return [line, false, err]; + return { line, more: false }; } - /** readSlice() reads until the first occurrence of delim in the input, + /** `readSlice()` reads until the first occurrence of `delim` in the input, * returning a slice pointing at the bytes in the buffer. The bytes stop - * being valid at the next read. If readSlice() encounters an error before - * finding a delimiter, it returns all the data in the buffer and the error - * itself (often io.EOF). readSlice() fails with error ErrBufferFull if the - * buffer fills without a delim. Because the data returned from readSlice() - * will be overwritten by the next I/O operation, most clients should use - * readBytes() or readString() instead. readSlice() returns err != nil if and - * only if line does not end in delim. + * being valid at the next read. + * + * If `readSlice()` encounters an error before finding a delimiter, or the + * buffer fills without finding a delimiter, it throws an error with a + * `partial` property that contains the entire buffer. + * + * If `readSlice()` encounters the end of the underlying stream and there are + * any bytes left in the buffer, the rest of the buffer is returned. In other + * words, EOF is always treated as a delimiter. Once the buffer is empty, + * it returns `EOF`. + * + * Because the data returned from `readSlice()` will be overwritten by the + * next I/O operation, most clients should use `readString()` instead. */ - async readSlice(delim: number): Promise<[Uint8Array, BufState]> { + async readSlice(delim: number): Promise { let s = 0; // search start index - let line: Uint8Array; - let err: BufState; + let slice: Uint8Array; + while (true) { // Search buffer. let i = this.buf.subarray(this.r + s, this.w).indexOf(delim); if (i >= 0) { i += s; - line = this.buf.subarray(this.r, this.r + i + 1); + slice = this.buf.subarray(this.r, this.r + i + 1); this.r += i + 1; break; } - // Pending error? - if (this.err) { - line = this.buf.subarray(this.r, this.w); + // EOF? + if (this.eof) { + if (this.r === this.w) { + return EOF; + } + slice = this.buf.subarray(this.r, this.w); this.r = this.w; - err = this._readErr(); break; } // Buffer full? if (this.buffered() >= this.buf.byteLength) { this.r = this.w; - line = this.buf; - err = "BufferFull"; - break; + throw new BufferFullError(this.buf); } s = this.w - this.r; // do not rescan area we scanned before - await this._fill(); // buffer is not full + // Buffer is not full. + try { + await this._fill(); + } catch (err) { + err.partial = slice; + throw err; + } } // Handle last byte, if any. - let i = line.byteLength - 1; - if (i >= 0) { - this.lastByte = line[i]; - // this.lastRuneSize = -1 - } + // const i = slice.byteLength - 1; + // if (i >= 0) { + // this.lastByte = slice[i]; + // this.lastCharSize = -1 + // } - return [line, err]; + return slice; } - /** Peek returns the next n bytes without advancing the reader. The bytes stop - * being valid at the next read call. If Peek returns fewer than n bytes, it - * also returns an error explaining why the read is short. The error is - * ErrBufferFull if n is larger than b's buffer size. + /** `peek()` returns the next `n` bytes without advancing the reader. The + * bytes stop being valid at the next read call. + * + * When the end of the underlying stream is reached, but there are unread + * bytes left in the buffer, those bytes are returned. If there are no bytes + * left in the buffer, it returns `EOF`. + * + * If an error is encountered before `n` bytes are available, `peek()` throws + * an error with the `partial` property set to a slice of the buffer that + * contains the bytes that were available before the error occurred. */ - async peek(n: number): Promise<[Uint8Array, BufState]> { + async peek(n: number): Promise { if (n < 0) { throw Error("negative count"); } - while ( - this.w - this.r < n && - this.w - this.r < this.buf.byteLength && - this.err == null - ) { - await this._fill(); // this.w - this.r < len(this.buf) => buffer is not full + let avail = this.w - this.r; + while (avail < n && avail < this.buf.byteLength && !this.eof) { + try { + await this._fill(); + } catch (err) { + err.partial = this.buf.subarray(this.r, this.w); + throw err; + } + avail = this.w - this.r; } - if (n > this.buf.byteLength) { - return [this.buf.subarray(this.r, this.w), "BufferFull"]; + if (avail === 0 && this.eof) { + return EOF; + } else if (avail < n && this.eof) { + return this.buf.subarray(this.r, this.r + avail); + } else if (avail < n) { + throw new BufferFullError(this.buf.subarray(this.r, this.w)); } - // 0 <= n <= len(this.buf) - let err: BufState; - let avail = this.w - this.r; - if (avail < n) { - // not enough data in buffer - n = avail; - err = this._readErr(); - if (!err) { - err = "BufferFull"; - } - } - return [this.buf.subarray(this.r, this.r + n), err]; + return this.buf.subarray(this.r, this.r + n); } } @@ -371,7 +406,7 @@ export class BufReader implements Reader { export class BufWriter implements Writer { buf: Uint8Array; n: number = 0; - err: null | BufState = null; + err: Error | null = null; /** return new BufWriter unless w is BufWriter */ static create(w: Writer, size = DEFAULT_BUF_SIZE): BufWriter { @@ -400,34 +435,27 @@ export class BufWriter implements Writer { } /** Flush writes any buffered data to the underlying io.Writer. */ - async flush(): Promise { - if (this.err != null) { - return this.err; - } - if (this.n == 0) { - return null; - } + async flush(): Promise { + if (this.err !== null) throw this.err; + if (this.n === 0) return; let n: number; - let err: BufState = null; try { n = await this.wr.write(this.buf.subarray(0, this.n)); } catch (e) { - err = e; + this.err = e; + throw e; } - if (n < this.n && err == null) { - err = "ShortWrite"; - } - - if (err != null) { - if (n > 0 && n < this.n) { + if (n < this.n) { + if (n > 0) { this.buf.copyWithin(0, n, this.n); + this.n -= n; } - this.n -= n; - this.err = err; - return err; + this.err = new Error("Short write"); + throw this.err; } + this.n = 0; } @@ -447,16 +475,20 @@ export class BufWriter implements Writer { * Returns the number of bytes written. */ async write(p: Uint8Array): Promise { + if (this.err !== null) throw this.err; + if (p.length === 0) return 0; + let nn = 0; let n: number; - while (p.byteLength > this.available() && !this.err) { - if (this.buffered() == 0) { + while (p.byteLength > this.available()) { + if (this.buffered() === 0) { // Large write, empty buffer. // Write directly from p to avoid copy. try { n = await this.wr.write(p); } catch (e) { this.err = e; + throw e; } } else { n = copyBytes(this.buf, p, this.n); @@ -466,9 +498,7 @@ export class BufWriter implements Writer { nn += n; p = p.subarray(n); } - if (this.err) { - throw this.err; - } + n = copyBytes(this.buf, p, this.n); this.n += n; nn += n; diff --git a/io/bufio_test.ts b/io/bufio_test.ts index d1db119d8a43..84b6f9142c9e 100644 --- a/io/bufio_test.ts +++ b/io/bufio_test.ts @@ -6,14 +6,30 @@ const { Buffer } = Deno; type Reader = Deno.Reader; type ReadResult = Deno.ReadResult; -import { test } from "../testing/mod.ts"; -import { assert, assertEquals } from "../testing/asserts.ts"; -import { BufReader, BufWriter } from "./bufio.ts"; +import { test, runIfMain } from "../testing/mod.ts"; +import { + assert, + assertEquals, + assertNotEquals, + fail +} from "../testing/asserts.ts"; +import { + BufReader, + BufWriter, + EOF, + BufferFullError, + UnexpectedEOFError +} from "./bufio.ts"; import * as iotest from "./iotest.ts"; import { charCode, copyBytes, stringsReader } from "./util.ts"; const encoder = new TextEncoder(); +function assertNotEOF(val: T | EOF): T { + assertNotEquals(val, EOF); + return val as T; +} + async function readBytes(buf: BufReader): Promise { const b = new Uint8Array(1000); let nb = 0; @@ -129,17 +145,20 @@ test(async function bufioBufferFull(): Promise { const longString = "And now, hello, world! It is the time for all good men to come to the aid of their party"; const buf = new BufReader(stringsReader(longString), MIN_READ_BUFFER_SIZE); - let [line, err] = await buf.readSlice(charCode("!")); - const decoder = new TextDecoder(); - let actual = decoder.decode(line); - assertEquals(err, "BufferFull"); - assertEquals(actual, "And now, hello, "); - [line, err] = await buf.readSlice(charCode("!")); - actual = decoder.decode(line); + try { + await buf.readSlice(charCode("!")); + fail("readSlice should throw"); + } catch (err) { + assert(err instanceof BufferFullError); + assert(err.partial instanceof Uint8Array); + assertEquals(decoder.decode(err.partial), "And now, hello, "); + } + + const line = assertNotEOF(await buf.readSlice(charCode("!"))); + const actual = decoder.decode(line); assertEquals(actual, "world!"); - assert(err == null); }); const testInput = encoder.encode( @@ -178,14 +197,12 @@ async function testReadLine(input: Uint8Array): Promise { let reader = new TestReader(input, stride); let l = new BufReader(reader, input.byteLength + 1); while (true) { - let [line, isPrefix, err] = await l.readLine(); - if (line.byteLength > 0 && err != null) { - throw Error("readLine returned both data and error"); - } - assertEquals(isPrefix, false); - if (err == "EOF") { + const r = await l.readLine(); + if (r === EOF) { break; } + const { line, more } = r; + assertEquals(more, false); // eslint-disable-next-line @typescript-eslint/restrict-plus-operands let want = testOutput.subarray(done, done + line.byteLength); assertEquals( @@ -218,56 +235,51 @@ test(async function bufioPeek(): Promise { MIN_READ_BUFFER_SIZE ); - let [actual, err] = await buf.peek(1); + let actual = assertNotEOF(await buf.peek(1)); assertEquals(decoder.decode(actual), "a"); - assert(err == null); - [actual, err] = await buf.peek(4); + actual = assertNotEOF(await buf.peek(4)); assertEquals(decoder.decode(actual), "abcd"); - assert(err == null); - [actual, err] = await buf.peek(32); - assertEquals(decoder.decode(actual), "abcdefghijklmnop"); - assertEquals(err, "BufferFull"); + try { + await buf.peek(32); + fail("peek() should throw"); + } catch (err) { + assert(err instanceof BufferFullError); + assert(err.partial instanceof Uint8Array); + assertEquals(decoder.decode(err.partial), "abcdefghijklmnop"); + } await buf.read(p.subarray(0, 3)); assertEquals(decoder.decode(p.subarray(0, 3)), "abc"); - [actual, err] = await buf.peek(1); + actual = assertNotEOF(await buf.peek(1)); assertEquals(decoder.decode(actual), "d"); - assert(err == null); - [actual, err] = await buf.peek(1); + actual = assertNotEOF(await buf.peek(1)); assertEquals(decoder.decode(actual), "d"); - assert(err == null); - [actual, err] = await buf.peek(1); + actual = assertNotEOF(await buf.peek(1)); assertEquals(decoder.decode(actual), "d"); - assert(err == null); - [actual, err] = await buf.peek(2); + actual = assertNotEOF(await buf.peek(2)); assertEquals(decoder.decode(actual), "de"); - assert(err == null); let { eof } = await buf.read(p.subarray(0, 3)); assertEquals(decoder.decode(p.subarray(0, 3)), "def"); assert(!eof); - assert(err == null); - [actual, err] = await buf.peek(4); + actual = assertNotEOF(await buf.peek(4)); assertEquals(decoder.decode(actual), "ghij"); - assert(err == null); await buf.read(p); assertEquals(decoder.decode(p), "ghijklmnop"); - [actual, err] = await buf.peek(0); + actual = assertNotEOF(await buf.peek(0)); assertEquals(decoder.decode(actual), ""); - assert(err == null); - [actual, err] = await buf.peek(1); - assertEquals(decoder.decode(actual), ""); - assert(err == "EOF"); + const r = await buf.peek(1); + assert(r === EOF); /* TODO // Test for issue 3022, not exposing a reader's error on a successful Peek. buf = NewReaderSize(dataAndEOFReader("abcd"), 32) @@ -328,16 +340,22 @@ test(async function bufReaderReadFull(): Promise { const bufr = new BufReader(data, 3); { const buf = new Uint8Array(6); - const [nread, err] = await bufr.readFull(buf); - assertEquals(nread, 6); - assert(!err); + const r = assertNotEOF(await bufr.readFull(buf)); + assertEquals(r, buf); assertEquals(dec.decode(buf), "Hello "); } { const buf = new Uint8Array(6); - const [nread, err] = await bufr.readFull(buf); - assertEquals(nread, 5); - assertEquals(err, "EOF"); - assertEquals(dec.decode(buf.subarray(0, 5)), "World"); + try { + await bufr.readFull(buf); + fail("readFull() should throw"); + } catch (err) { + assert(err instanceof UnexpectedEOFError); + assert(err.partial instanceof Uint8Array); + assertEquals(err.partial.length, 5); + assertEquals(dec.decode(buf.subarray(0, 5)), "World"); + } } }); + +runIfMain(import.meta); diff --git a/mime/multipart.ts b/mime/multipart.ts index 832211a27fae..580b81dc3138 100644 --- a/mime/multipart.ts +++ b/mime/multipart.ts @@ -1,19 +1,21 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. const { Buffer, copy, remove } = Deno; +const { min, max } = Math; type Closer = Deno.Closer; type Reader = Deno.Reader; type ReadResult = Deno.ReadResult; type Writer = Deno.Writer; import { FormFile } from "../multipart/formfile.ts"; -import * as bytes from "../bytes/mod.ts"; +import { equal, findIndex, findLastIndex, hasPrefix } from "../bytes/mod.ts"; +import { extname } from "../fs/path.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 { BufReader, BufWriter, EOF, UnexpectedEOFError } from "../io/bufio.ts"; import { encoder } from "../strings/mod.ts"; -import * as path from "../fs/path.ts"; +import { assertStrictEq } from "../testing/asserts.ts"; +import { TextProtoReader } from "../textproto/mod.ts"; function randomBoundary(): string { let boundary = "--------------------------"; @@ -23,18 +25,31 @@ function randomBoundary(): string { 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( - a: Uint8Array, + buf: Uint8Array, prefix: Uint8Array, - bufState: BufState -): number { - if (a.length === prefix.length) { - if (bufState) { - return 1; - } - return 0; + eof: boolean +): -1 | 0 | 1 { + if (buf.length === prefix.length) { + return eof ? 1 : 0; } - const c = a[prefix.length]; + const c = buf[prefix.length]; if ( c === " ".charCodeAt(0) || c === "\t".charCodeAt(0) || @@ -47,105 +62,117 @@ export function matchAfterPrefix( 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, - state: BufState -): [number, BufState] { + eof: boolean +): number | EOF { if (total === 0) { - if (bytes.hasPrefix(buf, dashBoundary)) { - switch (matchAfterPrefix(buf, dashBoundary, state)) { + // At beginning of body, allow dashBoundary. + if (hasPrefix(buf, dashBoundary)) { + switch (matchAfterPrefix(buf, dashBoundary, eof)) { case -1: - return [dashBoundary.length, null]; + return dashBoundary.length; case 0: - return [0, null]; + return 0; case 1: - return [0, "EOF"]; - } - if (bytes.hasPrefix(dashBoundary, buf)) { - return [0, state]; + return EOF; } } + if (hasPrefix(dashBoundary, buf)) { + return 0; + } } - const i = bytes.findIndex(buf, newLineDashBoundary); + + // Search for "\n--boundary". + const i = findIndex(buf, newLineDashBoundary); if (i >= 0) { - switch (matchAfterPrefix(buf.slice(i), newLineDashBoundary, state)) { + switch (matchAfterPrefix(buf.slice(i), newLineDashBoundary, eof)) { case -1: - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - return [i + newLineDashBoundary.length, null]; + return i + newLineDashBoundary.length; case 0: - return [i, null]; + return i; case 1: - return [i, "EOF"]; + return i > 0 ? i : EOF; } } - if (bytes.hasPrefix(newLineDashBoundary, buf)) { - return [0, state]; + if (hasPrefix(newLineDashBoundary, buf)) { + return 0; } - const j = bytes.findLastIndex(buf, newLineDashBoundary.slice(0, 1)); - if (j >= 0 && bytes.hasPrefix(newLineDashBoundary, buf.slice(j))) { - return [j, null]; + + // 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 = findLastIndex(buf, newLineDashBoundary.slice(0, 1)); + if (j >= 0 && hasPrefix(newLineDashBoundary, buf.slice(j))) { + return j; } - return [buf.length, state]; -} -let i = 0; + return buf.length; +} class PartReader implements Reader, Closer { - n: number = 0; + n: number | EOF = 0; total: number = 0; - bufState: BufState = null; - index = i++; constructor(private mr: MultipartReader, public readonly headers: Headers) {} async read(p: Uint8Array): Promise { const br = this.mr.bufReader; - const returnResult = (nread: number, bufState: BufState): ReadResult => { - if (bufState && bufState !== "EOF") { - throw bufState; + + // 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 = max(peekLength, br.buffered()); + const peekBuf = await br.peek(peekLength); + if (peekBuf === EOF) { + throw new UnexpectedEOFError(); } - return { nread, eof: bufState === "EOF" }; - }; - if (this.n === 0 && !this.bufState) { - const [peek] = await br.peek(br.buffered()); - const [n, state] = scanUntilBoundary( - peek, + const eof = peekBuf.length < peekLength; + this.n = scanUntilBoundary( + peekBuf, this.mr.dashBoundary, this.mr.newLineDashBoundary, this.total, - this.bufState + eof ); - this.n = n; - this.bufState = state; - if (this.n === 0 && !this.bufState) { - // eslint-disable-next-line @typescript-eslint/restrict-plus-operands - 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) { + // Force buffered I/O to read more into buffer. + assertStrictEq(eof, false); + peekLength++; } } - if (this.n === 0) { - return returnResult(0, this.bufState); - } - let n = 0; - if (p.byteLength > this.n) { - n = this.n; + if (this.n === EOF) { + return { nread: 0, eof: true }; } - const buf = p.slice(0, n); - const [nread] = await this.mr.bufReader.readFull(buf); - p.set(buf); - this.total += nread; + + const nread = min(p.length, this.n); + const buf = p.subarray(0, nread); + const r = await br.readFull(buf); + assertStrictEq(r, buf); this.n -= nread; - if (this.n === 0) { - return returnResult(n, this.bufState); - } - return returnResult(n, null); + this.total += nread; + return { nread, eof: false }; } close(): void {} @@ -212,7 +239,7 @@ export class MultipartReader { readonly dashBoundary = encoder.encode(`--${this.boundary}`); readonly bufReader: BufReader; - constructor(private reader: Reader, private boundary: string) { + constructor(reader: Reader, private boundary: string) { this.bufReader = new BufReader(reader); } @@ -228,7 +255,7 @@ export class MultipartReader { const buf = new Buffer(new Uint8Array(maxValueBytes)); for (;;) { const p = await this.nextPart(); - if (!p) { + if (p === EOF) { break; } if (p.formName === "") { @@ -251,7 +278,7 @@ export class MultipartReader { const n = await copy(buf, p); if (n > maxMemory) { // too big, write to disk and flush buffer - const ext = path.extname(p.fileName); + const ext = extname(p.fileName); const { file, filepath } = await tempFile(".", { prefix: "multipart-", postfix: ext @@ -277,7 +304,7 @@ export class MultipartReader { filename: p.fileName, type: p.headers.get("content-type"), content: buf.bytes(), - size: buf.bytes().byteLength + size: buf.length }; maxMemory -= n; maxValueBytes -= n; @@ -290,35 +317,32 @@ export class MultipartReader { private currentPart: PartReader; private partsRead: number; - private async nextPart(): Promise { + private async nextPart(): Promise { if (this.currentPart) { this.currentPart.close(); } - if (bytes.equal(this.dashBoundary, encoder.encode("--"))) { + if (equal(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()}`); + const line = await this.bufReader.readSlice("\n".charCodeAt(0)); + if (line === EOF) { + throw new UnexpectedEOFError(); } if (this.isBoundaryDelimiterLine(line)) { this.partsRead++; const r = new TextProtoReader(this.bufReader); - const [headers, state] = await r.readMIMEHeader(); - if (state) { - throw state; + const headers = await r.readMIMEHeader(); + if (headers === EOF) { + throw new UnexpectedEOFError(); } const np = new PartReader(this, headers); this.currentPart = np; return np; } if (this.isFinalBoundary(line)) { - break; + return EOF; } if (expectNewPart) { throw new Error(`expecting a new Part; got line ${line}`); @@ -326,28 +350,28 @@ export class MultipartReader { if (this.partsRead === 0) { continue; } - if (bytes.equal(line, this.newLine)) { + if (equal(line, this.newLine)) { expectNewPart = true; continue; } - throw new Error(`unexpected line in next(): ${line}`); + throw new Error(`unexpected line in nextPart(): ${line}`); } } private isFinalBoundary(line: Uint8Array): boolean { - if (!bytes.hasPrefix(line, this.dashBoundaryDash)) { + if (!hasPrefix(line, this.dashBoundaryDash)) { return false; } let rest = line.slice(this.dashBoundaryDash.length, line.length); - return rest.length === 0 || bytes.equal(skipLWSPChar(rest), this.newLine); + return rest.length === 0 || equal(skipLWSPChar(rest), this.newLine); } private isBoundaryDelimiterLine(line: Uint8Array): boolean { - if (!bytes.hasPrefix(line, this.dashBoundary)) { + if (!hasPrefix(line, this.dashBoundary)) { return false; } const rest = line.slice(this.dashBoundary.length); - return bytes.equal(skipLWSPChar(rest), this.newLine); + return equal(skipLWSPChar(rest), this.newLine); } } @@ -478,7 +502,7 @@ export class MultipartWriter { await copy(f, file); } - private flush(): Promise { + private flush(): Promise { return this.bufWriter.flush(); } diff --git a/mime/multipart_test.ts b/mime/multipart_test.ts index d7583cf23077..ed033ad9accd 100644 --- a/mime/multipart_test.ts +++ b/mime/multipart_test.ts @@ -7,7 +7,7 @@ import { assertThrows, assertThrowsAsync } from "../testing/asserts.ts"; -import { test } from "../testing/mod.ts"; +import { test, runIfMain } from "../testing/mod.ts"; import { matchAfterPrefix, MultipartReader, @@ -16,6 +16,7 @@ import { } from "./multipart.ts"; import * as path from "../fs/path.ts"; import { FormFile, isFormFile } from "../multipart/formfile.ts"; +import { EOF } from "../io/bufio.ts"; import { StringWriter } from "../io/writers.ts"; const e = new TextEncoder(); @@ -25,71 +26,67 @@ const nlDashBoundary = e.encode("\r\n--" + boundary); test(function multipartScanUntilBoundary1(): void { const data = `--${boundary}`; - const [n, err] = scanUntilBoundary( + const n = scanUntilBoundary( e.encode(data), dashBoundary, nlDashBoundary, 0, - "EOF" + true ); - assertEquals(n, 0); - assertEquals(err, "EOF"); + assertEquals(n, EOF); }); test(function multipartScanUntilBoundary2(): void { const data = `foo\r\n--${boundary}`; - const [n, err] = scanUntilBoundary( + const n = scanUntilBoundary( e.encode(data), dashBoundary, nlDashBoundary, 0, - "EOF" + true ); assertEquals(n, 3); - assertEquals(err, "EOF"); }); -test(function multipartScanUntilBoundary4(): void { - const data = `foo\r\n--`; - const [n, err] = scanUntilBoundary( +test(function multipartScanUntilBoundary3(): void { + const data = `foobar`; + const n = scanUntilBoundary( e.encode(data), dashBoundary, nlDashBoundary, 0, - null + false ); - assertEquals(n, 3); - assertEquals(err, null); + assertEquals(n, data.length); }); -test(function multipartScanUntilBoundary3(): void { - const data = `foobar`; - const [n, err] = scanUntilBoundary( +test(function multipartScanUntilBoundary4(): void { + const data = `foo\r\n--`; + const n = scanUntilBoundary( e.encode(data), dashBoundary, nlDashBoundary, 0, - null + false ); - assertEquals(n, data.length); - assertEquals(err, null); + assertEquals(n, 3); }); test(function multipartMatchAfterPrefix1(): void { const data = `${boundary}\r`; - const v = matchAfterPrefix(e.encode(data), e.encode(boundary), null); + const v = matchAfterPrefix(e.encode(data), e.encode(boundary), false); assertEquals(v, 1); }); test(function multipartMatchAfterPrefix2(): void { const data = `${boundary}hoge`; - const v = matchAfterPrefix(e.encode(data), e.encode(boundary), null); + const v = matchAfterPrefix(e.encode(data), e.encode(boundary), false); assertEquals(v, -1); }); test(function multipartMatchAfterPrefix3(): void { const data = `${boundary}`; - const v = matchAfterPrefix(e.encode(data), e.encode(boundary), null); + const v = matchAfterPrefix(e.encode(data), e.encode(boundary), false); assertEquals(v, 0); }); @@ -211,3 +208,5 @@ test(async function multipartMultipartReader2(): Promise { await remove(file.tempfile); } }); + +runIfMain(import.meta); diff --git a/textproto/mod.ts b/textproto/mod.ts index 72ecd252f2dc..66f303905e49 100644 --- a/textproto/mod.ts +++ b/textproto/mod.ts @@ -3,7 +3,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -import { BufReader, BufState } from "../io/bufio.ts"; +import { BufReader, EOF, UnexpectedEOFError } from "../io/bufio.ts"; import { charCode } from "../io/util.ts"; const asciiDecoder = new TextDecoder(); @@ -39,9 +39,10 @@ export class TextProtoReader { /** readLine() reads a single line from the TextProtoReader, * eliding the final \n or \r\n from the returned string. */ - async readLine(): Promise<[string, BufState]> { - let [line, err] = await this.readLineSlice(); - return [str(line), err]; + async readLine(): Promise { + const s = await this.readLineSlice(); + if (s === EOF) return EOF; + return str(s); } /** ReadMIMEHeader reads a MIME-style header from r. @@ -64,29 +65,31 @@ export class TextProtoReader { * "Long-Key": {"Even Longer Value"}, * } */ - async readMIMEHeader(): Promise<[Headers, BufState]> { + async readMIMEHeader(): Promise { let m = new Headers(); let line: Uint8Array; // The first line cannot start with a leading space. - let [buf, err] = await this.r.peek(1); - if (buf[0] == charCode(" ") || buf[0] == charCode("\t")) { - [line, err] = await this.readLineSlice(); + let buf = await this.r.peek(1); + if (buf === EOF) { + return EOF; + } else if (buf[0] == charCode(" ") || buf[0] == charCode("\t")) { + line = (await this.readLineSlice()) as Uint8Array; } - [buf, err] = await this.r.peek(1); - if (err == null && (buf[0] == charCode(" ") || buf[0] == charCode("\t"))) { + buf = await this.r.peek(1); + if (buf === EOF) { + throw new UnexpectedEOFError(); + } else if (buf[0] == charCode(" ") || buf[0] == charCode("\t")) { throw new ProtocolError( `malformed MIME header initial line: ${str(line)}` ); } while (true) { - let [kv, err] = await this.readLineSlice(); // readContinuedLineSlice - - if (kv.byteLength === 0) { - return [m, err]; - } + let kv = await this.readLineSlice(); // readContinuedLineSlice + if (kv === EOF) throw new UnexpectedEOFError(); + if (kv.byteLength === 0) return m; // Key ends at first colon; should not have trailing spaces // but they appear in the wild, violating specs, so we remove @@ -125,29 +128,26 @@ export class TextProtoReader { try { m.append(key, value); } catch {} - - if (err != null) { - throw err; - } } } - async readLineSlice(): Promise<[Uint8Array, BufState]> { + async readLineSlice(): Promise { // this.closeDot(); let line: Uint8Array; while (true) { - let [l, more, err] = await this.r.readLine(); - if (err != null) { - // Go's len(typed nil) works fine, but not in JS - return [new Uint8Array(0), err]; - } + const r = await this.r.readLine(); + if (r === EOF) return EOF; + const { line: l, more } = r; // Avoid the copy if the first call produced a full line. - if (line == null && !more) { + if (!line && !more) { + // TODO(ry): + // This skipSpace() is definitely misplaced, but I don't know where it + // comes from nor how to fix it. if (this.skipSpace(l) === 0) { - return [new Uint8Array(0), null]; + return new Uint8Array(0); } - return [l, null]; + return l; } line = append(line, l); @@ -155,7 +155,7 @@ export class TextProtoReader { break; } } - return [line, null]; + return line; } skipSpace(l: Uint8Array): number { diff --git a/textproto/reader_test.ts b/textproto/reader_test.ts index 2d054cabae3f..bd0d39fd339e 100644 --- a/textproto/reader_test.ts +++ b/textproto/reader_test.ts @@ -3,11 +3,21 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -import { BufReader } from "../io/bufio.ts"; +import { BufReader, EOF } from "../io/bufio.ts"; import { TextProtoReader, ProtocolError } from "./mod.ts"; import { stringsReader } from "../io/util.ts"; -import { assert, assertEquals, assertThrows } from "../testing/asserts.ts"; -import { test } from "../testing/mod.ts"; +import { + assert, + assertEquals, + assertNotEquals, + assertThrows +} from "../testing/asserts.ts"; +import { test, runIfMain } from "../testing/mod.ts"; + +function assertNotEOF(val: T | EOF): T { + assertNotEquals(val, EOF); + return val as T; +} function reader(s: string): TextProtoReader { return new TextProtoReader(new BufReader(stringsReader(s))); @@ -21,25 +31,21 @@ function reader(s: string): TextProtoReader { // }); test(async function textprotoReadEmpty(): Promise { - let r = reader(""); - let [, err] = await r.readMIMEHeader(); - // Should not crash! - assertEquals(err, "EOF"); + const r = reader(""); + const m = await r.readMIMEHeader(); + assertEquals(m, EOF); }); test(async function textprotoReader(): Promise { - let r = reader("line1\nline2\n"); - let [s, err] = await r.readLine(); + const r = reader("line1\nline2\n"); + let s = await r.readLine(); assertEquals(s, "line1"); - assert(err == null); - [s, err] = await r.readLine(); + s = await r.readLine(); assertEquals(s, "line2"); - assert(err == null); - [s, err] = await r.readLine(); - assertEquals(s, ""); - assert(err == "EOF"); + s = await r.readLine(); + assert(s === EOF); }); test({ @@ -48,10 +54,9 @@ test({ const input = "my-key: Value 1 \r\nLong-key: Even Longer Value\r\nmy-Key: Value 2\r\n\n"; const r = reader(input); - const [m, err] = await r.readMIMEHeader(); + const m = assertNotEOF(await r.readMIMEHeader()); assertEquals(m.get("My-Key"), "Value 1, Value 2"); assertEquals(m.get("Long-key"), "Even Longer Value"); - assert(!err); } }); @@ -60,9 +65,8 @@ test({ async fn(): Promise { const input = "Foo: bar\n\n"; const r = reader(input); - let [m, err] = await r.readMIMEHeader(); + const m = assertNotEOF(await r.readMIMEHeader()); assertEquals(m.get("Foo"), "bar"); - assert(!err); } }); @@ -71,9 +75,8 @@ test({ async fn(): Promise { const input = ": bar\ntest-1: 1\n\n"; const r = reader(input); - let [m, err] = await r.readMIMEHeader(); + const m = assertNotEOF(await r.readMIMEHeader()); assertEquals(m.get("Test-1"), "1"); - assert(!err); } }); @@ -86,11 +89,9 @@ test({ data.push("x"); } const sdata = data.join(""); - const r = reader(`Cookie: ${sdata}\r\n`); - let [m] = await r.readMIMEHeader(); + const r = reader(`Cookie: ${sdata}\r\n\r\n`); + const m = assertNotEOF(await r.readMIMEHeader()); assertEquals(m.get("Cookie"), sdata); - // TODO re-enable, here err === "EOF" is has to be null - // assert(!err); } }); @@ -106,12 +107,11 @@ test({ "Audio Mode : None\r\n" + "Privilege : 127\r\n\r\n"; const r = reader(input); - let [m, err] = await r.readMIMEHeader(); + const m = assertNotEOF(await r.readMIMEHeader()); assertEquals(m.get("Foo"), "bar"); assertEquals(m.get("Content-Language"), "en"); assertEquals(m.get("SID"), "0"); assertEquals(m.get("Privilege"), "127"); - assert(!err); // Not a legal http header assertThrows( (): void => { @@ -176,9 +176,10 @@ test({ "------WebKitFormBoundaryimeZ2Le9LjohiUiG--\r\n\n" ]; const r = reader(input.join("")); - let [m, err] = await r.readMIMEHeader(); + const m = assertNotEOF(await r.readMIMEHeader()); assertEquals(m.get("Accept"), "*/*"); assertEquals(m.get("Content-Disposition"), 'form-data; name="test"'); - assert(!err); } }); + +runIfMain(import.meta); diff --git a/ws/mod.ts b/ws/mod.ts index ced566d45cf5..7d8200dfcd95 100644 --- a/ws/mod.ts +++ b/ws/mod.ts @@ -4,7 +4,7 @@ import { decode, encode } from "../strings/mod.ts"; type Conn = Deno.Conn; type Writer = Deno.Writer; -import { BufReader, BufWriter } from "../io/bufio.ts"; +import { BufReader, BufWriter, EOF, UnexpectedEOFError } from "../io/bufio.ts"; import { readLong, readShort, sliceLongToBytes } from "../io/ioutil.ts"; import { Sha1 } from "./sha1.ts"; import { writeResponse } from "../http/server.ts"; @@ -130,8 +130,7 @@ export async function writeFrame( header = append(header, frame.payload); const w = BufWriter.create(writer); await w.write(header); - const err = await w.flush(); - if (err) throw err; + await w.flush(); } /** Read websocket frame from given BufReader */ @@ -403,79 +402,86 @@ export function createSecKey(): string { return btoa(key); } -/** Connect to given websocket endpoint url. Endpoint must be acceptable for URL */ -export async function connectWebSocket( - endpoint: string, - headers: Headers = new Headers() -): Promise { - const url = new URL(endpoint); +async function handshake( + url: URL, + headers: Headers, + bufReader: BufReader, + bufWriter: BufWriter +): Promise { const { hostname, pathname, searchParams } = url; - let port = url.port; - if (!url.port) { - if (url.protocol === "http" || url.protocol === "ws") { - port = "80"; - } else if (url.protocol === "https" || url.protocol === "wss") { - throw new Error("currently https/wss is not supported"); - } - } - const conn = await Deno.dial("tcp", `${hostname}:${port}`); - const abortHandshake = (err: Error): void => { - conn.close(); - throw err; - }; - const bufWriter = new BufWriter(conn); - const bufReader = new BufReader(conn); - await bufWriter.write( - encode(`GET ${pathname}?${searchParams || ""} HTTP/1.1\r\n`) - ); const key = createSecKey(); + if (!headers.has("host")) { headers.set("host", hostname); } headers.set("upgrade", "websocket"); headers.set("connection", "upgrade"); headers.set("sec-websocket-key", key); - let headerStr = ""; + + let headerStr = `GET ${pathname}?${searchParams || ""} HTTP/1.1\r\n`; for (const [key, value] of headers) { headerStr += `${key}: ${value}\r\n`; } headerStr += "\r\n"; + await bufWriter.write(encode(headerStr)); - let err, statusLine, responseHeaders; - err = await bufWriter.flush(); - if (err) { - throw new Error("ws: failed to send handshake: " + err); - } + await bufWriter.flush(); + const tpReader = new TextProtoReader(bufReader); - [statusLine, err] = await tpReader.readLine(); - if (err) { - abortHandshake(new Error("ws: failed to read status line: " + err)); + const statusLine = await tpReader.readLine(); + if (statusLine === EOF) { + throw new UnexpectedEOFError(); } - const m = statusLine.match(/^(.+?) (.+?) (.+?)$/); + const m = statusLine.match(/^(?\S+) (?\S+) /); if (!m) { - abortHandshake(new Error("ws: invalid status line: " + statusLine)); + throw new Error("ws: invalid status line: " + statusLine); } - const [_, version, statusCode] = m; + const { version, statusCode } = m.groups; if (version !== "HTTP/1.1" || statusCode !== "101") { - abortHandshake( - new Error( - `ws: server didn't accept handshake: version=${version}, statusCode=${statusCode}` - ) + throw new Error( + `ws: server didn't accept handshake: ` + + `version=${version}, statusCode=${statusCode}` ); } - [responseHeaders, err] = await tpReader.readMIMEHeader(); - if (err) { - abortHandshake(new Error("ws: failed to parse response headers: " + err)); + + const responseHeaders = await tpReader.readMIMEHeader(); + if (responseHeaders === EOF) { + throw new UnexpectedEOFError(); } + const expectedSecAccept = createSecAccept(key); const secAccept = responseHeaders.get("sec-websocket-accept"); if (secAccept !== expectedSecAccept) { - abortHandshake( - new Error( - `ws: unexpected sec-websocket-accept header: expected=${expectedSecAccept}, actual=${secAccept}` - ) + throw new Error( + `ws: unexpected sec-websocket-accept header: ` + + `expected=${expectedSecAccept}, actual=${secAccept}` ); } +} + +/** Connect to given websocket endpoint url. Endpoint must be acceptable for URL */ +export async function connectWebSocket( + endpoint: string, + headers: Headers = new Headers() +): Promise { + const url = new URL(endpoint); + let { hostname, port } = url; + if (!port) { + if (url.protocol === "http" || url.protocol === "ws") { + port = "80"; + } else if (url.protocol === "https" || url.protocol === "wss") { + throw new Error("currently https/wss is not supported"); + } + } + const conn = await Deno.dial("tcp", `${hostname}:${port}`); + const bufWriter = new BufWriter(conn); + const bufReader = new BufReader(conn); + try { + await handshake(url, headers, bufReader, bufWriter); + } catch (err) { + conn.close(); + throw err; + } return new WebSocketImpl(conn, { bufWriter, bufReader diff --git a/ws/test.ts b/ws/test.ts index 93936988a395..bac82453dcb6 100644 --- a/ws/test.ts +++ b/ws/test.ts @@ -107,8 +107,9 @@ test(async function wsReadUnmaskedPingPongFrame(): Promise { }); test(async function wsReadUnmaskedBigBinaryFrame(): Promise { + const payloadLength = 0x100; const a = [0x82, 0x7e, 0x01, 0x00]; - for (let i = 0; i < 256; i++) { + for (let i = 0; i < payloadLength; i++) { a.push(i); } const buf = new BufReader(new Buffer(new Uint8Array(a))); @@ -116,12 +117,13 @@ test(async function wsReadUnmaskedBigBinaryFrame(): Promise { assertEquals(bin.opcode, OpCode.BinaryFrame); assertEquals(bin.isLastFrame, true); assertEquals(bin.mask, undefined); - assertEquals(bin.payload.length, 256); + assertEquals(bin.payload.length, payloadLength); }); test(async function wsReadUnmaskedBigBigBinaryFrame(): Promise { + const payloadLength = 0x10000; const a = [0x82, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00]; - for (let i = 0; i < 0xffff; i++) { + for (let i = 0; i < payloadLength; i++) { a.push(i); } const buf = new BufReader(new Buffer(new Uint8Array(a))); @@ -129,7 +131,7 @@ test(async function wsReadUnmaskedBigBigBinaryFrame(): Promise { assertEquals(bin.opcode, OpCode.BinaryFrame); assertEquals(bin.isLastFrame, true); assertEquals(bin.mask, undefined); - assertEquals(bin.payload.length, 0xffff + 1); + assertEquals(bin.payload.length, payloadLength); }); test(async function wsCreateSecAccept(): Promise {