From 968d50842512a007dc9c2e4ae31b1970fdc6e6a2 Mon Sep 17 00:00:00 2001 From: Ryan Dahl Date: Tue, 18 Dec 2018 18:25:49 -0500 Subject: Rename project to deno_std Move typescript files to net/ Original: https://github.com/denoland/deno_std/commit/99e276eb89fbe0003bfa8d9e7b907ff3ef19ee47 --- README.md | 4 +- bufio.ts | 465 ------------------------------------------------ bufio_test.ts | 345 ----------------------------------- file_server.ts | 214 ---------------------- file_server_test.ts | 46 ----- http.ts | 212 ---------------------- http_bench.ts | 15 -- http_status.ts | 134 -------------- http_test.ts | 58 ------ iotest.ts | 61 ------- net/bufio.ts | 465 ++++++++++++++++++++++++++++++++++++++++++++++++ net/bufio_test.ts | 345 +++++++++++++++++++++++++++++++++++ net/file_server.ts | 214 ++++++++++++++++++++++ net/file_server_test.ts | 46 +++++ net/http.ts | 212 ++++++++++++++++++++++ net/http_bench.ts | 15 ++ net/http_status.ts | 134 ++++++++++++++ net/http_test.ts | 58 ++++++ net/iotest.ts | 61 +++++++ net/textproto.ts | 149 ++++++++++++++++ net/textproto_test.ts | 93 ++++++++++ net/util.ts | 29 +++ test.ts | 13 +- textproto.ts | 149 ---------------- textproto_test.ts | 93 ---------- util.ts | 29 --- 26 files changed, 1829 insertions(+), 1830 deletions(-) delete mode 100644 bufio.ts delete mode 100644 bufio_test.ts delete mode 100755 file_server.ts delete mode 100644 file_server_test.ts delete mode 100644 http.ts delete mode 100644 http_bench.ts delete mode 100644 http_status.ts delete mode 100644 http_test.ts delete mode 100644 iotest.ts create mode 100644 net/bufio.ts create mode 100644 net/bufio_test.ts create mode 100755 net/file_server.ts create mode 100644 net/file_server_test.ts create mode 100644 net/http.ts create mode 100644 net/http_bench.ts create mode 100644 net/http_status.ts create mode 100644 net/http_test.ts create mode 100644 net/iotest.ts create mode 100644 net/textproto.ts create mode 100644 net/textproto_test.ts create mode 100644 net/util.ts mode change 100644 => 100755 test.ts delete mode 100644 textproto.ts delete mode 100644 textproto_test.ts delete mode 100644 util.ts diff --git a/README.md b/README.md index 9252dbd3d..ab668e913 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Deno Networking Libraries +# Deno Standard Modules -[![Build Status](https://travis-ci.com/denoland/deno_net.svg?branch=master)](https://travis-ci.com/denoland/deno_net) +[![Build Status](https://travis-ci.com/denoland/deno_std.svg?branch=master)](https://travis-ci.com/denoland/deno_std) Usage: diff --git a/bufio.ts b/bufio.ts deleted file mode 100644 index b412cbce8..000000000 --- a/bufio.ts +++ /dev/null @@ -1,465 +0,0 @@ -// Based on https://github.com/golang/go/blob/891682/src/bufio/bufio.go -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -import { Reader, ReadResult, Writer } from "deno"; -import { assert, charCode, copyBytes } from "./util.ts"; - -const DEFAULT_BUF_SIZE = 4096; -const MIN_BUF_SIZE = 16; -const MAX_CONSECUTIVE_EMPTY_READS = 100; -const CR = charCode("\r"); -const LF = charCode("\n"); - -export type BufState = - | null - | "EOF" - | "BufferFull" - | "ShortWrite" - | "NoProgress" - | Error; - -/** BufReader implements buffering for a Reader object. */ -export class BufReader implements Reader { - private buf: Uint8Array; - 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; - - constructor(rd: Reader, size = DEFAULT_BUF_SIZE) { - if (size < MIN_BUF_SIZE) { - size = MIN_BUF_SIZE; - } - this._reset(new Uint8Array(size), rd); - } - - /** Returns the size of the underlying buffer in bytes. */ - size(): number { - return this.buf.byteLength; - } - - buffered(): number { - 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. - if (this.r > 0) { - this.buf.copyWithin(0, this.r, this.w); - this.w -= this.r; - this.r = 0; - } - - if (this.w >= this.buf.byteLength) { - throw Error("bufio: tried to fill full buffer"); - } - - // 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; - } - assert(rr.nread >= 0, "negative read"); - this.w += rr.nread; - if (rr.eof) { - this.err = "EOF"; - return; - } - if (rr.nread > 0) { - return; - } - } - this.err = "NoProgress"; - } - - /** Discards any buffered data, resets all state, and switches - * the buffered reader to read from r. - */ - reset(r: Reader): void { - this._reset(this.buf, r); - } - - private _reset(buf: Uint8Array, rd: Reader): void { - this.buf = buf; - this.rd = rd; - this.lastByte = -1; - // this.lastRuneSize = -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 (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); - 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(); - } - 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; - } - assert(rr.nread >= 0, "negative read"); - if (rr.nread === 0) { - if (this.err) { - throw this._readErr(); - } - 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); - this.r += rr.nread; - this.lastByte = this.buf[this.r - 1]; - // this.lastRuneSize = -1; - return rr; - } - - /** reads exactly len(p) bytes into p. - * 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; - } - return [nread, nread < p.length ? "EOF" : null]; - } - - - /** Returns the next byte [0, 255] or -1 if EOF. */ - async readByte(): Promise { - while (this.r === this.w) { - 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; - return c; - } - - /** readString() reads until the first occurrence of delim in the input, - * returning a string containing the data up to and including the delimiter. - * If ReadString encounters an error before finding a delimiter, - * it returns the data read before the error and the error itself (often io.EOF). - * ReadString returns err != nil if and only if the returned data does not end in - * delim. - * For simple uses, a Scanner may be more convenient. - */ - 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 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 - * 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 - * 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. - * - * 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. - */ - async readLine(): Promise<[Uint8Array, boolean, BufState]> { - let [line, err] = await this.readSlice(LF); - - if (err === "BufferFull") { - // Handle the case where "\r\n" straddles the buffer. - if (line.byteLength > 0 && line[line.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); - } - return [line, true, null]; - } - - if (line.byteLength === 0) { - return [line, false, err]; - } - err = null; - - if (line[line.byteLength - 1] == LF) { - let drop = 1; - if (line.byteLength > 1 && line[line.byteLength - 2] === CR) { - drop = 2; - } - line = line.subarray(0, line.byteLength - drop); - } - return [line, false, err]; - } - - /** 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. - */ - async readSlice(delim: number): Promise<[Uint8Array, BufState]> { - let s = 0; // search start index - let line: Uint8Array; - let err: BufState; - 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); - this.r += i + 1; - break; - } - - // Pending error? - if (this.err) { - line = 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; - } - - s = this.w - this.r; // do not rescan area we scanned before - - await this._fill(); // buffer is not full - } - - // Handle last byte, if any. - let i = line.byteLength - 1; - if (i >= 0) { - this.lastByte = line[i]; - // this.lastRuneSize = -1 - } - - return [line, err]; - } - - /** 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. - */ - async peek(n: number): Promise<[Uint8Array, BufState]> { - 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 - } - - if (n > this.buf.byteLength) { - return [this.buf.subarray(this.r, this.w), "BufferFull"]; - } - - // 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]; - } -} - -/** BufWriter implements buffering for an deno.Writer object. - * If an error occurs writing to a Writer, no more data will be - * accepted and all subsequent writes, and flush(), will return the error. - * After all data has been written, the client should call the - * flush() method to guarantee all data has been forwarded to - * the underlying deno.Writer. - */ -export class BufWriter implements Writer { - buf: Uint8Array; - n: number = 0; - err: null | BufState = null; - - constructor(private wr: Writer, size = DEFAULT_BUF_SIZE) { - if (size <= 0) { - size = DEFAULT_BUF_SIZE; - } - this.buf = new Uint8Array(size); - } - - /** Size returns the size of the underlying buffer in bytes. */ - size(): number { - return this.buf.byteLength; - } - - /** Discards any unflushed buffered data, clears any error, and - * resets b to write its output to w. - */ - reset(w: Writer): void { - this.err = null; - this.n = 0; - this.wr = w; - } - - /** 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; - } - - let n: number; - let err: BufState = null; - try { - n = await this.wr.write(this.buf.subarray(0, this.n)); - } catch (e) { - err = e; - } - - if (n < this.n && err == null) { - err = "ShortWrite"; - } - - if (err != null) { - if (n > 0 && n < this.n) { - this.buf.copyWithin(0, n, this.n); - } - this.n -= n; - this.err = err; - return err; - } - this.n = 0; - } - - /** Returns how many bytes are unused in the buffer. */ - available(): number { - return this.buf.byteLength - this.n; - } - - /** buffered returns the number of bytes that have been written into the - * current buffer. - */ - buffered(): number { - return this.n; - } - - /** Writes the contents of p into the buffer. - * Returns the number of bytes written. - */ - async write(p: Uint8Array): Promise { - let nn = 0; - let n: number; - while (p.byteLength > this.available() && !this.err) { - 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; - } - } else { - n = copyBytes(this.buf, p, this.n); - this.n += n; - await this.flush(); - } - nn += n; - p = p.subarray(n); - } - if (this.err) { - throw this.err; - } - n = copyBytes(this.buf, p, this.n); - this.n += n; - nn += n; - return nn; - } -} diff --git a/bufio_test.ts b/bufio_test.ts deleted file mode 100644 index 19954bdf6..000000000 --- a/bufio_test.ts +++ /dev/null @@ -1,345 +0,0 @@ -// Based on https://github.com/golang/go/blob/891682/src/bufio/bufio_test.go -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -import { Buffer, Reader, ReadResult } from "deno"; -import { - test, - assert, - assertEqual -} from "https://deno.land/x/testing/testing.ts"; -import { BufReader, BufState, BufWriter } from "./bufio.ts"; -import * as iotest from "./iotest.ts"; -import { charCode, copyBytes, stringsReader } from "./util.ts"; - -const encoder = new TextEncoder(); - -async function readBytes(buf: BufReader): Promise { - const b = new Uint8Array(1000); - let nb = 0; - while (true) { - let c = await buf.readByte(); - if (c < 0) { - break; // EOF - } - b[nb] = c; - nb++; - } - const decoder = new TextDecoder(); - return decoder.decode(b.subarray(0, nb)); -} - -test(async function bufioReaderSimple() { - const data = "hello world"; - const b = new BufReader(stringsReader(data)); - const s = await readBytes(b); - assertEqual(s, data); -}); - -type ReadMaker = { name: string; fn: (r: Reader) => Reader }; - -const readMakers: ReadMaker[] = [ - { name: "full", fn: r => r }, - { name: "byte", fn: r => new iotest.OneByteReader(r) }, - { name: "half", fn: r => new iotest.HalfReader(r) } - // TODO { name: "data+err", r => new iotest.DataErrReader(r) }, - // { name: "timeout", fn: r => new iotest.TimeoutReader(r) }, -]; - -function readLines(b: BufReader): string { - let s = ""; - while (true) { - let s1 = b.readString("\n"); - if (s1 == null) { - break; // EOF - } - s += s1; - } - return s; -} - -// Call read to accumulate the text of a file -async function reads(buf: BufReader, m: number): Promise { - const b = new Uint8Array(1000); - let nb = 0; - while (true) { - const { nread, eof } = await buf.read(b.subarray(nb, nb + m)); - nb += nread; - if (eof) { - break; - } - } - const decoder = new TextDecoder(); - return decoder.decode(b.subarray(0, nb)); -} - -type NamedBufReader = { name: string; fn: (r: BufReader) => Promise }; - -const bufreaders: NamedBufReader[] = [ - { name: "1", fn: (b: BufReader) => reads(b, 1) }, - { name: "2", fn: (b: BufReader) => reads(b, 2) }, - { name: "3", fn: (b: BufReader) => reads(b, 3) }, - { name: "4", fn: (b: BufReader) => reads(b, 4) }, - { name: "5", fn: (b: BufReader) => reads(b, 5) }, - { name: "7", fn: (b: BufReader) => reads(b, 7) }, - { name: "bytes", fn: readBytes } - // { name: "lines", fn: readLines }, -]; - -const MIN_READ_BUFFER_SIZE = 16; -const bufsizes: number[] = [ - 0, - MIN_READ_BUFFER_SIZE, - 23, - 32, - 46, - 64, - 93, - 128, - 1024, - 4096 -]; - -test(async function bufioBufReader() { - const texts = new Array(31); - let str = ""; - let all = ""; - for (let i = 0; i < texts.length - 1; i++) { - texts[i] = str + "\n"; - all += texts[i]; - str += String.fromCharCode(i % 26 + 97); - } - texts[texts.length - 1] = all; - - for (let text of texts) { - for (let readmaker of readMakers) { - for (let bufreader of bufreaders) { - for (let bufsize of bufsizes) { - const read = readmaker.fn(stringsReader(text)); - const buf = new BufReader(read, bufsize); - const s = await bufreader.fn(buf); - const debugStr = - `reader=${readmaker.name} ` + - `fn=${bufreader.name} bufsize=${bufsize} want=${text} got=${s}`; - assertEqual(s, text, debugStr); - } - } - } - } -}); - -test(async function bufioBufferFull() { - 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); - assertEqual(err, "BufferFull"); - assertEqual(actual, "And now, hello, "); - - [line, err] = await buf.readSlice(charCode("!")); - actual = decoder.decode(line); - assertEqual(actual, "world!"); - assert(err == null); -}); - -const testInput = encoder.encode( - "012\n345\n678\n9ab\ncde\nfgh\nijk\nlmn\nopq\nrst\nuvw\nxy" -); -const testInputrn = encoder.encode( - "012\r\n345\r\n678\r\n9ab\r\ncde\r\nfgh\r\nijk\r\nlmn\r\nopq\r\nrst\r\nuvw\r\nxy\r\n\n\r\n" -); -const testOutput = encoder.encode("0123456789abcdefghijklmnopqrstuvwxy"); - -// TestReader wraps a Uint8Array and returns reads of a specific length. -class TestReader implements Reader { - constructor(private data: Uint8Array, private stride: number) {} - - async read(buf: Uint8Array): Promise { - let nread = this.stride; - if (nread > this.data.byteLength) { - nread = this.data.byteLength; - } - if (nread > buf.byteLength) { - nread = buf.byteLength; - } - copyBytes(buf as Uint8Array, this.data); - this.data = this.data.subarray(nread); - let eof = false; - if (this.data.byteLength == 0) { - eof = true; - } - return { nread, eof }; - } -} - -async function testReadLine(input: Uint8Array): Promise { - for (let stride = 1; stride < 2; stride++) { - let done = 0; - 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"); - } - assertEqual(isPrefix, false); - if (err == "EOF") { - break; - } - let want = testOutput.subarray(done, done + line.byteLength); - assertEqual( - line, - want, - `Bad line at stride ${stride}: want: ${want} got: ${line}` - ); - done += line.byteLength; - } - assertEqual( - done, - testOutput.byteLength, - `readLine didn't return everything: got: ${done}, ` + - `want: ${testOutput} (stride: ${stride})` - ); - } -} - -test(async function bufioReadLine() { - await testReadLine(testInput); - await testReadLine(testInputrn); -}); - -test(async function bufioPeek() { - const decoder = new TextDecoder(); - let p = new Uint8Array(10); - // string is 16 (minReadBufferSize) long. - let buf = new BufReader( - stringsReader("abcdefghijklmnop"), - MIN_READ_BUFFER_SIZE - ); - - let [actual, err] = await buf.peek(1); - assertEqual(decoder.decode(actual), "a"); - assert(err == null); - - [actual, err] = await buf.peek(4); - assertEqual(decoder.decode(actual), "abcd"); - assert(err == null); - - [actual, err] = await buf.peek(32); - assertEqual(decoder.decode(actual), "abcdefghijklmnop"); - assertEqual(err, "BufferFull"); - - await buf.read(p.subarray(0, 3)); - assertEqual(decoder.decode(p.subarray(0, 3)), "abc"); - - [actual, err] = await buf.peek(1); - assertEqual(decoder.decode(actual), "d"); - assert(err == null); - - [actual, err] = await buf.peek(1); - assertEqual(decoder.decode(actual), "d"); - assert(err == null); - - [actual, err] = await buf.peek(1); - assertEqual(decoder.decode(actual), "d"); - assert(err == null); - - [actual, err] = await buf.peek(2); - assertEqual(decoder.decode(actual), "de"); - assert(err == null); - - let { eof } = await buf.read(p.subarray(0, 3)); - assertEqual(decoder.decode(p.subarray(0, 3)), "def"); - assert(!eof); - assert(err == null); - - [actual, err] = await buf.peek(4); - assertEqual(decoder.decode(actual), "ghij"); - assert(err == null); - - await buf.read(p); - assertEqual(decoder.decode(p), "ghijklmnop"); - - [actual, err] = await buf.peek(0); - assertEqual(decoder.decode(actual), ""); - assert(err == null); - - [actual, err] = await buf.peek(1); - assertEqual(decoder.decode(actual), ""); - assert(err == "EOF"); - /* TODO - // Test for issue 3022, not exposing a reader's error on a successful Peek. - buf = NewReaderSize(dataAndEOFReader("abcd"), 32) - if s, err := buf.Peek(2); string(s) != "ab" || err != nil { - t.Errorf(`Peek(2) on "abcd", EOF = %q, %v; want "ab", nil`, string(s), err) - } - if s, err := buf.Peek(4); string(s) != "abcd" || err != nil { - t.Errorf(`Peek(4) on "abcd", EOF = %q, %v; want "abcd", nil`, string(s), err) - } - if n, err := buf.Read(p[0:5]); string(p[0:n]) != "abcd" || err != nil { - t.Fatalf("Read after peek = %q, %v; want abcd, EOF", p[0:n], err) - } - if n, err := buf.Read(p[0:1]); string(p[0:n]) != "" || err != io.EOF { - t.Fatalf(`second Read after peek = %q, %v; want "", EOF`, p[0:n], err) - } - */ -}); - -test(async function bufioWriter() { - const data = new Uint8Array(8192); - - for (let i = 0; i < data.byteLength; i++) { - data[i] = charCode(" ") + i % (charCode("~") - charCode(" ")); - } - - const w = new Buffer(); - for (let nwrite of bufsizes) { - for (let bs of bufsizes) { - // Write nwrite bytes using buffer size bs. - // Check that the right amount makes it out - // and that the data is correct. - - w.reset(); - const buf = new BufWriter(w, bs); - - const context = `nwrite=${nwrite} bufsize=${bs}`; - const n = await buf.write(data.subarray(0, nwrite)); - assertEqual(n, nwrite, context); - - await buf.flush(); - - const written = w.bytes(); - assertEqual(written.byteLength, nwrite); - - for (let l = 0; l < written.byteLength; l++) { - assertEqual(written[l], data[l]); - } - } - } -}); - -test(async function bufReaderReadFull() { - const enc = new TextEncoder(); - const dec = new TextDecoder(); - const text = "Hello World"; - const data = new Buffer(enc.encode(text)); - const bufr = new BufReader(data, 3); - { - const buf = new Uint8Array(6); - const [nread, err] = await bufr.readFull(buf); - assertEqual(nread, 6); - assert(!err); - assertEqual(dec.decode(buf), "Hello "); - } - { - const buf = new Uint8Array(6); - const [nread, err] = await bufr.readFull(buf); - assertEqual(nread, 5); - assertEqual(err, "EOF"); - assertEqual(dec.decode(buf.subarray(0, 5)), "World"); - } -}); diff --git a/file_server.ts b/file_server.ts deleted file mode 100755 index bd1c52b88..000000000 --- a/file_server.ts +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env deno --allow-net - -// This program serves files in the current directory over HTTP. -// TODO Stream responses instead of reading them into memory. -// TODO Add tests like these: -// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js - -import { - listenAndServe, - ServerRequest, - setContentLength, - Response -} from "./http"; -import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno"; - -const dirViewerTemplate = ` - - - - - - - Deno File Server - - - -

Index of <%DIRNAME%>

- - - <%CONTENTS%> -
ModeSizeName
- - -`; - -let currentDir = cwd(); -const target = args[1]; -if (target) { - currentDir = `${currentDir}/${target}`; -} -const addr = `0.0.0.0:${args[2] || 4500}`; -const encoder = new TextEncoder(); - -function modeToString(isDir: boolean, maybeMode: number | null) { - const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"]; - - if (maybeMode === null) { - return "(unknown mode)"; - } - const mode = maybeMode!.toString(8); - if (mode.length < 3) { - return "(unknown mode)"; - } - let output = ""; - mode - .split("") - .reverse() - .slice(0, 3) - .forEach(v => { - output = modeMap[+v] + output; - }); - output = `(${isDir ? "d" : "-"}${output})`; - return output; -} - -function fileLenToString(len: number) { - const multipler = 1024; - let base = 1; - const suffix = ["B", "K", "M", "G", "T"]; - let suffixIndex = 0; - - while (base * multipler < len) { - if (suffixIndex >= suffix.length - 1) { - break; - } - base *= multipler; - suffixIndex++; - } - - return `${(len / base).toFixed(2)}${suffix[suffixIndex]}`; -} - -function createDirEntryDisplay( - name: string, - path: string, - size: number | null, - mode: number | null, - isDir: boolean -) { - const sizeStr = size === null ? "" : "" + fileLenToString(size!); - return ` - ${modeToString( - isDir, - mode - )}${sizeStr}${name}${ - isDir ? "/" : "" - } - - `; -} - -// TODO: simplify this after deno.stat and deno.readDir are fixed -async function serveDir(req: ServerRequest, dirPath: string, dirName: string) { - // dirname has no prefix - const listEntry: string[] = []; - const fileInfos = await readDir(dirPath); - for (const info of fileInfos) { - if (info.name === "index.html" && info.isFile()) { - // in case index.html as dir... - return await serveFile(req, info.path); - } - // Yuck! - let mode = null; - try { - mode = (await stat(info.path)).mode; - } catch (e) {} - listEntry.push( - createDirEntryDisplay( - info.name, - dirName + "/" + info.name, - info.isFile() ? info.len : null, - mode, - info.isDirectory() - ) - ); - } - - const page = new TextEncoder().encode( - dirViewerTemplate - .replace("<%DIRNAME%>", dirName + "/") - .replace("<%CONTENTS%>", listEntry.join("")) - ); - - const headers = new Headers(); - headers.set("content-type", "text/html"); - - const res = { - status: 200, - body: page, - headers - }; - setContentLength(res); - return res; -} - -async function serveFile(req: ServerRequest, filename: string) { - const file = await open(filename); - const fileInfo = await stat(filename); - const headers = new Headers(); - headers.set("content-length", fileInfo.len.toString()); - - const res = { - status: 200, - body: file, - headers - }; - return res; -} - -async function serveFallback(req: ServerRequest, e: Error) { - if ( - e instanceof DenoError && - (e as DenoError).kind === ErrorKind.NotFound - ) { - return { - status: 404, - body: encoder.encode("Not found") - }; - } else { - return { - status: 500, - body: encoder.encode("Internal server error") - }; - } -} - -function serverLog(req: ServerRequest, res: Response) { - const d = new Date().toISOString(); - const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`; - const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`; - console.log(s); -} - -listenAndServe(addr, async req => { - const fileName = req.url.replace(/\/$/, ""); - const filePath = currentDir + fileName; - - let response: Response; - - try { - const fileInfo = await stat(filePath); - if (fileInfo.isDirectory()) { - // Bug with deno.stat: name and path not populated - // Yuck! - response = await serveDir(req, filePath, fileName); - } else { - response = await serveFile(req, filePath); - } - } catch (e) { - response = await serveFallback(req, e); - } finally { - serverLog(req, response); - req.respond(response); - } -}); - -console.log(`HTTP server listening on http://${addr}/`); diff --git a/file_server_test.ts b/file_server_test.ts deleted file mode 100644 index a04ced7e5..000000000 --- a/file_server_test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { readFile } from "deno"; - -import { - test, - assert, - assertEqual -} from "https://deno.land/x/testing/testing.ts"; - -// Promise to completeResolve when all tests completes -let completeResolve; -export const completePromise = new Promise(res => (completeResolve = res)); -let completedTestCount = 0; - -function maybeCompleteTests() { - completedTestCount++; - // Change this when adding more tests - if (completedTestCount === 3) { - completeResolve(); - } -} - -export function runTests(serverReadyPromise: Promise) { - test(async function serveFile() { - await serverReadyPromise; - const res = await fetch("http://localhost:4500/.travis.yml"); - const downloadedFile = await res.text(); - const localFile = new TextDecoder().decode(await readFile("./.travis.yml")); - assertEqual(downloadedFile, localFile); - maybeCompleteTests(); - }); - - test(async function serveDirectory() { - await serverReadyPromise; - const res = await fetch("http://localhost:4500/"); - const page = await res.text(); - assert(page.includes(".travis.yml")); - maybeCompleteTests(); - }); - - test(async function serveFallback() { - await serverReadyPromise; - const res = await fetch("http://localhost:4500/badfile.txt"); - assertEqual(res.status, 404); - maybeCompleteTests(); - }); -} diff --git a/http.ts b/http.ts deleted file mode 100644 index bd45aea0d..000000000 --- a/http.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { listen, Conn, toAsyncIterator, Reader, copy } from "deno"; -import { BufReader, BufState, BufWriter } from "./bufio.ts"; -import { TextProtoReader } from "./textproto.ts"; -import { STATUS_TEXT } from "./http_status"; -import { assert } from "./util"; - -interface Deferred { - promise: Promise<{}>; - resolve: () => void; - reject: () => void; -} - -function deferred(): Deferred { - let resolve, reject; - const promise = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - return { - promise, - resolve, - reject - }; -} - -interface ServeEnv { - reqQueue: ServerRequest[]; - serveDeferred: Deferred; -} - -// Continuously read more requests from conn until EOF -// Mutually calling with maybeHandleReq -// TODO: make them async function after this change is done -// https://github.com/tc39/ecma262/pull/1250 -// See https://v8.dev/blog/fast-async -export function serveConn(env: ServeEnv, conn: Conn) { - readRequest(conn).then(maybeHandleReq.bind(null, env, conn)); -} -function maybeHandleReq(env: ServeEnv, conn: Conn, maybeReq: any) { - const [req, _err] = maybeReq; - if (_err) { - conn.close(); // assume EOF for now... - return; - } - env.reqQueue.push(req); // push req to queue - env.serveDeferred.resolve(); // signal while loop to process it - // TODO: protection against client req flooding - serveConn(env, conn); // try read more (reusing connection) -} - -export async function* serve(addr: string) { - const listener = listen("tcp", addr); - const env: ServeEnv = { - reqQueue: [], // in case multiple promises are ready - serveDeferred: deferred() - }; - - // Routine that keeps calling accept - const acceptRoutine = () => { - const handleConn = (conn: Conn) => { - serveConn(env, conn); // don't block - scheduleAccept(); // schedule next accept - }; - const scheduleAccept = () => { - listener.accept().then(handleConn); - }; - scheduleAccept(); - }; - - acceptRoutine(); - - // Loop hack to allow yield (yield won't work in callbacks) - while (true) { - await env.serveDeferred.promise; - env.serveDeferred = deferred(); // use a new deferred - let queueToProcess = env.reqQueue; - env.reqQueue = []; - for (const result of queueToProcess) { - yield result; - } - } - listener.close(); -} - -export async function listenAndServe( - addr: string, - handler: (req: ServerRequest) => void -) { - const server = serve(addr); - - for await (const request of server) { - await handler(request); - } -} - -export interface Response { - status?: number; - headers?: Headers; - body?: Uint8Array | Reader; -} - -export function setContentLength(r: Response): void { - if (!r.headers) { - r.headers = new Headers(); - } - - if (r.body) { - if (!r.headers.has("content-length")) { - if (r.body instanceof Uint8Array) { - const bodyLength = r.body.byteLength; - r.headers.append("Content-Length", bodyLength.toString()); - } else { - r.headers.append("Transfer-Encoding", "chunked"); - } - } - } -} - -export class ServerRequest { - url: string; - method: string; - proto: string; - headers: Headers; - w: BufWriter; - - private async _streamBody(body: Reader, bodyLength: number) { - const n = await copy(this.w, body); - assert(n == bodyLength); - } - - private async _streamChunkedBody(body: Reader) { - const encoder = new TextEncoder(); - - for await (const chunk of toAsyncIterator(body)) { - const start = encoder.encode(`${chunk.byteLength.toString(16)}\r\n`); - const end = encoder.encode("\r\n"); - await this.w.write(start); - await this.w.write(chunk); - await this.w.write(end); - } - - const endChunk = encoder.encode("0\r\n\r\n"); - await this.w.write(endChunk); - } - - async respond(r: Response): Promise { - const protoMajor = 1; - const protoMinor = 1; - const statusCode = r.status || 200; - const statusText = STATUS_TEXT.get(statusCode); - if (!statusText) { - throw Error("bad status code"); - } - - let out = `HTTP/${protoMajor}.${protoMinor} ${statusCode} ${statusText}\r\n`; - - setContentLength(r); - - if (r.headers) { - for (const [key, value] of r.headers) { - out += `${key}: ${value}\r\n`; - } - } - out += "\r\n"; - - const header = new TextEncoder().encode(out); - let n = await this.w.write(header); - assert(header.byteLength == n); - - if (r.body) { - if (r.body instanceof Uint8Array) { - n = await this.w.write(r.body); - assert(r.body.byteLength == n); - } else { - if (r.headers.has("content-length")) { - await this._streamBody( - r.body, - parseInt(r.headers.get("content-length")) - ); - } else { - await this._streamChunkedBody(r.body); - } - } - } - - await this.w.flush(); - } -} - -async function readRequest(c: Conn): Promise<[ServerRequest, BufState]> { - const bufr = new BufReader(c); - const bufw = new BufWriter(c); - const req = new ServerRequest(); - req.w = bufw; - const tp = new TextProtoReader(bufr); - - let s: string; - let err: BufState; - - // First line: GET /index.html HTTP/1.0 - [s, err] = await tp.readLine(); - if (err) { - return [null, err]; - } - [req.method, req.url, req.proto] = s.split(" ", 3); - - [req.headers, err] = await tp.readMIMEHeader(); - - // TODO: handle body - - return [req, err]; -} diff --git a/http_bench.ts b/http_bench.ts deleted file mode 100644 index 8e1e24ad6..000000000 --- a/http_bench.ts +++ /dev/null @@ -1,15 +0,0 @@ -import * as deno from "deno"; -import { serve } from "./http.ts"; - -const addr = deno.args[1] || "127.0.0.1:4500"; -const server = serve(addr); - -const body = new TextEncoder().encode("Hello World"); - -async function main(): Promise { - for await (const request of server) { - await request.respond({ status: 200, body }); - } -} - -main(); diff --git a/http_status.ts b/http_status.ts deleted file mode 100644 index a3006d319..000000000 --- a/http_status.ts +++ /dev/null @@ -1,134 +0,0 @@ -export enum Status { - Continue = 100, // RFC 7231, 6.2.1 - SwitchingProtocols = 101, // RFC 7231, 6.2.2 - Processing = 102, // RFC 2518, 10.1 - - OK = 200, // RFC 7231, 6.3.1 - Created = 201, // RFC 7231, 6.3.2 - Accepted = 202, // RFC 7231, 6.3.3 - NonAuthoritativeInfo = 203, // RFC 7231, 6.3.4 - NoContent = 204, // RFC 7231, 6.3.5 - ResetContent = 205, // RFC 7231, 6.3.6 - PartialContent = 206, // RFC 7233, 4.1 - MultiStatus = 207, // RFC 4918, 11.1 - AlreadyReported = 208, // RFC 5842, 7.1 - IMUsed = 226, // RFC 3229, 10.4.1 - - MultipleChoices = 300, // RFC 7231, 6.4.1 - MovedPermanently = 301, // RFC 7231, 6.4.2 - Found = 302, // RFC 7231, 6.4.3 - SeeOther = 303, // RFC 7231, 6.4.4 - NotModified = 304, // RFC 7232, 4.1 - UseProxy = 305, // RFC 7231, 6.4.5 - // _ = 306, // RFC 7231, 6.4.6 (Unused) - TemporaryRedirect = 307, // RFC 7231, 6.4.7 - PermanentRedirect = 308, // RFC 7538, 3 - - BadRequest = 400, // RFC 7231, 6.5.1 - Unauthorized = 401, // RFC 7235, 3.1 - PaymentRequired = 402, // RFC 7231, 6.5.2 - Forbidden = 403, // RFC 7231, 6.5.3 - NotFound = 404, // RFC 7231, 6.5.4 - MethodNotAllowed = 405, // RFC 7231, 6.5.5 - NotAcceptable = 406, // RFC 7231, 6.5.6 - ProxyAuthRequired = 407, // RFC 7235, 3.2 - RequestTimeout = 408, // RFC 7231, 6.5.7 - Conflict = 409, // RFC 7231, 6.5.8 - Gone = 410, // RFC 7231, 6.5.9 - LengthRequired = 411, // RFC 7231, 6.5.10 - PreconditionFailed = 412, // RFC 7232, 4.2 - RequestEntityTooLarge = 413, // RFC 7231, 6.5.11 - RequestURITooLong = 414, // RFC 7231, 6.5.12 - UnsupportedMediaType = 415, // RFC 7231, 6.5.13 - RequestedRangeNotSatisfiable = 416, // RFC 7233, 4.4 - ExpectationFailed = 417, // RFC 7231, 6.5.14 - Teapot = 418, // RFC 7168, 2.3.3 - MisdirectedRequest = 421, // RFC 7540, 9.1.2 - UnprocessableEntity = 422, // RFC 4918, 11.2 - Locked = 423, // RFC 4918, 11.3 - FailedDependency = 424, // RFC 4918, 11.4 - UpgradeRequired = 426, // RFC 7231, 6.5.15 - PreconditionRequired = 428, // RFC 6585, 3 - TooManyRequests = 429, // RFC 6585, 4 - RequestHeaderFieldsTooLarge = 431, // RFC 6585, 5 - UnavailableForLegalReasons = 451, // RFC 7725, 3 - - InternalServerError = 500, // RFC 7231, 6.6.1 - NotImplemented = 501, // RFC 7231, 6.6.2 - BadGateway = 502, // RFC 7231, 6.6.3 - ServiceUnavailable = 503, // RFC 7231, 6.6.4 - GatewayTimeout = 504, // RFC 7231, 6.6.5 - HTTPVersionNotSupported = 505, // RFC 7231, 6.6.6 - VariantAlsoNegotiates = 506, // RFC 2295, 8.1 - InsufficientStorage = 507, // RFC 4918, 11.5 - LoopDetected = 508, // RFC 5842, 7.2 - NotExtended = 510, // RFC 2774, 7 - NetworkAuthenticationRequired = 511 // RFC 6585, 6 -} - -export const STATUS_TEXT = new Map([ - [Status.Continue, "Continue"], - [Status.SwitchingProtocols, "Switching Protocols"], - [Status.Processing, "Processing"], - - [Status.OK, "OK"], - [Status.Created, "Created"], - [Status.Accepted, "Accepted"], - [Status.NonAuthoritativeInfo, "Non-Authoritative Information"], - [Status.NoContent, "No Content"], - [Status.ResetContent, "Reset Content"], - [Status.PartialContent, "Partial Content"], - [Status.MultiStatus, "Multi-Status"], - [Status.AlreadyReported, "Already Reported"], - [Status.IMUsed, "IM Used"], - - [Status.MultipleChoices, "Multiple Choices"], - [Status.MovedPermanently, "Moved Permanently"], - [Status.Found, "Found"], - [Status.SeeOther, "See Other"], - [Status.NotModified, "Not Modified"], - [Status.UseProxy, "Use Proxy"], - [Status.TemporaryRedirect, "Temporary Redirect"], - [Status.PermanentRedirect, "Permanent Redirect"], - - [Status.BadRequest, "Bad Request"], - [Status.Unauthorized, "Unauthorized"], - [Status.PaymentRequired, "Payment Required"], - [Status.Forbidden, "Forbidden"], - [Status.NotFound, "Not Found"], - [Status.MethodNotAllowed, "Method Not Allowed"], - [Status.NotAcceptable, "Not Acceptable"], - [Status.ProxyAuthRequired, "Proxy Authentication Required"], - [Status.RequestTimeout, "Request Timeout"], - [Status.Conflict, "Conflict"], - [Status.Gone, "Gone"], - [Status.LengthRequired, "Length Required"], - [Status.PreconditionFailed, "Precondition Failed"], - [Status.RequestEntityTooLarge, "Request Entity Too Large"], - [Status.RequestURITooLong, "Request URI Too Long"], - [Status.UnsupportedMediaType, "Unsupported Media Type"], - [Status.RequestedRangeNotSatisfiable, "Requested Range Not Satisfiable"], - [Status.ExpectationFailed, "Expectation Failed"], - [Status.Teapot, "I'm a teapot"], - [Status.MisdirectedRequest, "Misdirected Request"], - [Status.UnprocessableEntity, "Unprocessable Entity"], - [Status.Locked, "Locked"], - [Status.FailedDependency, "Failed Dependency"], - [Status.UpgradeRequired, "Upgrade Required"], - [Status.PreconditionRequired, "Precondition Required"], - [Status.TooManyRequests, "Too Many Requests"], - [Status.RequestHeaderFieldsTooLarge, "Request Header Fields Too Large"], - [Status.UnavailableForLegalReasons, "Unavailable For Legal Reasons"], - - [Status.InternalServerError, "Internal Server Error"], - [Status.NotImplemented, "Not Implemented"], - [Status.BadGateway, "Bad Gateway"], - [Status.ServiceUnavailable, "Service Unavailable"], - [Status.GatewayTimeout, "Gateway Timeout"], - [Status.HTTPVersionNotSupported, "HTTP Version Not Supported"], - [Status.VariantAlsoNegotiates, "Variant Also Negotiates"], - [Status.InsufficientStorage, "Insufficient Storage"], - [Status.LoopDetected, "Loop Detected"], - [Status.NotExtended, "Not Extended"], - [Status.NetworkAuthenticationRequired, "Network Authentication Required"] -]); diff --git a/http_test.ts b/http_test.ts deleted file mode 100644 index cdb7f8303..000000000 --- a/http_test.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Ported from -// https://github.com/golang/go/blob/master/src/net/http/responsewrite_test.go - -import { - test, - assert, - assertEqual -} from "https://deno.land/x/testing/testing.ts"; - -import { - listenAndServe, - ServerRequest, - setContentLength, - Response -} from "./http"; -import { Buffer } from "deno"; -import { BufWriter } from "./bufio"; - -interface ResponseTest { - response: Response; - raw: string; -} - -const responseTests: ResponseTest[] = [ - // Default response - { - response: {}, - raw: "HTTP/1.1 200 OK\r\n" + "\r\n" - }, - // HTTP/1.1, chunked coding; empty trailer; close - { - response: { - status: 200, - body: new Buffer(new TextEncoder().encode("abcdef")) - }, - - raw: - "HTTP/1.1 200 OK\r\n" + - "transfer-encoding: chunked\r\n\r\n" + - "6\r\nabcdef\r\n0\r\n\r\n" - } -]; - -test(async function responseWrite() { - for (const testCase of responseTests) { - const buf = new Buffer(); - const bufw = new BufWriter(buf); - const request = new ServerRequest(); - request.w = bufw; - - await request.respond(testCase.response); - assertEqual(buf.toString(), testCase.raw); - } -}); diff --git a/iotest.ts b/iotest.ts deleted file mode 100644 index e3a42f58a..000000000 --- a/iotest.ts +++ /dev/null @@ -1,61 +0,0 @@ -// Ported to Deno from -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -import { Reader, ReadResult } from "deno"; - -/** OneByteReader returns a Reader that implements - * each non-empty Read by reading one byte from r. - */ -export class OneByteReader implements Reader { - constructor(readonly r: Reader) {} - - async read(p: Uint8Array): Promise { - if (p.byteLength === 0) { - return { nread: 0, eof: false }; - } - if (!(p instanceof Uint8Array)) { - throw Error("expected Uint8Array"); - } - return this.r.read(p.subarray(0, 1)); - } -} - -/** HalfReader returns a Reader that implements Read - * by reading half as many requested bytes from r. - */ -export class HalfReader implements Reader { - constructor(readonly r: Reader) {} - - async read(p: Uint8Array): Promise { - if (!(p instanceof Uint8Array)) { - throw Error("expected Uint8Array"); - } - const half = Math.floor((p.byteLength + 1) / 2); - return this.r.read(p.subarray(0, half)); - } -} - -export class ErrTimeout extends Error { - constructor() { - super("timeout"); - this.name = "ErrTimeout"; - } -} - -/** TimeoutReader returns ErrTimeout on the second read - * with no data. Subsequent calls to read succeed. - */ -export class TimeoutReader implements Reader { - count = 0; - constructor(readonly r: Reader) {} - - async read(p: Uint8Array): Promise { - this.count++; - if (this.count === 2) { - throw new ErrTimeout(); - } - return this.r.read(p); - } -} diff --git a/net/bufio.ts b/net/bufio.ts new file mode 100644 index 000000000..b412cbce8 --- /dev/null +++ b/net/bufio.ts @@ -0,0 +1,465 @@ +// Based on https://github.com/golang/go/blob/891682/src/bufio/bufio.go +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import { Reader, ReadResult, Writer } from "deno"; +import { assert, charCode, copyBytes } from "./util.ts"; + +const DEFAULT_BUF_SIZE = 4096; +const MIN_BUF_SIZE = 16; +const MAX_CONSECUTIVE_EMPTY_READS = 100; +const CR = charCode("\r"); +const LF = charCode("\n"); + +export type BufState = + | null + | "EOF" + | "BufferFull" + | "ShortWrite" + | "NoProgress" + | Error; + +/** BufReader implements buffering for a Reader object. */ +export class BufReader implements Reader { + private buf: Uint8Array; + 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; + + constructor(rd: Reader, size = DEFAULT_BUF_SIZE) { + if (size < MIN_BUF_SIZE) { + size = MIN_BUF_SIZE; + } + this._reset(new Uint8Array(size), rd); + } + + /** Returns the size of the underlying buffer in bytes. */ + size(): number { + return this.buf.byteLength; + } + + buffered(): number { + 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. + if (this.r > 0) { + this.buf.copyWithin(0, this.r, this.w); + this.w -= this.r; + this.r = 0; + } + + if (this.w >= this.buf.byteLength) { + throw Error("bufio: tried to fill full buffer"); + } + + // 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; + } + assert(rr.nread >= 0, "negative read"); + this.w += rr.nread; + if (rr.eof) { + this.err = "EOF"; + return; + } + if (rr.nread > 0) { + return; + } + } + this.err = "NoProgress"; + } + + /** Discards any buffered data, resets all state, and switches + * the buffered reader to read from r. + */ + reset(r: Reader): void { + this._reset(this.buf, r); + } + + private _reset(buf: Uint8Array, rd: Reader): void { + this.buf = buf; + this.rd = rd; + this.lastByte = -1; + // this.lastRuneSize = -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 (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); + 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(); + } + 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; + } + assert(rr.nread >= 0, "negative read"); + if (rr.nread === 0) { + if (this.err) { + throw this._readErr(); + } + 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); + this.r += rr.nread; + this.lastByte = this.buf[this.r - 1]; + // this.lastRuneSize = -1; + return rr; + } + + /** reads exactly len(p) bytes into p. + * 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; + } + return [nread, nread < p.length ? "EOF" : null]; + } + + + /** Returns the next byte [0, 255] or -1 if EOF. */ + async readByte(): Promise { + while (this.r === this.w) { + 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; + return c; + } + + /** readString() reads until the first occurrence of delim in the input, + * returning a string containing the data up to and including the delimiter. + * If ReadString encounters an error before finding a delimiter, + * it returns the data read before the error and the error itself (often io.EOF). + * ReadString returns err != nil if and only if the returned data does not end in + * delim. + * For simple uses, a Scanner may be more convenient. + */ + 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 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 + * 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 + * 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. + * + * 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. + */ + async readLine(): Promise<[Uint8Array, boolean, BufState]> { + let [line, err] = await this.readSlice(LF); + + if (err === "BufferFull") { + // Handle the case where "\r\n" straddles the buffer. + if (line.byteLength > 0 && line[line.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); + } + return [line, true, null]; + } + + if (line.byteLength === 0) { + return [line, false, err]; + } + err = null; + + if (line[line.byteLength - 1] == LF) { + let drop = 1; + if (line.byteLength > 1 && line[line.byteLength - 2] === CR) { + drop = 2; + } + line = line.subarray(0, line.byteLength - drop); + } + return [line, false, err]; + } + + /** 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. + */ + async readSlice(delim: number): Promise<[Uint8Array, BufState]> { + let s = 0; // search start index + let line: Uint8Array; + let err: BufState; + 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); + this.r += i + 1; + break; + } + + // Pending error? + if (this.err) { + line = 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; + } + + s = this.w - this.r; // do not rescan area we scanned before + + await this._fill(); // buffer is not full + } + + // Handle last byte, if any. + let i = line.byteLength - 1; + if (i >= 0) { + this.lastByte = line[i]; + // this.lastRuneSize = -1 + } + + return [line, err]; + } + + /** 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. + */ + async peek(n: number): Promise<[Uint8Array, BufState]> { + 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 + } + + if (n > this.buf.byteLength) { + return [this.buf.subarray(this.r, this.w), "BufferFull"]; + } + + // 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]; + } +} + +/** BufWriter implements buffering for an deno.Writer object. + * If an error occurs writing to a Writer, no more data will be + * accepted and all subsequent writes, and flush(), will return the error. + * After all data has been written, the client should call the + * flush() method to guarantee all data has been forwarded to + * the underlying deno.Writer. + */ +export class BufWriter implements Writer { + buf: Uint8Array; + n: number = 0; + err: null | BufState = null; + + constructor(private wr: Writer, size = DEFAULT_BUF_SIZE) { + if (size <= 0) { + size = DEFAULT_BUF_SIZE; + } + this.buf = new Uint8Array(size); + } + + /** Size returns the size of the underlying buffer in bytes. */ + size(): number { + return this.buf.byteLength; + } + + /** Discards any unflushed buffered data, clears any error, and + * resets b to write its output to w. + */ + reset(w: Writer): void { + this.err = null; + this.n = 0; + this.wr = w; + } + + /** 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; + } + + let n: number; + let err: BufState = null; + try { + n = await this.wr.write(this.buf.subarray(0, this.n)); + } catch (e) { + err = e; + } + + if (n < this.n && err == null) { + err = "ShortWrite"; + } + + if (err != null) { + if (n > 0 && n < this.n) { + this.buf.copyWithin(0, n, this.n); + } + this.n -= n; + this.err = err; + return err; + } + this.n = 0; + } + + /** Returns how many bytes are unused in the buffer. */ + available(): number { + return this.buf.byteLength - this.n; + } + + /** buffered returns the number of bytes that have been written into the + * current buffer. + */ + buffered(): number { + return this.n; + } + + /** Writes the contents of p into the buffer. + * Returns the number of bytes written. + */ + async write(p: Uint8Array): Promise { + let nn = 0; + let n: number; + while (p.byteLength > this.available() && !this.err) { + 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; + } + } else { + n = copyBytes(this.buf, p, this.n); + this.n += n; + await this.flush(); + } + nn += n; + p = p.subarray(n); + } + if (this.err) { + throw this.err; + } + n = copyBytes(this.buf, p, this.n); + this.n += n; + nn += n; + return nn; + } +} diff --git a/net/bufio_test.ts b/net/bufio_test.ts new file mode 100644 index 000000000..19954bdf6 --- /dev/null +++ b/net/bufio_test.ts @@ -0,0 +1,345 @@ +// Based on https://github.com/golang/go/blob/891682/src/bufio/bufio_test.go +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import { Buffer, Reader, ReadResult } from "deno"; +import { + test, + assert, + assertEqual +} from "https://deno.land/x/testing/testing.ts"; +import { BufReader, BufState, BufWriter } from "./bufio.ts"; +import * as iotest from "./iotest.ts"; +import { charCode, copyBytes, stringsReader } from "./util.ts"; + +const encoder = new TextEncoder(); + +async function readBytes(buf: BufReader): Promise { + const b = new Uint8Array(1000); + let nb = 0; + while (true) { + let c = await buf.readByte(); + if (c < 0) { + break; // EOF + } + b[nb] = c; + nb++; + } + const decoder = new TextDecoder(); + return decoder.decode(b.subarray(0, nb)); +} + +test(async function bufioReaderSimple() { + const data = "hello world"; + const b = new BufReader(stringsReader(data)); + const s = await readBytes(b); + assertEqual(s, data); +}); + +type ReadMaker = { name: string; fn: (r: Reader) => Reader }; + +const readMakers: ReadMaker[] = [ + { name: "full", fn: r => r }, + { name: "byte", fn: r => new iotest.OneByteReader(r) }, + { name: "half", fn: r => new iotest.HalfReader(r) } + // TODO { name: "data+err", r => new iotest.DataErrReader(r) }, + // { name: "timeout", fn: r => new iotest.TimeoutReader(r) }, +]; + +function readLines(b: BufReader): string { + let s = ""; + while (true) { + let s1 = b.readString("\n"); + if (s1 == null) { + break; // EOF + } + s += s1; + } + return s; +} + +// Call read to accumulate the text of a file +async function reads(buf: BufReader, m: number): Promise { + const b = new Uint8Array(1000); + let nb = 0; + while (true) { + const { nread, eof } = await buf.read(b.subarray(nb, nb + m)); + nb += nread; + if (eof) { + break; + } + } + const decoder = new TextDecoder(); + return decoder.decode(b.subarray(0, nb)); +} + +type NamedBufReader = { name: string; fn: (r: BufReader) => Promise }; + +const bufreaders: NamedBufReader[] = [ + { name: "1", fn: (b: BufReader) => reads(b, 1) }, + { name: "2", fn: (b: BufReader) => reads(b, 2) }, + { name: "3", fn: (b: BufReader) => reads(b, 3) }, + { name: "4", fn: (b: BufReader) => reads(b, 4) }, + { name: "5", fn: (b: BufReader) => reads(b, 5) }, + { name: "7", fn: (b: BufReader) => reads(b, 7) }, + { name: "bytes", fn: readBytes } + // { name: "lines", fn: readLines }, +]; + +const MIN_READ_BUFFER_SIZE = 16; +const bufsizes: number[] = [ + 0, + MIN_READ_BUFFER_SIZE, + 23, + 32, + 46, + 64, + 93, + 128, + 1024, + 4096 +]; + +test(async function bufioBufReader() { + const texts = new Array(31); + let str = ""; + let all = ""; + for (let i = 0; i < texts.length - 1; i++) { + texts[i] = str + "\n"; + all += texts[i]; + str += String.fromCharCode(i % 26 + 97); + } + texts[texts.length - 1] = all; + + for (let text of texts) { + for (let readmaker of readMakers) { + for (let bufreader of bufreaders) { + for (let bufsize of bufsizes) { + const read = readmaker.fn(stringsReader(text)); + const buf = new BufReader(read, bufsize); + const s = await bufreader.fn(buf); + const debugStr = + `reader=${readmaker.name} ` + + `fn=${bufreader.name} bufsize=${bufsize} want=${text} got=${s}`; + assertEqual(s, text, debugStr); + } + } + } + } +}); + +test(async function bufioBufferFull() { + 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); + assertEqual(err, "BufferFull"); + assertEqual(actual, "And now, hello, "); + + [line, err] = await buf.readSlice(charCode("!")); + actual = decoder.decode(line); + assertEqual(actual, "world!"); + assert(err == null); +}); + +const testInput = encoder.encode( + "012\n345\n678\n9ab\ncde\nfgh\nijk\nlmn\nopq\nrst\nuvw\nxy" +); +const testInputrn = encoder.encode( + "012\r\n345\r\n678\r\n9ab\r\ncde\r\nfgh\r\nijk\r\nlmn\r\nopq\r\nrst\r\nuvw\r\nxy\r\n\n\r\n" +); +const testOutput = encoder.encode("0123456789abcdefghijklmnopqrstuvwxy"); + +// TestReader wraps a Uint8Array and returns reads of a specific length. +class TestReader implements Reader { + constructor(private data: Uint8Array, private stride: number) {} + + async read(buf: Uint8Array): Promise { + let nread = this.stride; + if (nread > this.data.byteLength) { + nread = this.data.byteLength; + } + if (nread > buf.byteLength) { + nread = buf.byteLength; + } + copyBytes(buf as Uint8Array, this.data); + this.data = this.data.subarray(nread); + let eof = false; + if (this.data.byteLength == 0) { + eof = true; + } + return { nread, eof }; + } +} + +async function testReadLine(input: Uint8Array): Promise { + for (let stride = 1; stride < 2; stride++) { + let done = 0; + 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"); + } + assertEqual(isPrefix, false); + if (err == "EOF") { + break; + } + let want = testOutput.subarray(done, done + line.byteLength); + assertEqual( + line, + want, + `Bad line at stride ${stride}: want: ${want} got: ${line}` + ); + done += line.byteLength; + } + assertEqual( + done, + testOutput.byteLength, + `readLine didn't return everything: got: ${done}, ` + + `want: ${testOutput} (stride: ${stride})` + ); + } +} + +test(async function bufioReadLine() { + await testReadLine(testInput); + await testReadLine(testInputrn); +}); + +test(async function bufioPeek() { + const decoder = new TextDecoder(); + let p = new Uint8Array(10); + // string is 16 (minReadBufferSize) long. + let buf = new BufReader( + stringsReader("abcdefghijklmnop"), + MIN_READ_BUFFER_SIZE + ); + + let [actual, err] = await buf.peek(1); + assertEqual(decoder.decode(actual), "a"); + assert(err == null); + + [actual, err] = await buf.peek(4); + assertEqual(decoder.decode(actual), "abcd"); + assert(err == null); + + [actual, err] = await buf.peek(32); + assertEqual(decoder.decode(actual), "abcdefghijklmnop"); + assertEqual(err, "BufferFull"); + + await buf.read(p.subarray(0, 3)); + assertEqual(decoder.decode(p.subarray(0, 3)), "abc"); + + [actual, err] = await buf.peek(1); + assertEqual(decoder.decode(actual), "d"); + assert(err == null); + + [actual, err] = await buf.peek(1); + assertEqual(decoder.decode(actual), "d"); + assert(err == null); + + [actual, err] = await buf.peek(1); + assertEqual(decoder.decode(actual), "d"); + assert(err == null); + + [actual, err] = await buf.peek(2); + assertEqual(decoder.decode(actual), "de"); + assert(err == null); + + let { eof } = await buf.read(p.subarray(0, 3)); + assertEqual(decoder.decode(p.subarray(0, 3)), "def"); + assert(!eof); + assert(err == null); + + [actual, err] = await buf.peek(4); + assertEqual(decoder.decode(actual), "ghij"); + assert(err == null); + + await buf.read(p); + assertEqual(decoder.decode(p), "ghijklmnop"); + + [actual, err] = await buf.peek(0); + assertEqual(decoder.decode(actual), ""); + assert(err == null); + + [actual, err] = await buf.peek(1); + assertEqual(decoder.decode(actual), ""); + assert(err == "EOF"); + /* TODO + // Test for issue 3022, not exposing a reader's error on a successful Peek. + buf = NewReaderSize(dataAndEOFReader("abcd"), 32) + if s, err := buf.Peek(2); string(s) != "ab" || err != nil { + t.Errorf(`Peek(2) on "abcd", EOF = %q, %v; want "ab", nil`, string(s), err) + } + if s, err := buf.Peek(4); string(s) != "abcd" || err != nil { + t.Errorf(`Peek(4) on "abcd", EOF = %q, %v; want "abcd", nil`, string(s), err) + } + if n, err := buf.Read(p[0:5]); string(p[0:n]) != "abcd" || err != nil { + t.Fatalf("Read after peek = %q, %v; want abcd, EOF", p[0:n], err) + } + if n, err := buf.Read(p[0:1]); string(p[0:n]) != "" || err != io.EOF { + t.Fatalf(`second Read after peek = %q, %v; want "", EOF`, p[0:n], err) + } + */ +}); + +test(async function bufioWriter() { + const data = new Uint8Array(8192); + + for (let i = 0; i < data.byteLength; i++) { + data[i] = charCode(" ") + i % (charCode("~") - charCode(" ")); + } + + const w = new Buffer(); + for (let nwrite of bufsizes) { + for (let bs of bufsizes) { + // Write nwrite bytes using buffer size bs. + // Check that the right amount makes it out + // and that the data is correct. + + w.reset(); + const buf = new BufWriter(w, bs); + + const context = `nwrite=${nwrite} bufsize=${bs}`; + const n = await buf.write(data.subarray(0, nwrite)); + assertEqual(n, nwrite, context); + + await buf.flush(); + + const written = w.bytes(); + assertEqual(written.byteLength, nwrite); + + for (let l = 0; l < written.byteLength; l++) { + assertEqual(written[l], data[l]); + } + } + } +}); + +test(async function bufReaderReadFull() { + const enc = new TextEncoder(); + const dec = new TextDecoder(); + const text = "Hello World"; + const data = new Buffer(enc.encode(text)); + const bufr = new BufReader(data, 3); + { + const buf = new Uint8Array(6); + const [nread, err] = await bufr.readFull(buf); + assertEqual(nread, 6); + assert(!err); + assertEqual(dec.decode(buf), "Hello "); + } + { + const buf = new Uint8Array(6); + const [nread, err] = await bufr.readFull(buf); + assertEqual(nread, 5); + assertEqual(err, "EOF"); + assertEqual(dec.decode(buf.subarray(0, 5)), "World"); + } +}); diff --git a/net/file_server.ts b/net/file_server.ts new file mode 100755 index 000000000..bd1c52b88 --- /dev/null +++ b/net/file_server.ts @@ -0,0 +1,214 @@ +#!/usr/bin/env deno --allow-net + +// This program serves files in the current directory over HTTP. +// TODO Stream responses instead of reading them into memory. +// TODO Add tests like these: +// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js + +import { + listenAndServe, + ServerRequest, + setContentLength, + Response +} from "./http"; +import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno"; + +const dirViewerTemplate = ` + + + + + + + Deno File Server + + + +

Index of <%DIRNAME%>

+ + + <%CONTENTS%> +
ModeSizeName
+ + +`; + +let currentDir = cwd(); +const target = args[1]; +if (target) { + currentDir = `${currentDir}/${target}`; +} +const addr = `0.0.0.0:${args[2] || 4500}`; +const encoder = new TextEncoder(); + +function modeToString(isDir: boolean, maybeMode: number | null) { + const modeMap = ["---", "--x", "-w-", "-wx", "r--", "r-x", "rw-", "rwx"]; + + if (maybeMode === null) { + return "(unknown mode)"; + } + const mode = maybeMode!.toString(8); + if (mode.length < 3) { + return "(unknown mode)"; + } + let output = ""; + mode + .split("") + .reverse() + .slice(0, 3) + .forEach(v => { + output = modeMap[+v] + output; + }); + output = `(${isDir ? "d" : "-"}${output})`; + return output; +} + +function fileLenToString(len: number) { + const multipler = 1024; + let base = 1; + const suffix = ["B", "K", "M", "G", "T"]; + let suffixIndex = 0; + + while (base * multipler < len) { + if (suffixIndex >= suffix.length - 1) { + break; + } + base *= multipler; + suffixIndex++; + } + + return `${(len / base).toFixed(2)}${suffix[suffixIndex]}`; +} + +function createDirEntryDisplay( + name: string, + path: string, + size: number | null, + mode: number | null, + isDir: boolean +) { + const sizeStr = size === null ? "" : "" + fileLenToString(size!); + return ` + ${modeToString( + isDir, + mode + )}${sizeStr}${name}${ + isDir ? "/" : "" + } + + `; +} + +// TODO: simplify this after deno.stat and deno.readDir are fixed +async function serveDir(req: ServerRequest, dirPath: string, dirName: string) { + // dirname has no prefix + const listEntry: string[] = []; + const fileInfos = await readDir(dirPath); + for (const info of fileInfos) { + if (info.name === "index.html" && info.isFile()) { + // in case index.html as dir... + return await serveFile(req, info.path); + } + // Yuck! + let mode = null; + try { + mode = (await stat(info.path)).mode; + } catch (e) {} + listEntry.push( + createDirEntryDisplay( + info.name, + dirName + "/" + info.name, + info.isFile() ? info.len : null, + mode, + info.isDirectory() + ) + ); + } + + const page = new TextEncoder().encode( + dirViewerTemplate + .replace("<%DIRNAME%>", dirName + "/") + .replace("<%CONTENTS%>", listEntry.join("")) + ); + + const headers = new Headers(); + headers.set("content-type", "text/html"); + + const res = { + status: 200, + body: page, + headers + }; + setContentLength(res); + return res; +} + +async function serveFile(req: ServerRequest, filename: string) { + const file = await open(filename); + const fileInfo = await stat(filename); + const headers = new Headers(); + headers.set("content-length", fileInfo.len.toString()); + + const res = { + status: 200, + body: file, + headers + }; + return res; +} + +async function serveFallback(req: ServerRequest, e: Error) { + if ( + e instanceof DenoError && + (e as DenoError).kind === ErrorKind.NotFound + ) { + return { + status: 404, + body: encoder.encode("Not found") + }; + } else { + return { + status: 500, + body: encoder.encode("Internal server error") + }; + } +} + +function serverLog(req: ServerRequest, res: Response) { + const d = new Date().toISOString(); + const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`; + const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`; + console.log(s); +} + +listenAndServe(addr, async req => { + const fileName = req.url.replace(/\/$/, ""); + const filePath = currentDir + fileName; + + let response: Response; + + try { + const fileInfo = await stat(filePath); + if (fileInfo.isDirectory()) { + // Bug with deno.stat: name and path not populated + // Yuck! + response = await serveDir(req, filePath, fileName); + } else { + response = await serveFile(req, filePath); + } + } catch (e) { + response = await serveFallback(req, e); + } finally { + serverLog(req, response); + req.respond(response); + } +}); + +console.log(`HTTP server listening on http://${addr}/`); diff --git a/net/file_server_test.ts b/net/file_server_test.ts new file mode 100644 index 000000000..a04ced7e5 --- /dev/null +++ b/net/file_server_test.ts @@ -0,0 +1,46 @@ +import { readFile } from "deno"; + +import { + test, + assert, + assertEqual +} from "https://deno.land/x/testing/testing.ts"; + +// Promise to completeResolve when all tests completes +let completeResolve; +export const completePromise = new Promise(res => (completeResolve = res)); +let completedTestCount = 0; + +function maybeCompleteTests() { + completedTestCount++; + // Change this when adding more tests + if (completedTestCount === 3) { + completeResolve(); + } +} + +export function runTests(serverReadyPromise: Promise) { + test(async function serveFile() { + await serverReadyPromise; + const res = await fetch("http://localhost:4500/.travis.yml"); + const downloadedFile = await res.text(); + const localFile = new TextDecoder().decode(await readFile("./.travis.yml")); + assertEqual(downloadedFile, localFile); + maybeCompleteTests(); + }); + + test(async function serveDirectory() { + await serverReadyPromise; + const res = await fetch("http://localhost:4500/"); + const page = await res.text(); + assert(page.includes(".travis.yml")); + maybeCompleteTests(); + }); + + test(async function serveFallback() { + await serverReadyPromise; + const res = await fetch("http://localhost:4500/badfile.txt"); + assertEqual(res.status, 404); + maybeCompleteTests(); + }); +} diff --git a/net/http.ts b/net/http.ts new file mode 100644 index 000000000..bd45aea0d --- /dev/null +++ b/net/http.ts @@ -0,0 +1,212 @@ +import { listen, Conn, toAsyncIterator, Reader, copy } from "deno"; +import { BufReader, BufState, BufWriter } from "./bufio.ts"; +import { TextProtoReader } from "./textproto.ts"; +import { STATUS_TEXT } from "./http_status"; +import { assert } from "./util"; + +interface Deferred { + promise: Promise<{}>; + resolve: () => void; + reject: () => void; +} + +function deferred(): Deferred { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { + promise, + resolve, + reject + }; +} + +interface ServeEnv { + reqQueue: ServerRequest[]; + serveDeferred: Deferred; +} + +// Continuously read more requests from conn until EOF +// Mutually calling with maybeHandleReq +// TODO: make them async function after this change is done +// https://github.com/tc39/ecma262/pull/1250 +// See https://v8.dev/blog/fast-async +export function serveConn(env: ServeEnv, conn: Conn) { + readRequest(conn).then(maybeHandleReq.bind(null, env, conn)); +} +function maybeHandleReq(env: ServeEnv, conn: Conn, maybeReq: any) { + const [req, _err] = maybeReq; + if (_err) { + conn.close(); // assume EOF for now... + return; + } + env.reqQueue.push(req); // push req to queue + env.serveDeferred.resolve(); // signal while loop to process it + // TODO: protection against client req flooding + serveConn(env, conn); // try read more (reusing connection) +} + +export async function* serve(addr: string) { + const listener = listen("tcp", addr); + const env: ServeEnv = { + reqQueue: [], // in case multiple promises are ready + serveDeferred: deferred() + }; + + // Routine that keeps calling accept + const acceptRoutine = () => { + const handleConn = (conn: Conn) => { + serveConn(env, conn); // don't block + scheduleAccept(); // schedule next accept + }; + const scheduleAccept = () => { + listener.accept().then(handleConn); + }; + scheduleAccept(); + }; + + acceptRoutine(); + + // Loop hack to allow yield (yield won't work in callbacks) + while (true) { + await env.serveDeferred.promise; + env.serveDeferred = deferred(); // use a new deferred + let queueToProcess = env.reqQueue; + env.reqQueue = []; + for (const result of queueToProcess) { + yield result; + } + } + listener.close(); +} + +export async function listenAndServe( + addr: string, + handler: (req: ServerRequest) => void +) { + const server = serve(addr); + + for await (const request of server) { + await handler(request); + } +} + +export interface Response { + status?: number; + headers?: Headers; + body?: Uint8Array | Reader; +} + +export function setContentLength(r: Response): void { + if (!r.headers) { + r.headers = new Headers(); + } + + if (r.body) { + if (!r.headers.has("content-length")) { + if (r.body instanceof Uint8Array) { + const bodyLength = r.body.byteLength; + r.headers.append("Content-Length", bodyLength.toString()); + } else { + r.headers.append("Transfer-Encoding", "chunked"); + } + } + } +} + +export class ServerRequest { + url: string; + method: string; + proto: string; + headers: Headers; + w: BufWriter; + + private async _streamBody(body: Reader, bodyLength: number) { + const n = await copy(this.w, body); + assert(n == bodyLength); + } + + private async _streamChunkedBody(body: Reader) { + const encoder = new TextEncoder(); + + for await (const chunk of toAsyncIterator(body)) { + const start = encoder.encode(`${chunk.byteLength.toString(16)}\r\n`); + const end = encoder.encode("\r\n"); + await this.w.write(start); + await this.w.write(chunk); + await this.w.write(end); + } + + const endChunk = encoder.encode("0\r\n\r\n"); + await this.w.write(endChunk); + } + + async respond(r: Response): Promise { + const protoMajor = 1; + const protoMinor = 1; + const statusCode = r.status || 200; + const statusText = STATUS_TEXT.get(statusCode); + if (!statusText) { + throw Error("bad status code"); + } + + let out = `HTTP/${protoMajor}.${protoMinor} ${statusCode} ${statusText}\r\n`; + + setContentLength(r); + + if (r.headers) { + for (const [key, value] of r.headers) { + out += `${key}: ${value}\r\n`; + } + } + out += "\r\n"; + + const header = new TextEncoder().encode(out); + let n = await this.w.write(header); + assert(header.byteLength == n); + + if (r.body) { + if (r.body instanceof Uint8Array) { + n = await this.w.write(r.body); + assert(r.body.byteLength == n); + } else { + if (r.headers.has("content-length")) { + await this._streamBody( + r.body, + parseInt(r.headers.get("content-length")) + ); + } else { + await this._streamChunkedBody(r.body); + } + } + } + + await this.w.flush(); + } +} + +async function readRequest(c: Conn): Promise<[ServerRequest, BufState]> { + const bufr = new BufReader(c); + const bufw = new BufWriter(c); + const req = new ServerRequest(); + req.w = bufw; + const tp = new TextProtoReader(bufr); + + let s: string; + let err: BufState; + + // First line: GET /index.html HTTP/1.0 + [s, err] = await tp.readLine(); + if (err) { + return [null, err]; + } + [req.method, req.url, req.proto] = s.split(" ", 3); + + [req.headers, err] = await tp.readMIMEHeader(); + + // TODO: handle body + + return [req, err]; +} diff --git a/net/http_bench.ts b/net/http_bench.ts new file mode 100644 index 000000000..8e1e24ad6 --- /dev/null +++ b/net/http_bench.ts @@ -0,0 +1,15 @@ +import * as deno from "deno"; +import { serve } from "./http.ts"; + +const addr = deno.args[1] || "127.0.0.1:4500"; +const server = serve(addr); + +const body = new TextEncoder().encode("Hello World"); + +async function main(): Promise { + for await (const request of server) { + await request.respond({ status: 200, body }); + } +} + +main(); diff --git a/net/http_status.ts b/net/http_status.ts new file mode 100644 index 000000000..a3006d319 --- /dev/null +++ b/net/http_status.ts @@ -0,0 +1,134 @@ +export enum Status { + Continue = 100, // RFC 7231, 6.2.1 + SwitchingProtocols = 101, // RFC 7231, 6.2.2 + Processing = 102, // RFC 2518, 10.1 + + OK = 200, // RFC 7231, 6.3.1 + Created = 201, // RFC 7231, 6.3.2 + Accepted = 202, // RFC 7231, 6.3.3 + NonAuthoritativeInfo = 203, // RFC 7231, 6.3.4 + NoContent = 204, // RFC 7231, 6.3.5 + ResetContent = 205, // RFC 7231, 6.3.6 + PartialContent = 206, // RFC 7233, 4.1 + MultiStatus = 207, // RFC 4918, 11.1 + AlreadyReported = 208, // RFC 5842, 7.1 + IMUsed = 226, // RFC 3229, 10.4.1 + + MultipleChoices = 300, // RFC 7231, 6.4.1 + MovedPermanently = 301, // RFC 7231, 6.4.2 + Found = 302, // RFC 7231, 6.4.3 + SeeOther = 303, // RFC 7231, 6.4.4 + NotModified = 304, // RFC 7232, 4.1 + UseProxy = 305, // RFC 7231, 6.4.5 + // _ = 306, // RFC 7231, 6.4.6 (Unused) + TemporaryRedirect = 307, // RFC 7231, 6.4.7 + PermanentRedirect = 308, // RFC 7538, 3 + + BadRequest = 400, // RFC 7231, 6.5.1 + Unauthorized = 401, // RFC 7235, 3.1 + PaymentRequired = 402, // RFC 7231, 6.5.2 + Forbidden = 403, // RFC 7231, 6.5.3 + NotFound = 404, // RFC 7231, 6.5.4 + MethodNotAllowed = 405, // RFC 7231, 6.5.5 + NotAcceptable = 406, // RFC 7231, 6.5.6 + ProxyAuthRequired = 407, // RFC 7235, 3.2 + RequestTimeout = 408, // RFC 7231, 6.5.7 + Conflict = 409, // RFC 7231, 6.5.8 + Gone = 410, // RFC 7231, 6.5.9 + LengthRequired = 411, // RFC 7231, 6.5.10 + PreconditionFailed = 412, // RFC 7232, 4.2 + RequestEntityTooLarge = 413, // RFC 7231, 6.5.11 + RequestURITooLong = 414, // RFC 7231, 6.5.12 + UnsupportedMediaType = 415, // RFC 7231, 6.5.13 + RequestedRangeNotSatisfiable = 416, // RFC 7233, 4.4 + ExpectationFailed = 417, // RFC 7231, 6.5.14 + Teapot = 418, // RFC 7168, 2.3.3 + MisdirectedRequest = 421, // RFC 7540, 9.1.2 + UnprocessableEntity = 422, // RFC 4918, 11.2 + Locked = 423, // RFC 4918, 11.3 + FailedDependency = 424, // RFC 4918, 11.4 + UpgradeRequired = 426, // RFC 7231, 6.5.15 + PreconditionRequired = 428, // RFC 6585, 3 + TooManyRequests = 429, // RFC 6585, 4 + RequestHeaderFieldsTooLarge = 431, // RFC 6585, 5 + UnavailableForLegalReasons = 451, // RFC 7725, 3 + + InternalServerError = 500, // RFC 7231, 6.6.1 + NotImplemented = 501, // RFC 7231, 6.6.2 + BadGateway = 502, // RFC 7231, 6.6.3 + ServiceUnavailable = 503, // RFC 7231, 6.6.4 + GatewayTimeout = 504, // RFC 7231, 6.6.5 + HTTPVersionNotSupported = 505, // RFC 7231, 6.6.6 + VariantAlsoNegotiates = 506, // RFC 2295, 8.1 + InsufficientStorage = 507, // RFC 4918, 11.5 + LoopDetected = 508, // RFC 5842, 7.2 + NotExtended = 510, // RFC 2774, 7 + NetworkAuthenticationRequired = 511 // RFC 6585, 6 +} + +export const STATUS_TEXT = new Map([ + [Status.Continue, "Continue"], + [Status.SwitchingProtocols, "Switching Protocols"], + [Status.Processing, "Processing"], + + [Status.OK, "OK"], + [Status.Created, "Created"], + [Status.Accepted, "Accepted"], + [Status.NonAuthoritativeInfo, "Non-Authoritative Information"], + [Status.NoContent, "No Content"], + [Status.ResetContent, "Reset Content"], + [Status.PartialContent, "Partial Content"], + [Status.MultiStatus, "Multi-Status"], + [Status.AlreadyReported, "Already Reported"], + [Status.IMUsed, "IM Used"], + + [Status.MultipleChoices, "Multiple Choices"], + [Status.MovedPermanently, "Moved Permanently"], + [Status.Found, "Found"], + [Status.SeeOther, "See Other"], + [Status.NotModified, "Not Modified"], + [Status.UseProxy, "Use Proxy"], + [Status.TemporaryRedirect, "Temporary Redirect"], + [Status.PermanentRedirect, "Permanent Redirect"], + + [Status.BadRequest, "Bad Request"], + [Status.Unauthorized, "Unauthorized"], + [Status.PaymentRequired, "Payment Required"], + [Status.Forbidden, "Forbidden"], + [Status.NotFound, "Not Found"], + [Status.MethodNotAllowed, "Method Not Allowed"], + [Status.NotAcceptable, "Not Acceptable"], + [Status.ProxyAuthRequired, "Proxy Authentication Required"], + [Status.RequestTimeout, "Request Timeout"], + [Status.Conflict, "Conflict"], + [Status.Gone, "Gone"], + [Status.LengthRequired, "Length Required"], + [Status.PreconditionFailed, "Precondition Failed"], + [Status.RequestEntityTooLarge, "Request Entity Too Large"], + [Status.RequestURITooLong, "Request URI Too Long"], + [Status.UnsupportedMediaType, "Unsupported Media Type"], + [Status.RequestedRangeNotSatisfiable, "Requested Range Not Satisfiable"], + [Status.ExpectationFailed, "Expectation Failed"], + [Status.Teapot, "I'm a teapot"], + [Status.MisdirectedRequest, "Misdirected Request"], + [Status.UnprocessableEntity, "Unprocessable Entity"], + [Status.Locked, "Locked"], + [Status.FailedDependency, "Failed Dependency"], + [Status.UpgradeRequired, "Upgrade Required"], + [Status.PreconditionRequired, "Precondition Required"], + [Status.TooManyRequests, "Too Many Requests"], + [Status.RequestHeaderFieldsTooLarge, "Request Header Fields Too Large"], + [Status.UnavailableForLegalReasons, "Unavailable For Legal Reasons"], + + [Status.InternalServerError, "Internal Server Error"], + [Status.NotImplemented, "Not Implemented"], + [Status.BadGateway, "Bad Gateway"], + [Status.ServiceUnavailable, "Service Unavailable"], + [Status.GatewayTimeout, "Gateway Timeout"], + [Status.HTTPVersionNotSupported, "HTTP Version Not Supported"], + [Status.VariantAlsoNegotiates, "Variant Also Negotiates"], + [Status.InsufficientStorage, "Insufficient Storage"], + [Status.LoopDetected, "Loop Detected"], + [Status.NotExtended, "Not Extended"], + [Status.NetworkAuthenticationRequired, "Network Authentication Required"] +]); diff --git a/net/http_test.ts b/net/http_test.ts new file mode 100644 index 000000000..cdb7f8303 --- /dev/null +++ b/net/http_test.ts @@ -0,0 +1,58 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Ported from +// https://github.com/golang/go/blob/master/src/net/http/responsewrite_test.go + +import { + test, + assert, + assertEqual +} from "https://deno.land/x/testing/testing.ts"; + +import { + listenAndServe, + ServerRequest, + setContentLength, + Response +} from "./http"; +import { Buffer } from "deno"; +import { BufWriter } from "./bufio"; + +interface ResponseTest { + response: Response; + raw: string; +} + +const responseTests: ResponseTest[] = [ + // Default response + { + response: {}, + raw: "HTTP/1.1 200 OK\r\n" + "\r\n" + }, + // HTTP/1.1, chunked coding; empty trailer; close + { + response: { + status: 200, + body: new Buffer(new TextEncoder().encode("abcdef")) + }, + + raw: + "HTTP/1.1 200 OK\r\n" + + "transfer-encoding: chunked\r\n\r\n" + + "6\r\nabcdef\r\n0\r\n\r\n" + } +]; + +test(async function responseWrite() { + for (const testCase of responseTests) { + const buf = new Buffer(); + const bufw = new BufWriter(buf); + const request = new ServerRequest(); + request.w = bufw; + + await request.respond(testCase.response); + assertEqual(buf.toString(), testCase.raw); + } +}); diff --git a/net/iotest.ts b/net/iotest.ts new file mode 100644 index 000000000..e3a42f58a --- /dev/null +++ b/net/iotest.ts @@ -0,0 +1,61 @@ +// Ported to Deno from +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import { Reader, ReadResult } from "deno"; + +/** OneByteReader returns a Reader that implements + * each non-empty Read by reading one byte from r. + */ +export class OneByteReader implements Reader { + constructor(readonly r: Reader) {} + + async read(p: Uint8Array): Promise { + if (p.byteLength === 0) { + return { nread: 0, eof: false }; + } + if (!(p instanceof Uint8Array)) { + throw Error("expected Uint8Array"); + } + return this.r.read(p.subarray(0, 1)); + } +} + +/** HalfReader returns a Reader that implements Read + * by reading half as many requested bytes from r. + */ +export class HalfReader implements Reader { + constructor(readonly r: Reader) {} + + async read(p: Uint8Array): Promise { + if (!(p instanceof Uint8Array)) { + throw Error("expected Uint8Array"); + } + const half = Math.floor((p.byteLength + 1) / 2); + return this.r.read(p.subarray(0, half)); + } +} + +export class ErrTimeout extends Error { + constructor() { + super("timeout"); + this.name = "ErrTimeout"; + } +} + +/** TimeoutReader returns ErrTimeout on the second read + * with no data. Subsequent calls to read succeed. + */ +export class TimeoutReader implements Reader { + count = 0; + constructor(readonly r: Reader) {} + + async read(p: Uint8Array): Promise { + this.count++; + if (this.count === 2) { + throw new ErrTimeout(); + } + return this.r.read(p); + } +} diff --git a/net/textproto.ts b/net/textproto.ts new file mode 100644 index 000000000..342d74b33 --- /dev/null +++ b/net/textproto.ts @@ -0,0 +1,149 @@ +// Based on https://github.com/golang/go/blob/891682/src/net/textproto/ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import { BufReader, BufState } from "./bufio.ts"; +import { charCode } from "./util.ts"; + +const asciiDecoder = new TextDecoder(); +function str(buf: Uint8Array): string { + if (buf == null) { + return ""; + } else { + return asciiDecoder.decode(buf); + } +} + +export class ProtocolError extends Error { + constructor(msg: string) { + super(msg); + this.name = "ProtocolError"; + } +} + +export class TextProtoReader { + constructor(readonly r: BufReader) {} + + /** 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]; + } + + /** ReadMIMEHeader reads a MIME-style header from r. + * The header is a sequence of possibly continued Key: Value lines + * ending in a blank line. + * The returned map m maps CanonicalMIMEHeaderKey(key) to a + * sequence of values in the same order encountered in the input. + * + * For example, consider this input: + * + * My-Key: Value 1 + * Long-Key: Even + * Longer Value + * My-Key: Value 2 + * + * Given that input, ReadMIMEHeader returns the map: + * + * map[string][]string{ + * "My-Key": {"Value 1", "Value 2"}, + * "Long-Key": {"Even Longer Value"}, + * } + */ + async readMIMEHeader(): Promise<[Headers, BufState]> { + 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(); + } + + [buf, err] = await this.r.peek(1); + if (err == null && (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]; + } + + // Key ends at first colon; should not have trailing spaces + // but they appear in the wild, violating specs, so we remove + // them if present. + let i = kv.indexOf(charCode(":")); + if (i < 0) { + throw new ProtocolError(`malformed MIME header line: ${str(kv)}`); + } + let endKey = i; + while (endKey > 0 && kv[endKey - 1] == charCode(" ")) { + endKey--; + } + + //let key = canonicalMIMEHeaderKey(kv.subarray(0, endKey)); + let key = str(kv.subarray(0, endKey)); + + // As per RFC 7230 field-name is a token, tokens consist of one or more chars. + // We could return a ProtocolError here, but better to be liberal in what we + // accept, so if we get an empty key, skip it. + if (key == "") { + continue; + } + + // Skip initial spaces in value. + i++; // skip colon + while ( + i < kv.byteLength && + (kv[i] == charCode(" ") || kv[i] == charCode("\t")) + ) { + i++; + } + let value = str(kv.subarray(i)); + + m.append(key, value); + + if (err != null) { + throw err; + } + } + } + + async readLineSlice(): Promise<[Uint8Array, BufState]> { + // this.closeDot(); + let line: null | Uint8Array; + while (true) { + let [l, more, err] = await this.r.readLine(); + if (err != null) { + return [null, err]; + } + // Avoid the copy if the first call produced a full line. + if (line == null && !more) { + return [l, null]; + } + line = append(line, l); + if (!more) { + break; + } + } + return [line, null]; + } +} + +export function append(a: Uint8Array, b: Uint8Array): Uint8Array { + if (a == null) { + return b; + } else { + const output = new Uint8Array(a.length + b.length); + output.set(a, 0); + output.set(b, a.length); + return output; + } +} diff --git a/net/textproto_test.ts b/net/textproto_test.ts new file mode 100644 index 000000000..25c12b0e8 --- /dev/null +++ b/net/textproto_test.ts @@ -0,0 +1,93 @@ +// Based on https://github.com/golang/go/blob/891682/src/net/textproto/ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import { BufReader } from "./bufio.ts"; +import { TextProtoReader, append } from "./textproto.ts"; +import { stringsReader } from "./util.ts"; +import { + test, + assert, + assertEqual +} from "https://deno.land/x/testing/testing.ts"; + +function reader(s: string): TextProtoReader { + return new TextProtoReader(new BufReader(stringsReader(s))); +} + +test(async function textprotoReader() { + let r = reader("line1\nline2\n"); + let [s, err] = await r.readLine(); + assertEqual(s, "line1"); + assert(err == null); + + [s, err] = await r.readLine(); + assertEqual(s, "line2"); + assert(err == null); + + [s, err] = await r.readLine(); + assertEqual(s, ""); + assert(err == "EOF"); +}); + +/* +test(async function textprotoReadMIMEHeader() { + let r = reader("my-key: Value 1 \r\nLong-key: Even \n Longer Value\r\nmy-Key: Value 2\r\n\n"); + let [m, err] = await r.readMIMEHeader(); + + console.log("Got headers", m.toString()); + want := MIMEHeader{ + "My-Key": {"Value 1", "Value 2"}, + "Long-Key": {"Even Longer Value"}, + } + if !reflect.DeepEqual(m, want) || err != nil { + t.Fatalf("ReadMIMEHeader: %v, %v; want %v", m, err, want) + } +}); +*/ + +test(async function textprotoReadMIMEHeaderSingle() { + let r = reader("Foo: bar\n\n"); + let [m, err] = await r.readMIMEHeader(); + assertEqual(m.get("Foo"), "bar"); + assert(!err); +}); + +// Test that we read slightly-bogus MIME headers seen in the wild, +// with spaces before colons, and spaces in keys. +test(async function textprotoReadMIMEHeaderNonCompliant() { + // Invalid HTTP response header as sent by an Axis security + // camera: (this is handled by IE, Firefox, Chrome, curl, etc.) + let r = reader( + "Foo: bar\r\n" + + "Content-Language: en\r\n" + + "SID : 0\r\n" + + "Audio Mode : None\r\n" + + "Privilege : 127\r\n\r\n" + ); + let [m, err] = await r.readMIMEHeader(); + console.log(m.toString()); + assert(!err); + /* + let want = MIMEHeader{ + "Foo": {"bar"}, + "Content-Language": {"en"}, + "Sid": {"0"}, + "Audio Mode": {"None"}, + "Privilege": {"127"}, + } + if !reflect.DeepEqual(m, want) || err != nil { + t.Fatalf("ReadMIMEHeader =\n%v, %v; want:\n%v", m, err, want) + } + */ +}); + +test(async function textprotoAppend() { + const enc = new TextEncoder(); + const dec = new TextDecoder(); + const u1 = enc.encode("Hello "); + const u2 = enc.encode("World"); + const joined = append(u1, u2); + assertEqual(dec.decode(joined), "Hello World"); +}); diff --git a/net/util.ts b/net/util.ts new file mode 100644 index 000000000..811940b4d --- /dev/null +++ b/net/util.ts @@ -0,0 +1,29 @@ +import { Buffer, Reader } from "deno"; + +export function assert(cond: boolean, msg = "assert") { + if (!cond) { + throw Error(msg); + } +} + +// `off` is the offset into `dst` where it will at which to begin writing values +// from `src`. +// Returns the number of bytes copied. +export function copyBytes(dst: Uint8Array, src: Uint8Array, off = 0): number { + const r = dst.byteLength - off; + if (src.byteLength > r) { + src = src.subarray(0, r); + } + dst.set(src, off); + return src.byteLength; +} + +export function charCode(s: string): number { + return s.charCodeAt(0); +} + +const encoder = new TextEncoder(); +export function stringsReader(s: string): Reader { + const ui8 = encoder.encode(s); + return new Buffer(ui8.buffer as ArrayBuffer); +} diff --git a/test.ts b/test.ts old mode 100644 new mode 100755 index 7c395663e..95003e0ad --- a/test.ts +++ b/test.ts @@ -1,13 +1,14 @@ +#!/usr/bin/env deno --allow-run --allow-net import { run } from "deno"; -import "./bufio_test.ts"; -import "./http_test.ts"; -import "./textproto_test.ts"; -import { runTests, completePromise } from "./file_server_test.ts"; +import "net/bufio_test.ts"; +import "net/http_test.ts"; +import "net/textproto_test.ts"; +import { runTests, completePromise } from "net/file_server_test.ts"; // file server test const fileServer = run({ - args: ["deno", "--allow-net", "file_server.ts", "."] + args: ["deno", "--allow-net", "net/file_server.ts", "."] }); // I am also too lazy to do this properly LOL runTests(new Promise(res => setTimeout(res, 5000))); @@ -15,5 +16,3 @@ runTests(new Promise(res => setTimeout(res, 5000))); await completePromise; fileServer.close(); })(); - -// TODO import "./http_test.ts"; diff --git a/textproto.ts b/textproto.ts deleted file mode 100644 index 342d74b33..000000000 --- a/textproto.ts +++ /dev/null @@ -1,149 +0,0 @@ -// Based on https://github.com/golang/go/blob/891682/src/net/textproto/ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -import { BufReader, BufState } from "./bufio.ts"; -import { charCode } from "./util.ts"; - -const asciiDecoder = new TextDecoder(); -function str(buf: Uint8Array): string { - if (buf == null) { - return ""; - } else { - return asciiDecoder.decode(buf); - } -} - -export class ProtocolError extends Error { - constructor(msg: string) { - super(msg); - this.name = "ProtocolError"; - } -} - -export class TextProtoReader { - constructor(readonly r: BufReader) {} - - /** 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]; - } - - /** ReadMIMEHeader reads a MIME-style header from r. - * The header is a sequence of possibly continued Key: Value lines - * ending in a blank line. - * The returned map m maps CanonicalMIMEHeaderKey(key) to a - * sequence of values in the same order encountered in the input. - * - * For example, consider this input: - * - * My-Key: Value 1 - * Long-Key: Even - * Longer Value - * My-Key: Value 2 - * - * Given that input, ReadMIMEHeader returns the map: - * - * map[string][]string{ - * "My-Key": {"Value 1", "Value 2"}, - * "Long-Key": {"Even Longer Value"}, - * } - */ - async readMIMEHeader(): Promise<[Headers, BufState]> { - 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(); - } - - [buf, err] = await this.r.peek(1); - if (err == null && (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]; - } - - // Key ends at first colon; should not have trailing spaces - // but they appear in the wild, violating specs, so we remove - // them if present. - let i = kv.indexOf(charCode(":")); - if (i < 0) { - throw new ProtocolError(`malformed MIME header line: ${str(kv)}`); - } - let endKey = i; - while (endKey > 0 && kv[endKey - 1] == charCode(" ")) { - endKey--; - } - - //let key = canonicalMIMEHeaderKey(kv.subarray(0, endKey)); - let key = str(kv.subarray(0, endKey)); - - // As per RFC 7230 field-name is a token, tokens consist of one or more chars. - // We could return a ProtocolError here, but better to be liberal in what we - // accept, so if we get an empty key, skip it. - if (key == "") { - continue; - } - - // Skip initial spaces in value. - i++; // skip colon - while ( - i < kv.byteLength && - (kv[i] == charCode(" ") || kv[i] == charCode("\t")) - ) { - i++; - } - let value = str(kv.subarray(i)); - - m.append(key, value); - - if (err != null) { - throw err; - } - } - } - - async readLineSlice(): Promise<[Uint8Array, BufState]> { - // this.closeDot(); - let line: null | Uint8Array; - while (true) { - let [l, more, err] = await this.r.readLine(); - if (err != null) { - return [null, err]; - } - // Avoid the copy if the first call produced a full line. - if (line == null && !more) { - return [l, null]; - } - line = append(line, l); - if (!more) { - break; - } - } - return [line, null]; - } -} - -export function append(a: Uint8Array, b: Uint8Array): Uint8Array { - if (a == null) { - return b; - } else { - const output = new Uint8Array(a.length + b.length); - output.set(a, 0); - output.set(b, a.length); - return output; - } -} diff --git a/textproto_test.ts b/textproto_test.ts deleted file mode 100644 index 25c12b0e8..000000000 --- a/textproto_test.ts +++ /dev/null @@ -1,93 +0,0 @@ -// Based on https://github.com/golang/go/blob/891682/src/net/textproto/ -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -import { BufReader } from "./bufio.ts"; -import { TextProtoReader, append } from "./textproto.ts"; -import { stringsReader } from "./util.ts"; -import { - test, - assert, - assertEqual -} from "https://deno.land/x/testing/testing.ts"; - -function reader(s: string): TextProtoReader { - return new TextProtoReader(new BufReader(stringsReader(s))); -} - -test(async function textprotoReader() { - let r = reader("line1\nline2\n"); - let [s, err] = await r.readLine(); - assertEqual(s, "line1"); - assert(err == null); - - [s, err] = await r.readLine(); - assertEqual(s, "line2"); - assert(err == null); - - [s, err] = await r.readLine(); - assertEqual(s, ""); - assert(err == "EOF"); -}); - -/* -test(async function textprotoReadMIMEHeader() { - let r = reader("my-key: Value 1 \r\nLong-key: Even \n Longer Value\r\nmy-Key: Value 2\r\n\n"); - let [m, err] = await r.readMIMEHeader(); - - console.log("Got headers", m.toString()); - want := MIMEHeader{ - "My-Key": {"Value 1", "Value 2"}, - "Long-Key": {"Even Longer Value"}, - } - if !reflect.DeepEqual(m, want) || err != nil { - t.Fatalf("ReadMIMEHeader: %v, %v; want %v", m, err, want) - } -}); -*/ - -test(async function textprotoReadMIMEHeaderSingle() { - let r = reader("Foo: bar\n\n"); - let [m, err] = await r.readMIMEHeader(); - assertEqual(m.get("Foo"), "bar"); - assert(!err); -}); - -// Test that we read slightly-bogus MIME headers seen in the wild, -// with spaces before colons, and spaces in keys. -test(async function textprotoReadMIMEHeaderNonCompliant() { - // Invalid HTTP response header as sent by an Axis security - // camera: (this is handled by IE, Firefox, Chrome, curl, etc.) - let r = reader( - "Foo: bar\r\n" + - "Content-Language: en\r\n" + - "SID : 0\r\n" + - "Audio Mode : None\r\n" + - "Privilege : 127\r\n\r\n" - ); - let [m, err] = await r.readMIMEHeader(); - console.log(m.toString()); - assert(!err); - /* - let want = MIMEHeader{ - "Foo": {"bar"}, - "Content-Language": {"en"}, - "Sid": {"0"}, - "Audio Mode": {"None"}, - "Privilege": {"127"}, - } - if !reflect.DeepEqual(m, want) || err != nil { - t.Fatalf("ReadMIMEHeader =\n%v, %v; want:\n%v", m, err, want) - } - */ -}); - -test(async function textprotoAppend() { - const enc = new TextEncoder(); - const dec = new TextDecoder(); - const u1 = enc.encode("Hello "); - const u2 = enc.encode("World"); - const joined = append(u1, u2); - assertEqual(dec.decode(joined), "Hello World"); -}); diff --git a/util.ts b/util.ts deleted file mode 100644 index 811940b4d..000000000 --- a/util.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Buffer, Reader } from "deno"; - -export function assert(cond: boolean, msg = "assert") { - if (!cond) { - throw Error(msg); - } -} - -// `off` is the offset into `dst` where it will at which to begin writing values -// from `src`. -// Returns the number of bytes copied. -export function copyBytes(dst: Uint8Array, src: Uint8Array, off = 0): number { - const r = dst.byteLength - off; - if (src.byteLength > r) { - src = src.subarray(0, r); - } - dst.set(src, off); - return src.byteLength; -} - -export function charCode(s: string): number { - return s.charCodeAt(0); -} - -const encoder = new TextEncoder(); -export function stringsReader(s: string): Reader { - const ui8 = encoder.encode(s); - return new Buffer(ui8.buffer as ArrayBuffer); -} -- cgit v1.2.3