diff options
Diffstat (limited to 'std/io')
m--------- | std | 0 | ||||
-rw-r--r-- | std/io/bufio.ts | 509 | ||||
-rw-r--r-- | std/io/bufio_test.ts | 381 | ||||
-rw-r--r-- | std/io/iotest.ts | 60 | ||||
-rw-r--r-- | std/io/ioutil.ts | 79 | ||||
-rw-r--r-- | std/io/ioutil_test.ts | 88 | ||||
-rw-r--r-- | std/io/readers.ts | 42 | ||||
-rw-r--r-- | std/io/readers_test.ts | 39 | ||||
-rw-r--r-- | std/io/util.ts | 45 | ||||
-rw-r--r-- | std/io/util_test.ts | 51 | ||||
-rw-r--r-- | std/io/writers.ts | 37 | ||||
-rw-r--r-- | std/io/writers_test.ts | 15 |
12 files changed, 1346 insertions, 0 deletions
diff --git a/std b/std deleted file mode 160000 -Subproject 43aafbf33285753e7b42230f0eb7969b300f71c diff --git a/std/io/bufio.ts b/std/io/bufio.ts new file mode 100644 index 000000000..213870c3c --- /dev/null +++ b/std/io/bufio.ts @@ -0,0 +1,509 @@ +// 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. + +type Reader = Deno.Reader; +type Writer = Deno.Writer; +import { charCode, copyBytes } from "./util.ts"; +import { assert } from "../testing/asserts.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 class BufferFullError extends Error { + name = "BufferFullError"; + constructor(public partial: Uint8Array) { + super("Buffer full"); + } +} + +export class UnexpectedEOFError extends Error { + name = "UnexpectedEOFError"; + constructor() { + super("Unexpected EOF"); + } +} + +/** Result type returned by of BufReader.readLine(). */ +export interface ReadLineResult { + line: Uint8Array; + more: boolean; +} + +/** BufReader implements buffering for a Reader object. */ +export class BufReader implements Reader { + private buf!: Uint8Array; + private rd!: Reader; // Reader provided by caller. + private r = 0; // buf read position. + private w = 0; // buf write position. + private eof = false; + // private lastByte: number; + // private lastCharSize: number; + + /** return new BufReader unless r is BufReader */ + static create(r: Reader, size: number = DEFAULT_BUF_SIZE): BufReader { + return r instanceof BufReader ? r : new BufReader(r, size); + } + + constructor(rd: Reader, size: number = 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; + } + + // Reads a new chunk into the buffer. + private async _fill(): Promise<void> { + // 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--) { + const rr = await this.rd.read(this.buf.subarray(this.w)); + if (rr === Deno.EOF) { + this.eof = true; + return; + } + assert(rr >= 0, "negative read"); + this.w += rr; + if (rr > 0) { + return; + } + } + + throw new Error( + `No progress after ${MAX_CONSECUTIVE_EMPTY_READS} read() calls` + ); + } + + /** 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.eof = false; + // this.lastByte = -1; + // this.lastCharSize = -1; + } + + /** reads data into p. + * It returns the number of bytes read into p. + * The bytes are taken from at most one Read on the underlying Reader, + * hence n may be less than len(p). + * To read exactly len(p) bytes, use io.ReadFull(b, p). + */ + async read(p: Uint8Array): Promise<number | Deno.EOF> { + let rr: number | Deno.EOF = p.byteLength; + if (p.byteLength === 0) return rr; + + if (this.r === this.w) { + if (p.byteLength >= this.buf.byteLength) { + // Large read, empty buffer. + // Read directly into p to avoid copy. + const rr = await this.rd.read(p); + const nread = rr === Deno.EOF ? 0 : rr; + assert(nread >= 0, "negative read"); + // if (rr.nread > 0) { + // this.lastByte = p[rr.nread - 1]; + // this.lastCharSize = -1; + // } + return rr; + } + + // One read. + // Do not use this.fill, which will loop. + this.r = 0; + this.w = 0; + rr = await this.rd.read(this.buf); + if (rr === 0 || rr === Deno.EOF) return rr; + assert(rr >= 0, "negative read"); + this.w += rr; + } + + // copy as much as we can + const copied = copyBytes(p, this.buf.subarray(this.r, this.w), 0); + this.r += copied; + // this.lastByte = this.buf[this.r - 1]; + // this.lastCharSize = -1; + return copied; + } + + /** reads exactly `p.length` bytes into `p`. + * + * If successful, `p` is returned. + * + * If the end of the underlying stream has been reached, and there are no more + * bytes available in the buffer, `readFull()` returns `EOF` instead. + * + * An error is thrown if some bytes could be read, but not enough to fill `p` + * entirely before the underlying stream reported an error or EOF. Any error + * thrown will have a `partial` property that indicates the slice of the + * buffer that has been successfully filled with data. + * + * Ported from https://golang.org/pkg/io/#ReadFull + */ + async readFull(p: Uint8Array): Promise<Uint8Array | Deno.EOF> { + let bytesRead = 0; + while (bytesRead < p.length) { + try { + const rr = await this.read(p.subarray(bytesRead)); + if (rr === Deno.EOF) { + if (bytesRead === 0) { + return Deno.EOF; + } else { + throw new UnexpectedEOFError(); + } + } + bytesRead += rr; + } catch (err) { + err.partial = p.subarray(0, bytesRead); + throw err; + } + } + return p; + } + + /** Returns the next byte [0, 255] or `EOF`. */ + async readByte(): Promise<number | Deno.EOF> { + while (this.r === this.w) { + if (this.eof) return Deno.EOF; + await this._fill(); // buffer is empty. + } + 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<string | Deno.EOF> { + if (delim.length !== 1) + throw new Error("Delimiter should be a single character"); + const buffer = await this.readSlice(delim.charCodeAt(0)); + return new TextDecoder().decode(buffer || undefined); + } + + /** `readLine()` is a low-level line-reading primitive. Most callers should + * use `readString('\n')` instead or use a Scanner. + * + * `readLine()` tries to return a single line, not including the end-of-line + * bytes. If the line was too long for the buffer then `more` is set and the + * beginning of the line is returned. The rest of the line will be returned + * from future calls. `more` will be false when returning the last fragment + * of the line. The returned buffer is only valid until the next call to + * `readLine()`. + * + * The text returned from ReadLine does not include the line end ("\r\n" or + * "\n"). + * + * When the end of the underlying stream is reached, the final bytes in the + * stream are returned. No indication or error is given if the input ends + * without a final line end. When there are no more trailing bytes to read, + * `readLine()` returns the `EOF` symbol. + * + * Calling `unreadByte()` after `readLine()` will always unread the last byte + * read (possibly a character belonging to the line end) even if that byte is + * not part of the line returned by `readLine()`. + */ + async readLine(): Promise<ReadLineResult | Deno.EOF> { + let line: Uint8Array | Deno.EOF; + + try { + line = await this.readSlice(LF); + } catch (err) { + let { partial } = err; + assert( + partial instanceof Uint8Array, + "bufio: caught error from `readSlice()` without `partial` property" + ); + + // Don't throw if `readSlice()` failed with `BufferFullError`, instead we + // just return whatever is available and set the `more` flag. + if (!(err instanceof BufferFullError)) { + throw err; + } + + // Handle the case where "\r\n" straddles the buffer. + if ( + !this.eof && + partial.byteLength > 0 && + partial[partial.byteLength - 1] === CR + ) { + // Put the '\r' back on buf and drop it from line. + // Let the next call to ReadLine check for "\r\n". + assert(this.r > 0, "bufio: tried to rewind past start of buffer"); + this.r--; + partial = partial.subarray(0, partial.byteLength - 1); + } + + return { line: partial, more: !this.eof }; + } + + if (line === Deno.EOF) { + return Deno.EOF; + } + + if (line.byteLength === 0) { + return { line, more: false }; + } + + 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, more: false }; + } + + /** `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, or the + * buffer fills without finding a delimiter, it throws an error with a + * `partial` property that contains the entire buffer. + * + * If `readSlice()` encounters the end of the underlying stream and there are + * any bytes left in the buffer, the rest of the buffer is returned. In other + * words, EOF is always treated as a delimiter. Once the buffer is empty, + * it returns `EOF`. + * + * Because the data returned from `readSlice()` will be overwritten by the + * next I/O operation, most clients should use `readString()` instead. + */ + async readSlice(delim: number): Promise<Uint8Array | Deno.EOF> { + let s = 0; // search start index + let slice: Uint8Array; + + while (true) { + // Search buffer. + let i = this.buf.subarray(this.r + s, this.w).indexOf(delim); + if (i >= 0) { + i += s; + slice = this.buf.subarray(this.r, this.r + i + 1); + this.r += i + 1; + break; + } + + // EOF? + if (this.eof) { + if (this.r === this.w) { + return Deno.EOF; + } + slice = this.buf.subarray(this.r, this.w); + this.r = this.w; + break; + } + + // Buffer full? + if (this.buffered() >= this.buf.byteLength) { + this.r = this.w; + throw new BufferFullError(this.buf); + } + + s = this.w - this.r; // do not rescan area we scanned before + + // Buffer is not full. + try { + await this._fill(); + } catch (err) { + err.partial = slice!; + throw err; + } + } + + // Handle last byte, if any. + // const i = slice.byteLength - 1; + // if (i >= 0) { + // this.lastByte = slice[i]; + // this.lastCharSize = -1 + // } + + return slice; + } + + /** `peek()` returns the next `n` bytes without advancing the reader. The + * bytes stop being valid at the next read call. + * + * When the end of the underlying stream is reached, but there are unread + * bytes left in the buffer, those bytes are returned. If there are no bytes + * left in the buffer, it returns `EOF`. + * + * If an error is encountered before `n` bytes are available, `peek()` throws + * an error with the `partial` property set to a slice of the buffer that + * contains the bytes that were available before the error occurred. + */ + async peek(n: number): Promise<Uint8Array | Deno.EOF> { + if (n < 0) { + throw Error("negative count"); + } + + let avail = this.w - this.r; + while (avail < n && avail < this.buf.byteLength && !this.eof) { + try { + await this._fill(); + } catch (err) { + err.partial = this.buf.subarray(this.r, this.w); + throw err; + } + avail = this.w - this.r; + } + + if (avail === 0 && this.eof) { + return Deno.EOF; + } else if (avail < n && this.eof) { + return this.buf.subarray(this.r, this.r + avail); + } else if (avail < n) { + throw new BufferFullError(this.buf.subarray(this.r, this.w)); + } + + return this.buf.subarray(this.r, this.r + n); + } +} + +/** 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 = 0; + err: Error | null = null; + + /** return new BufWriter unless w is BufWriter */ + static create(w: Writer, size: number = DEFAULT_BUF_SIZE): BufWriter { + return w instanceof BufWriter ? w : new BufWriter(w, size); + } + + constructor(private wr: Writer, size: number = 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<void> { + if (this.err !== null) throw this.err; + if (this.n === 0) return; + + let n = 0; + try { + n = await this.wr.write(this.buf.subarray(0, this.n)); + } catch (e) { + this.err = e; + throw e; + } + + if (n < this.n) { + if (n > 0) { + this.buf.copyWithin(0, n, this.n); + this.n -= n; + } + this.err = new Error("Short write"); + throw this.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<number> { + if (this.err !== null) throw this.err; + if (p.length === 0) return 0; + + let nn = 0; + let n = 0; + while (p.byteLength > this.available()) { + if (this.buffered() === 0) { + // Large write, empty buffer. + // Write directly from p to avoid copy. + try { + n = await this.wr.write(p); + } catch (e) { + this.err = e; + throw e; + } + } else { + n = copyBytes(this.buf, p, this.n); + this.n += n; + await this.flush(); + } + nn += n; + p = p.subarray(n); + } + + n = copyBytes(this.buf, p, this.n); + this.n += n; + nn += n; + return nn; + } +} diff --git a/std/io/bufio_test.ts b/std/io/bufio_test.ts new file mode 100644 index 000000000..75664694a --- /dev/null +++ b/std/io/bufio_test.ts @@ -0,0 +1,381 @@ +// 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. + +const { Buffer } = Deno; +type Reader = Deno.Reader; +import { test, runIfMain } from "../testing/mod.ts"; +import { + assert, + assertEquals, + assertNotEquals, + fail +} from "../testing/asserts.ts"; +import { + BufReader, + BufWriter, + BufferFullError, + UnexpectedEOFError +} from "./bufio.ts"; +import * as iotest from "./iotest.ts"; +import { charCode, copyBytes, stringsReader } from "./util.ts"; + +const encoder = new TextEncoder(); + +function assertNotEOF<T extends {}>(val: T | Deno.EOF): T { + assertNotEquals(val, Deno.EOF); + return val as T; +} + +async function readBytes(buf: BufReader): Promise<string> { + const b = new Uint8Array(1000); + let nb = 0; + while (true) { + const c = await buf.readByte(); + if (c === Deno.EOF) { + break; // EOF + } + b[nb] = c; + nb++; + } + const decoder = new TextDecoder(); + return decoder.decode(b.subarray(0, nb)); +} + +test(async function bufioReaderSimple(): Promise<void> { + const data = "hello world"; + const b = new BufReader(stringsReader(data)); + const s = await readBytes(b); + assertEquals(s, data); +}); + +interface ReadMaker { + name: string; + fn: (r: Reader) => Reader; +} + +const readMakers: ReadMaker[] = [ + { name: "full", fn: (r): Reader => r }, + { + name: "byte", + fn: (r): iotest.OneByteReader => new iotest.OneByteReader(r) + }, + { name: "half", fn: (r): iotest.HalfReader => new iotest.HalfReader(r) } + // TODO { name: "data+err", r => new iotest.DataErrReader(r) }, + // { name: "timeout", fn: r => new iotest.TimeoutReader(r) }, +]; + +// Call read to accumulate the text of a file +async function reads(buf: BufReader, m: number): Promise<string> { + const b = new Uint8Array(1000); + let nb = 0; + while (true) { + const result = await buf.read(b.subarray(nb, nb + m)); + if (result === Deno.EOF) { + break; + } + nb += result; + } + const decoder = new TextDecoder(); + return decoder.decode(b.subarray(0, nb)); +} + +interface NamedBufReader { + name: string; + fn: (r: BufReader) => Promise<string>; +} + +const bufreaders: NamedBufReader[] = [ + { name: "1", fn: (b: BufReader): Promise<string> => reads(b, 1) }, + { name: "2", fn: (b: BufReader): Promise<string> => reads(b, 2) }, + { name: "3", fn: (b: BufReader): Promise<string> => reads(b, 3) }, + { name: "4", fn: (b: BufReader): Promise<string> => reads(b, 4) }, + { name: "5", fn: (b: BufReader): Promise<string> => reads(b, 5) }, + { name: "7", fn: (b: BufReader): Promise<string> => 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(): Promise<void> { + const texts = new Array<string>(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 (const text of texts) { + for (const readmaker of readMakers) { + for (const bufreader of bufreaders) { + for (const 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}`; + assertEquals(s, text, debugStr); + } + } + } + } +}); + +test(async function bufioBufferFull(): Promise<void> { + 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); + const decoder = new TextDecoder(); + + try { + await buf.readSlice(charCode("!")); + fail("readSlice should throw"); + } catch (err) { + assert(err instanceof BufferFullError); + assert(err.partial instanceof Uint8Array); + assertEquals(decoder.decode(err.partial), "And now, hello, "); + } + + const line = assertNotEOF(await buf.readSlice(charCode("!"))); + const actual = decoder.decode(line); + assertEquals(actual, "world!"); +}); + +test(async function bufioReadString(): Promise<void> { + const string = "And now, hello, world!"; + const buf = new BufReader(stringsReader(string), MIN_READ_BUFFER_SIZE); + + const line = assertNotEOF(await buf.readString(",")); + assertEquals(line, "And now,"); + assertEquals(line.length, 8); + + try { + await buf.readString("deno"); + + fail("should throw"); + } catch (err) { + assert(err.message, "Delimiter should be a single character"); + } +}); + +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\n" + + "uvw\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<number | Deno.EOF> { + let nread = this.stride; + if (nread > this.data.byteLength) { + nread = this.data.byteLength; + } + if (nread > buf.byteLength) { + nread = buf.byteLength; + } + if (nread === 0) { + return Deno.EOF; + } + copyBytes(buf as Uint8Array, this.data); + this.data = this.data.subarray(nread); + return nread; + } +} + +async function testReadLine(input: Uint8Array): Promise<void> { + for (let stride = 1; stride < 2; stride++) { + let done = 0; + const reader = new TestReader(input, stride); + const l = new BufReader(reader, input.byteLength + 1); + while (true) { + const r = await l.readLine(); + if (r === Deno.EOF) { + break; + } + const { line, more } = r; + assertEquals(more, false); + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + const want = testOutput.subarray(done, done + line.byteLength); + assertEquals( + line, + want, + `Bad line at stride ${stride}: want: ${want} got: ${line}` + ); + done += line.byteLength; + } + assertEquals( + done, + testOutput.byteLength, + `readLine didn't return everything: got: ${done}, ` + + `want: ${testOutput} (stride: ${stride})` + ); + } +} + +test(async function bufioReadLine(): Promise<void> { + await testReadLine(testInput); + await testReadLine(testInputrn); +}); + +test(async function bufioPeek(): Promise<void> { + const decoder = new TextDecoder(); + const p = new Uint8Array(10); + // string is 16 (minReadBufferSize) long. + const buf = new BufReader( + stringsReader("abcdefghijklmnop"), + MIN_READ_BUFFER_SIZE + ); + + let actual = assertNotEOF(await buf.peek(1)); + assertEquals(decoder.decode(actual), "a"); + + actual = assertNotEOF(await buf.peek(4)); + assertEquals(decoder.decode(actual), "abcd"); + + try { + await buf.peek(32); + fail("peek() should throw"); + } catch (err) { + assert(err instanceof BufferFullError); + assert(err.partial instanceof Uint8Array); + assertEquals(decoder.decode(err.partial), "abcdefghijklmnop"); + } + + await buf.read(p.subarray(0, 3)); + assertEquals(decoder.decode(p.subarray(0, 3)), "abc"); + + actual = assertNotEOF(await buf.peek(1)); + assertEquals(decoder.decode(actual), "d"); + + actual = assertNotEOF(await buf.peek(1)); + assertEquals(decoder.decode(actual), "d"); + + actual = assertNotEOF(await buf.peek(1)); + assertEquals(decoder.decode(actual), "d"); + + actual = assertNotEOF(await buf.peek(2)); + assertEquals(decoder.decode(actual), "de"); + + const res = await buf.read(p.subarray(0, 3)); + assertEquals(decoder.decode(p.subarray(0, 3)), "def"); + assert(res !== Deno.EOF); + + actual = assertNotEOF(await buf.peek(4)); + assertEquals(decoder.decode(actual), "ghij"); + + await buf.read(p); + assertEquals(decoder.decode(p), "ghijklmnop"); + + actual = assertNotEOF(await buf.peek(0)); + assertEquals(decoder.decode(actual), ""); + + const r = await buf.peek(1); + assert(r === Deno.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(): Promise<void> { + const data = new Uint8Array(8192); + + for (let i = 0; i < data.byteLength; i++) { + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + data[i] = charCode(" ") + (i % (charCode("~") - charCode(" "))); + } + + const w = new Buffer(); + for (const nwrite of bufsizes) { + for (const 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)); + assertEquals(n, nwrite, context); + + await buf.flush(); + + const written = w.bytes(); + assertEquals(written.byteLength, nwrite); + + for (let l = 0; l < written.byteLength; l++) { + assertEquals(written[l], data[l]); + } + } + } +}); + +test(async function bufReaderReadFull(): Promise<void> { + 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 r = assertNotEOF(await bufr.readFull(buf)); + assertEquals(r, buf); + assertEquals(dec.decode(buf), "Hello "); + } + { + const buf = new Uint8Array(6); + try { + await bufr.readFull(buf); + fail("readFull() should throw"); + } catch (err) { + assert(err instanceof UnexpectedEOFError); + assert(err.partial instanceof Uint8Array); + assertEquals(err.partial.length, 5); + assertEquals(dec.decode(buf.subarray(0, 5)), "World"); + } + } +}); + +runIfMain(import.meta); diff --git a/std/io/iotest.ts b/std/io/iotest.ts new file mode 100644 index 000000000..8d2cee6e2 --- /dev/null +++ b/std/io/iotest.ts @@ -0,0 +1,60 @@ +// 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. +type Reader = Deno.Reader; + +/** 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<number | Deno.EOF> { + if (p.byteLength === 0) { + return 0; + } + 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<number | Deno.EOF> { + 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<number | Deno.EOF> { + this.count++; + if (this.count === 2) { + throw new ErrTimeout(); + } + return this.r.read(p); + } +} diff --git a/std/io/ioutil.ts b/std/io/ioutil.ts new file mode 100644 index 000000000..f1ca54e14 --- /dev/null +++ b/std/io/ioutil.ts @@ -0,0 +1,79 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { BufReader, UnexpectedEOFError } from "./bufio.ts"; +type Reader = Deno.Reader; +type Writer = Deno.Writer; +import { assert } from "../testing/asserts.ts"; + +/** copy N size at the most. + * If read size is lesser than N, then returns nread + * */ +export async function copyN( + dest: Writer, + r: Reader, + size: number +): Promise<number> { + let bytesRead = 0; + let buf = new Uint8Array(1024); + while (bytesRead < size) { + if (size - bytesRead < 1024) { + buf = new Uint8Array(size - bytesRead); + } + const result = await r.read(buf); + const nread = result === Deno.EOF ? 0 : result; + bytesRead += nread; + if (nread > 0) { + const n = await dest.write(buf.slice(0, nread)); + assert(n === nread, "could not write"); + } + if (result === Deno.EOF) { + break; + } + } + return bytesRead; +} + +/** Read big endian 16bit short from BufReader */ +export async function readShort(buf: BufReader): Promise<number | Deno.EOF> { + const high = await buf.readByte(); + if (high === Deno.EOF) return Deno.EOF; + const low = await buf.readByte(); + if (low === Deno.EOF) throw new UnexpectedEOFError(); + return (high << 8) | low; +} + +/** Read big endian 32bit integer from BufReader */ +export async function readInt(buf: BufReader): Promise<number | Deno.EOF> { + const high = await readShort(buf); + if (high === Deno.EOF) return Deno.EOF; + const low = await readShort(buf); + if (low === Deno.EOF) throw new UnexpectedEOFError(); + return (high << 16) | low; +} + +const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER); + +/** Read big endian 64bit long from BufReader */ +export async function readLong(buf: BufReader): Promise<number | Deno.EOF> { + const high = await readInt(buf); + if (high === Deno.EOF) return Deno.EOF; + const low = await readInt(buf); + if (low === Deno.EOF) throw new UnexpectedEOFError(); + const big = (BigInt(high) << 32n) | BigInt(low); + // We probably should provide a similar API that returns BigInt values. + if (big > MAX_SAFE_INTEGER) { + throw new RangeError( + "Long value too big to be represented as a Javascript number." + ); + } + return Number(big); +} + +/** Slice number into 64bit big endian byte array */ +export function sliceLongToBytes(d: number, dest = new Array(8)): number[] { + let big = BigInt(d); + for (let i = 0; i < 8; i++) { + dest[7 - i] = Number(big & 0xffn); + big >>= 8n; + } + return dest; +} diff --git a/std/io/ioutil_test.ts b/std/io/ioutil_test.ts new file mode 100644 index 000000000..97ab244c0 --- /dev/null +++ b/std/io/ioutil_test.ts @@ -0,0 +1,88 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +const { Buffer } = Deno; +type Reader = Deno.Reader; +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import { + copyN, + readInt, + readLong, + readShort, + sliceLongToBytes +} from "./ioutil.ts"; +import { BufReader } from "./bufio.ts"; +import { stringsReader } from "./util.ts"; + +class BinaryReader implements Reader { + index = 0; + + constructor(private bytes: Uint8Array = new Uint8Array(0)) {} + + async read(p: Uint8Array): Promise<number | Deno.EOF> { + p.set(this.bytes.subarray(this.index, p.byteLength)); + this.index += p.byteLength; + return p.byteLength; + } +} + +test(async function testReadShort(): Promise<void> { + const r = new BinaryReader(new Uint8Array([0x12, 0x34])); + const short = await readShort(new BufReader(r)); + assertEquals(short, 0x1234); +}); + +test(async function testReadInt(): Promise<void> { + const r = new BinaryReader(new Uint8Array([0x12, 0x34, 0x56, 0x78])); + const int = await readInt(new BufReader(r)); + assertEquals(int, 0x12345678); +}); + +test(async function testReadLong(): Promise<void> { + const r = new BinaryReader( + new Uint8Array([0x00, 0x00, 0x00, 0x78, 0x12, 0x34, 0x56, 0x78]) + ); + const long = await readLong(new BufReader(r)); + assertEquals(long, 0x7812345678); +}); + +test(async function testReadLong2(): Promise<void> { + const r = new BinaryReader( + new Uint8Array([0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78]) + ); + const long = await readLong(new BufReader(r)); + assertEquals(long, 0x12345678); +}); + +test(async function testSliceLongToBytes(): Promise<void> { + const arr = sliceLongToBytes(0x1234567890abcdef); + const actual = readLong(new BufReader(new BinaryReader(new Uint8Array(arr)))); + const expected = readLong( + new BufReader( + new BinaryReader( + new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef]) + ) + ) + ); + assertEquals(actual, expected); +}); + +test(async function testSliceLongToBytes2(): Promise<void> { + const arr = sliceLongToBytes(0x12345678); + assertEquals(arr, [0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78]); +}); + +test(async function testCopyN1(): Promise<void> { + const w = new Buffer(); + const r = stringsReader("abcdefghij"); + const n = await copyN(w, r, 3); + assertEquals(n, 3); + assertEquals(w.toString(), "abc"); +}); + +test(async function testCopyN2(): Promise<void> { + const w = new Buffer(); + const r = stringsReader("abcdefghij"); + const n = await copyN(w, r, 11); + assertEquals(n, 10); + assertEquals(w.toString(), "abcdefghij"); +}); diff --git a/std/io/readers.ts b/std/io/readers.ts new file mode 100644 index 000000000..0208de413 --- /dev/null +++ b/std/io/readers.ts @@ -0,0 +1,42 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +type Reader = Deno.Reader; +import { encode } from "../strings/mod.ts"; + +/** Reader utility for strings */ +export class StringReader implements Reader { + private offs = 0; + private buf = new Uint8Array(encode(this.s)); + + constructor(private readonly s: string) {} + + async read(p: Uint8Array): Promise<number | Deno.EOF> { + const n = Math.min(p.byteLength, this.buf.byteLength - this.offs); + p.set(this.buf.slice(this.offs, this.offs + n)); + this.offs += n; + if (n === 0) { + return Deno.EOF; + } + return n; + } +} + +/** Reader utility for combining multiple readers */ +export class MultiReader implements Reader { + private readonly readers: Reader[]; + private currentIndex = 0; + + constructor(...readers: Reader[]) { + this.readers = readers; + } + + async read(p: Uint8Array): Promise<number | Deno.EOF> { + const r = this.readers[this.currentIndex]; + if (!r) return Deno.EOF; + const result = await r.read(p); + if (result === Deno.EOF) { + this.currentIndex++; + return 0; + } + return result; + } +} diff --git a/std/io/readers_test.ts b/std/io/readers_test.ts new file mode 100644 index 000000000..474407427 --- /dev/null +++ b/std/io/readers_test.ts @@ -0,0 +1,39 @@ +const { copy } = Deno; +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import { MultiReader, StringReader } from "./readers.ts"; +import { StringWriter } from "./writers.ts"; +import { copyN } from "./ioutil.ts"; +import { decode } from "../strings/mod.ts"; + +test(async function ioStringReader(): Promise<void> { + const r = new StringReader("abcdef"); + const res0 = await r.read(new Uint8Array(6)); + assertEquals(res0, 6); + const res1 = await r.read(new Uint8Array(6)); + assertEquals(res1, Deno.EOF); +}); + +test(async function ioStringReader(): Promise<void> { + const r = new StringReader("abcdef"); + const buf = new Uint8Array(3); + const res1 = await r.read(buf); + assertEquals(res1, 3); + assertEquals(decode(buf), "abc"); + const res2 = await r.read(buf); + assertEquals(res2, 3); + assertEquals(decode(buf), "def"); + const res3 = await r.read(buf); + assertEquals(res3, Deno.EOF); + assertEquals(decode(buf), "def"); +}); + +test(async function ioMultiReader(): Promise<void> { + const r = new MultiReader(new StringReader("abc"), new StringReader("def")); + const w = new StringWriter(); + const n = await copyN(w, r, 4); + assertEquals(n, 4); + assertEquals(w.toString(), "abcd"); + await copy(w, r); + assertEquals(w.toString(), "abcdef"); +}); diff --git a/std/io/util.ts b/std/io/util.ts new file mode 100644 index 000000000..96ff10b0e --- /dev/null +++ b/std/io/util.ts @@ -0,0 +1,45 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +const { Buffer, mkdir, open } = Deno; +type File = Deno.File; +type Reader = Deno.Reader; +import { encode } from "../strings/mod.ts"; +import * as path from "../fs/path.ts"; +// `off` is the offset into `dst` where it will at which to begin writing values +// from `src`. +// Returns the number of bytes copied. +export function copyBytes(dst: Uint8Array, src: Uint8Array, off = 0): number { + off = Math.max(0, Math.min(off, dst.byteLength)); + 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); +} + +export function stringsReader(s: string): Reader { + return new Buffer(encode(s).buffer); +} + +/** Create or open a temporal file at specified directory with prefix and + * postfix + * */ +export async function tempFile( + dir: string, + opts: { + prefix?: string; + postfix?: string; + } = { prefix: "", postfix: "" } +): Promise<{ file: File; filepath: string }> { + const r = Math.floor(Math.random() * 1000000); + const filepath = path.resolve( + `${dir}/${opts.prefix || ""}${r}${opts.postfix || ""}` + ); + await mkdir(path.dirname(filepath), true); + const file = await open(filepath, "a"); + return { file, filepath }; +} diff --git a/std/io/util_test.ts b/std/io/util_test.ts new file mode 100644 index 000000000..c616a4bba --- /dev/null +++ b/std/io/util_test.ts @@ -0,0 +1,51 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +const { remove } = Deno; +import { test } from "../testing/mod.ts"; +import { assert, assertEquals } from "../testing/asserts.ts"; +import { copyBytes, tempFile } from "./util.ts"; +import * as path from "../fs/path.ts"; + +test(function testCopyBytes(): void { + const dst = new Uint8Array(4); + + dst.fill(0); + let src = Uint8Array.of(1, 2); + let len = copyBytes(dst, src, 0); + assert(len === 2); + assertEquals(dst, Uint8Array.of(1, 2, 0, 0)); + + dst.fill(0); + src = Uint8Array.of(1, 2); + len = copyBytes(dst, src, 1); + assert(len === 2); + assertEquals(dst, Uint8Array.of(0, 1, 2, 0)); + + dst.fill(0); + src = Uint8Array.of(1, 2, 3, 4, 5); + len = copyBytes(dst, src); + assert(len === 4); + assertEquals(dst, Uint8Array.of(1, 2, 3, 4)); + + dst.fill(0); + src = Uint8Array.of(1, 2); + len = copyBytes(dst, src, 100); + assert(len === 0); + assertEquals(dst, Uint8Array.of(0, 0, 0, 0)); + + dst.fill(0); + src = Uint8Array.of(3, 4); + len = copyBytes(dst, src, -2); + assert(len === 2); + assertEquals(dst, Uint8Array.of(3, 4, 0, 0)); +}); + +test(async function ioTempfile(): Promise<void> { + const f = await tempFile(".", { + prefix: "prefix-", + postfix: "-postfix" + }); + console.log(f.file, f.filepath); + const base = path.basename(f.filepath); + assert(!!base.match(/^prefix-.+?-postfix$/)); + await remove(f.filepath); +}); diff --git a/std/io/writers.ts b/std/io/writers.ts new file mode 100644 index 000000000..91d19a9d7 --- /dev/null +++ b/std/io/writers.ts @@ -0,0 +1,37 @@ +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +type Writer = Deno.Writer; +import { decode, encode } from "../strings/mod.ts"; + +/** Writer utility for buffering string chunks */ +export class StringWriter implements Writer { + private chunks: Uint8Array[] = []; + private byteLength = 0; + private cache: string | undefined; + + constructor(private base: string = "") { + const c = encode(base); + this.chunks.push(c); + this.byteLength += c.byteLength; + } + + async write(p: Uint8Array): Promise<number> { + this.chunks.push(p); + this.byteLength += p.byteLength; + this.cache = undefined; + return p.byteLength; + } + + toString(): string { + if (this.cache) { + return this.cache; + } + const buf = new Uint8Array(this.byteLength); + let offs = 0; + for (const chunk of this.chunks) { + buf.set(chunk, offs); + offs += chunk.byteLength; + } + this.cache = decode(buf); + return this.cache!; + } +} diff --git a/std/io/writers_test.ts b/std/io/writers_test.ts new file mode 100644 index 000000000..decf611f1 --- /dev/null +++ b/std/io/writers_test.ts @@ -0,0 +1,15 @@ +const { copy } = Deno; +import { test } from "../testing/mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import { StringWriter } from "./writers.ts"; +import { StringReader } from "./readers.ts"; +import { copyN } from "./ioutil.ts"; + +test(async function ioStringWriter(): Promise<void> { + const w = new StringWriter("base"); + const r = new StringReader("0123456789"); + await copyN(w, r, 4); + assertEquals(w.toString(), "base0123"); + await copy(w, r); + assertEquals(w.toString(), "base0123456789"); +}); |