mirror of
https://github.com/denoland/deno.git
synced 2025-10-03 07:34:36 +00:00
BREAKING: Remove Deno.EOF, use null instead (#4953)
This commit is contained in:
parent
47c2f034e9
commit
678313b176
46 changed files with 329 additions and 325 deletions
|
@ -41,12 +41,12 @@ class FileReader implements Deno.Reader {
|
|||
|
||||
constructor(private filePath: string) {}
|
||||
|
||||
public async read(p: Uint8Array): Promise<number | Deno.EOF> {
|
||||
public async read(p: Uint8Array): Promise<number | null> {
|
||||
if (!this.file) {
|
||||
this.file = await Deno.open(this.filePath, { read: true });
|
||||
}
|
||||
const res = await Deno.read(this.file.rid, p);
|
||||
if (res === Deno.EOF) {
|
||||
if (res === null) {
|
||||
Deno.close(this.file.rid);
|
||||
this.file = undefined;
|
||||
}
|
||||
|
|
|
@ -17,8 +17,8 @@ Available Functions:
|
|||
```typescript
|
||||
sizeof(dataType: RawTypes): number
|
||||
getNBytes(r: Deno.Reader, n: number): Promise<Uint8Array>
|
||||
varnum(b: Uint8Array, o: VarnumOptions = {}): number | Deno.EOF
|
||||
varbig(b: Uint8Array, o: VarbigOptions = {}): bigint | Deno.EOF
|
||||
varnum(b: Uint8Array, o: VarnumOptions = {}): number | null
|
||||
varbig(b: Uint8Array, o: VarbigOptions = {}): bigint | null
|
||||
putVarnum(b: Uint8Array, x: number, o: VarnumOptions = {}): number
|
||||
putVarbig(b: Uint8Array, x: bigint, o: VarbigOptions = {}): number
|
||||
readVarnum(r: Deno.Reader, o: VarnumOptions = {}): Promise<number>
|
||||
|
|
|
@ -51,19 +51,16 @@ export async function getNBytes(
|
|||
): Promise<Uint8Array> {
|
||||
const scratch = new Uint8Array(n);
|
||||
const nRead = await r.read(scratch);
|
||||
if (nRead === Deno.EOF || nRead < n) throw new Deno.errors.UnexpectedEof();
|
||||
if (nRead === null || nRead < n) throw new Deno.errors.UnexpectedEof();
|
||||
return scratch;
|
||||
}
|
||||
|
||||
/** Decode a number from `b`, and return it as a `number`. Data-type defaults to `int32`.
|
||||
* Returns `EOF` if `b` is too short for the data-type given in `o`. */
|
||||
export function varnum(
|
||||
b: Uint8Array,
|
||||
o: VarnumOptions = {}
|
||||
): number | Deno.EOF {
|
||||
* Returns `null` if `b` is too short for the data-type given in `o`. */
|
||||
export function varnum(b: Uint8Array, o: VarnumOptions = {}): number | null {
|
||||
o.dataType = o.dataType ?? "int32";
|
||||
const littleEndian = (o.endian ?? "big") === "little" ? true : false;
|
||||
if (b.length < sizeof(o.dataType)) return Deno.EOF;
|
||||
if (b.length < sizeof(o.dataType)) return null;
|
||||
const view = new DataView(b.buffer);
|
||||
switch (o.dataType) {
|
||||
case "int8":
|
||||
|
@ -86,14 +83,11 @@ export function varnum(
|
|||
}
|
||||
|
||||
/** Decode an integer from `b`, and return it as a `bigint`. Data-type defaults to `int64`.
|
||||
* Returns `EOF` if `b` is too short for the data-type given in `o`. */
|
||||
export function varbig(
|
||||
b: Uint8Array,
|
||||
o: VarbigOptions = {}
|
||||
): bigint | Deno.EOF {
|
||||
* Returns `null` if `b` is too short for the data-type given in `o`. */
|
||||
export function varbig(b: Uint8Array, o: VarbigOptions = {}): bigint | null {
|
||||
o.dataType = o.dataType ?? "int64";
|
||||
const littleEndian = (o.endian ?? "big") === "little" ? true : false;
|
||||
if (b.length < sizeof(o.dataType)) return Deno.EOF;
|
||||
if (b.length < sizeof(o.dataType)) return null;
|
||||
const view = new DataView(b.buffer);
|
||||
switch (o.dataType) {
|
||||
case "int8":
|
||||
|
|
|
@ -64,12 +64,12 @@ async function readRecord(
|
|||
Startline: number,
|
||||
reader: BufReader,
|
||||
opt: ReadOptions = { comma: ",", trimLeadingSpace: false }
|
||||
): Promise<string[] | Deno.EOF> {
|
||||
): Promise<string[] | null> {
|
||||
const tp = new TextProtoReader(reader);
|
||||
const lineIndex = Startline;
|
||||
let line = await readLine(tp);
|
||||
|
||||
if (line === Deno.EOF) return Deno.EOF;
|
||||
if (line === null) return null;
|
||||
if (line.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ async function readRecord(
|
|||
// Hit end of line (copy all data so far).
|
||||
recordBuffer += line;
|
||||
const r = await readLine(tp);
|
||||
if (r === Deno.EOF) {
|
||||
if (r === null) {
|
||||
if (!opt.lazyQuotes) {
|
||||
quoteError = ERR_QUOTE;
|
||||
break parseField;
|
||||
|
@ -182,13 +182,13 @@ async function readRecord(
|
|||
}
|
||||
|
||||
async function isEOF(tp: TextProtoReader): Promise<boolean> {
|
||||
return (await tp.r.peek(0)) === Deno.EOF;
|
||||
return (await tp.r.peek(0)) === null;
|
||||
}
|
||||
|
||||
async function readLine(tp: TextProtoReader): Promise<string | Deno.EOF> {
|
||||
async function readLine(tp: TextProtoReader): Promise<string | null> {
|
||||
let line: string;
|
||||
const r = await tp.readLine();
|
||||
if (r === Deno.EOF) return Deno.EOF;
|
||||
if (r === null) return null;
|
||||
line = r;
|
||||
|
||||
// For backwards compatibility, drop trailing \r before EOF.
|
||||
|
@ -226,7 +226,7 @@ export async function readMatrix(
|
|||
|
||||
for (;;) {
|
||||
const r = await readRecord(lineIndex, reader, opt);
|
||||
if (r === Deno.EOF) break;
|
||||
if (r === null) break;
|
||||
lineResult = r;
|
||||
lineIndex++;
|
||||
// If fieldsPerRecord is 0, Read sets it to
|
||||
|
|
|
@ -17,7 +17,7 @@ async function startServer(): Promise<Deno.Process> {
|
|||
assert(server.stdout != null);
|
||||
const r = new TextProtoReader(new BufReader(server.stdout));
|
||||
const s = await r.readLine();
|
||||
assert(s !== Deno.EOF && s.includes("chat server starting"));
|
||||
assert(s !== null && s.includes("chat server starting"));
|
||||
} catch (err) {
|
||||
server.stdout!.close();
|
||||
server.close();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env -S deno --allow-net --allow-env
|
||||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||
import { parse } from "https://deno.land/std/flags/mod.ts";
|
||||
import { parse } from "../flags/mod.ts";
|
||||
|
||||
function pathBase(p: string): string {
|
||||
const parts = p.split("/");
|
||||
|
|
|
@ -16,7 +16,7 @@ Deno.test("[examples/echo_server]", async () => {
|
|||
const processReader = new BufReader(process.stdout!);
|
||||
const message = await processReader.readLine();
|
||||
|
||||
assertNotEquals(message, Deno.EOF);
|
||||
assertNotEquals(message, null);
|
||||
assertStrictEq(
|
||||
decoder.decode((message as ReadLineResult).line).trim(),
|
||||
"Listening on 0.0.0.0:8080"
|
||||
|
@ -28,7 +28,7 @@ Deno.test("[examples/echo_server]", async () => {
|
|||
await conn.write(encoder.encode("Hello echo_server\n"));
|
||||
const result = await connReader.readLine();
|
||||
|
||||
assertNotEquals(result, Deno.EOF);
|
||||
assertNotEquals(result, null);
|
||||
|
||||
const actualResponse = decoder
|
||||
.decode((result as ReadLineResult).line)
|
||||
|
|
|
@ -25,7 +25,7 @@ async function startFileServer(): Promise<void> {
|
|||
assert(fileServer.stdout != null);
|
||||
const r = new TextProtoReader(new BufReader(fileServer.stdout));
|
||||
const s = await r.readLine();
|
||||
assert(s !== Deno.EOF && s.includes("server listening"));
|
||||
assert(s !== null && s.includes("server listening"));
|
||||
}
|
||||
|
||||
function killFileServer(): void {
|
||||
|
@ -114,7 +114,7 @@ test("servePermissionDenied", async function (): Promise<void> {
|
|||
assert(deniedServer.stderr != null);
|
||||
const errReader = new TextProtoReader(new BufReader(deniedServer.stderr));
|
||||
const s = await reader.readLine();
|
||||
assert(s !== Deno.EOF && s.includes("server listening"));
|
||||
assert(s !== null && s.includes("server listening"));
|
||||
|
||||
try {
|
||||
const res = await fetch("http://localhost:4500/");
|
||||
|
@ -138,7 +138,7 @@ test("printHelp", async function (): Promise<void> {
|
|||
assert(helpProcess.stdout != null);
|
||||
const r = new TextProtoReader(new BufReader(helpProcess.stdout));
|
||||
const s = await r.readLine();
|
||||
assert(s !== Deno.EOF && s.includes("Deno File Server"));
|
||||
assert(s !== null && s.includes("Deno File Server"));
|
||||
helpProcess.close();
|
||||
helpProcess.stdout.close();
|
||||
});
|
||||
|
|
|
@ -7,8 +7,8 @@ import { STATUS_TEXT } from "./http_status.ts";
|
|||
|
||||
export function emptyReader(): Deno.Reader {
|
||||
return {
|
||||
read(_: Uint8Array): Promise<number | Deno.EOF> {
|
||||
return Promise.resolve(Deno.EOF);
|
||||
read(_: Uint8Array): Promise<number | null> {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -16,9 +16,9 @@ export function emptyReader(): Deno.Reader {
|
|||
export function bodyReader(contentLength: number, r: BufReader): Deno.Reader {
|
||||
let totalRead = 0;
|
||||
let finished = false;
|
||||
async function read(buf: Uint8Array): Promise<number | Deno.EOF> {
|
||||
if (finished) return Deno.EOF;
|
||||
let result: number | Deno.EOF;
|
||||
async function read(buf: Uint8Array): Promise<number | null> {
|
||||
if (finished) return null;
|
||||
let result: number | null;
|
||||
const remaining = contentLength - totalRead;
|
||||
if (remaining >= buf.byteLength) {
|
||||
result = await r.read(buf);
|
||||
|
@ -26,7 +26,7 @@ export function bodyReader(contentLength: number, r: BufReader): Deno.Reader {
|
|||
const readBuf = buf.subarray(0, remaining);
|
||||
result = await r.read(readBuf);
|
||||
}
|
||||
if (result !== Deno.EOF) {
|
||||
if (result !== null) {
|
||||
totalRead += result;
|
||||
}
|
||||
finished = totalRead === contentLength;
|
||||
|
@ -43,8 +43,8 @@ export function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
|
|||
offset: number;
|
||||
data: Uint8Array;
|
||||
}> = [];
|
||||
async function read(buf: Uint8Array): Promise<number | Deno.EOF> {
|
||||
if (finished) return Deno.EOF;
|
||||
async function read(buf: Uint8Array): Promise<number | null> {
|
||||
if (finished) return null;
|
||||
const [chunk] = chunks;
|
||||
if (chunk) {
|
||||
const chunkRemaining = chunk.data.byteLength - chunk.offset;
|
||||
|
@ -56,14 +56,14 @@ export function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
|
|||
if (chunk.offset === chunk.data.byteLength) {
|
||||
chunks.shift();
|
||||
// Consume \r\n;
|
||||
if ((await tp.readLine()) === Deno.EOF) {
|
||||
if ((await tp.readLine()) === null) {
|
||||
throw new Deno.errors.UnexpectedEof();
|
||||
}
|
||||
}
|
||||
return readLength;
|
||||
}
|
||||
const line = await tp.readLine();
|
||||
if (line === Deno.EOF) throw new Deno.errors.UnexpectedEof();
|
||||
if (line === null) throw new Deno.errors.UnexpectedEof();
|
||||
// TODO: handle chunk extension
|
||||
const [chunkSizeString] = line.split(";");
|
||||
const chunkSize = parseInt(chunkSizeString, 16);
|
||||
|
@ -73,12 +73,12 @@ export function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
|
|||
if (chunkSize > 0) {
|
||||
if (chunkSize > buf.byteLength) {
|
||||
let eof = await r.readFull(buf);
|
||||
if (eof === Deno.EOF) {
|
||||
if (eof === null) {
|
||||
throw new Deno.errors.UnexpectedEof();
|
||||
}
|
||||
const restChunk = new Uint8Array(chunkSize - buf.byteLength);
|
||||
eof = await r.readFull(restChunk);
|
||||
if (eof === Deno.EOF) {
|
||||
if (eof === null) {
|
||||
throw new Deno.errors.UnexpectedEof();
|
||||
} else {
|
||||
chunks.push({
|
||||
|
@ -90,11 +90,11 @@ export function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
|
|||
} else {
|
||||
const bufToFill = buf.subarray(0, chunkSize);
|
||||
const eof = await r.readFull(bufToFill);
|
||||
if (eof === Deno.EOF) {
|
||||
if (eof === null) {
|
||||
throw new Deno.errors.UnexpectedEof();
|
||||
}
|
||||
// Consume \r\n
|
||||
if ((await tp.readLine()) === Deno.EOF) {
|
||||
if ((await tp.readLine()) === null) {
|
||||
throw new Deno.errors.UnexpectedEof();
|
||||
}
|
||||
return chunkSize;
|
||||
|
@ -102,12 +102,12 @@ export function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
|
|||
} else {
|
||||
assert(chunkSize === 0);
|
||||
// Consume \r\n
|
||||
if ((await r.readLine()) === Deno.EOF) {
|
||||
if ((await r.readLine()) === null) {
|
||||
throw new Deno.errors.UnexpectedEof();
|
||||
}
|
||||
await readTrailers(h, r);
|
||||
finished = true;
|
||||
return Deno.EOF;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return { read };
|
||||
|
@ -131,7 +131,7 @@ export async function readTrailers(
|
|||
if (!keys) return;
|
||||
const tp = new TextProtoReader(r);
|
||||
const result = await tp.readMIMEHeader();
|
||||
assert(result != Deno.EOF, "trailer must be set");
|
||||
assert(result !== null, "trailer must be set");
|
||||
for (const [k, v] of result) {
|
||||
if (!keys.has(k)) {
|
||||
throw new Error("Undeclared trailer field");
|
||||
|
@ -332,12 +332,12 @@ export function parseHTTPVersion(vers: string): [number, number] {
|
|||
export async function readRequest(
|
||||
conn: Deno.Conn,
|
||||
bufr: BufReader
|
||||
): Promise<ServerRequest | Deno.EOF> {
|
||||
): Promise<ServerRequest | null> {
|
||||
const tp = new TextProtoReader(bufr);
|
||||
const firstLine = await tp.readLine(); // e.g. GET /index.html HTTP/1.0
|
||||
if (firstLine === Deno.EOF) return Deno.EOF;
|
||||
if (firstLine === null) return null;
|
||||
const headers = await tp.readMIMEHeader();
|
||||
if (headers === Deno.EOF) throw new Deno.errors.UnexpectedEof();
|
||||
if (headers === null) throw new Deno.errors.UnexpectedEof();
|
||||
|
||||
const req = new ServerRequest();
|
||||
req.conn = conn;
|
||||
|
|
|
@ -3,7 +3,6 @@ import {
|
|||
assertThrowsAsync,
|
||||
assertEquals,
|
||||
assert,
|
||||
assertNotEOF,
|
||||
assertNotEquals,
|
||||
} from "../testing/asserts.ts";
|
||||
import {
|
||||
|
@ -43,11 +42,11 @@ test("chunkedBodyReader", async () => {
|
|||
].join("");
|
||||
const h = new Headers();
|
||||
const r = chunkedBodyReader(h, new BufReader(new Buffer(encode(body))));
|
||||
let result: number | Deno.EOF;
|
||||
let result: number | null;
|
||||
// Use small buffer as some chunks exceed buffer size
|
||||
const buf = new Uint8Array(5);
|
||||
const dest = new Buffer();
|
||||
while ((result = await r.read(buf)) !== Deno.EOF) {
|
||||
while ((result = await r.read(buf)) !== null) {
|
||||
const len = Math.min(buf.byteLength, result);
|
||||
await dest.write(buf.subarray(0, len));
|
||||
}
|
||||
|
@ -223,25 +222,28 @@ test("writeUint8ArrayResponse", async function (): Promise<void> {
|
|||
const decoder = new TextDecoder("utf-8");
|
||||
const reader = new BufReader(buf);
|
||||
|
||||
let r: ReadLineResult;
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
let r: ReadLineResult | null = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK");
|
||||
assertEquals(r.more, false);
|
||||
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
r = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(decoder.decode(r.line), `content-length: ${shortText.length}`);
|
||||
assertEquals(r.more, false);
|
||||
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
r = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(r.line.byteLength, 0);
|
||||
assertEquals(r.more, false);
|
||||
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
r = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(decoder.decode(r.line), shortText);
|
||||
assertEquals(r.more, false);
|
||||
|
||||
const eof = await reader.readLine();
|
||||
assertEquals(eof, Deno.EOF);
|
||||
assertEquals(eof, null);
|
||||
});
|
||||
|
||||
test("writeStringResponse", async function (): Promise<void> {
|
||||
|
@ -255,25 +257,28 @@ test("writeStringResponse", async function (): Promise<void> {
|
|||
const decoder = new TextDecoder("utf-8");
|
||||
const reader = new BufReader(buf);
|
||||
|
||||
let r: ReadLineResult;
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
let r: ReadLineResult | null = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK");
|
||||
assertEquals(r.more, false);
|
||||
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
r = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(decoder.decode(r.line), `content-length: ${body.length}`);
|
||||
assertEquals(r.more, false);
|
||||
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
r = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(r.line.byteLength, 0);
|
||||
assertEquals(r.more, false);
|
||||
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
r = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(decoder.decode(r.line), body);
|
||||
assertEquals(r.more, false);
|
||||
|
||||
const eof = await reader.readLine();
|
||||
assertEquals(eof, Deno.EOF);
|
||||
assertEquals(eof, null);
|
||||
});
|
||||
|
||||
test("writeStringReaderResponse", async function (): Promise<void> {
|
||||
|
@ -288,28 +293,33 @@ test("writeStringReaderResponse", async function (): Promise<void> {
|
|||
const decoder = new TextDecoder("utf-8");
|
||||
const reader = new BufReader(buf);
|
||||
|
||||
let r: ReadLineResult;
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
let r: ReadLineResult | null = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(decoder.decode(r.line), "HTTP/1.1 200 OK");
|
||||
assertEquals(r.more, false);
|
||||
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
r = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(decoder.decode(r.line), "transfer-encoding: chunked");
|
||||
assertEquals(r.more, false);
|
||||
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
r = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(r.line.byteLength, 0);
|
||||
assertEquals(r.more, false);
|
||||
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
r = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(decoder.decode(r.line), shortText.length.toString());
|
||||
assertEquals(r.more, false);
|
||||
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
r = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(decoder.decode(r.line), shortText);
|
||||
assertEquals(r.more, false);
|
||||
|
||||
r = assertNotEOF(await reader.readLine());
|
||||
r = await reader.readLine();
|
||||
assert(r !== null);
|
||||
assertEquals(decoder.decode(r.line), "0");
|
||||
assertEquals(r.more, false);
|
||||
});
|
||||
|
@ -372,7 +382,7 @@ test("testReadRequestError", async function (): Promise<void> {
|
|||
in: "GET / HTTP/1.1\r\nheader:foo\r\n",
|
||||
err: Deno.errors.UnexpectedEof,
|
||||
},
|
||||
{ in: "", err: Deno.EOF },
|
||||
{ in: "", eof: true },
|
||||
{
|
||||
in: "HEAD / HTTP/1.1\r\nContent-Length:4\r\n\r\n",
|
||||
err: "http: method cannot contain a Content-Length",
|
||||
|
@ -427,14 +437,14 @@ test("testReadRequestError", async function (): Promise<void> {
|
|||
for (const test of testCases) {
|
||||
const reader = new BufReader(new StringReader(test.in));
|
||||
let err;
|
||||
let req: ServerRequest | Deno.EOF | undefined;
|
||||
let req: ServerRequest | null = null;
|
||||
try {
|
||||
req = await readRequest(mockConn(), reader);
|
||||
} catch (e) {
|
||||
err = e;
|
||||
}
|
||||
if (test.err === Deno.EOF) {
|
||||
assertEquals(req, Deno.EOF);
|
||||
if (test.eof) {
|
||||
assertEquals(req, null);
|
||||
} else if (typeof test.err === "string") {
|
||||
assertEquals(err.message, test.err);
|
||||
} else if (test.err) {
|
||||
|
@ -443,7 +453,7 @@ test("testReadRequestError", async function (): Promise<void> {
|
|||
assert(req instanceof ServerRequest);
|
||||
assert(test.headers);
|
||||
assertEquals(err, undefined);
|
||||
assertNotEquals(req, Deno.EOF);
|
||||
assertNotEquals(req, null);
|
||||
for (const h of test.headers) {
|
||||
assertEquals(req.headers.get(h.key), h.value);
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ export function mockConn(base: Partial<Deno.Conn> = {}): Deno.Conn {
|
|||
rid: -1,
|
||||
closeRead: (): void => {},
|
||||
closeWrite: (): void => {},
|
||||
read: (): Promise<number | Deno.EOF> => {
|
||||
read: (): Promise<number | null> => {
|
||||
return Promise.resolve(0);
|
||||
},
|
||||
write: (): Promise<number> => {
|
||||
|
|
|
@ -13,7 +13,7 @@ async function startServer(): Promise<void> {
|
|||
assert(server.stdout != null);
|
||||
const r = new TextProtoReader(new BufReader(server.stdout));
|
||||
const s = await r.readLine();
|
||||
assert(s !== Deno.EOF && s.includes("Racing server listening..."));
|
||||
assert(s !== null && s.includes("Racing server listening..."));
|
||||
}
|
||||
function killServer(): void {
|
||||
server.close();
|
||||
|
|
|
@ -60,7 +60,7 @@ export class ServerRequest {
|
|||
* let totRead = 0;
|
||||
* while (true) {
|
||||
* const nread = await req.body.read(bufSlice);
|
||||
* if (nread === Deno.EOF) break;
|
||||
* if (nread === null) break;
|
||||
* totRead += nread;
|
||||
* if (totRead >= req.contentLength) break;
|
||||
* bufSlice = bufSlice.subarray(nread);
|
||||
|
@ -117,7 +117,7 @@ export class ServerRequest {
|
|||
// Consume unread body
|
||||
const body = this.body;
|
||||
const buf = new Uint8Array(1024);
|
||||
while ((await body.read(buf)) !== Deno.EOF) {}
|
||||
while ((await body.read(buf)) !== null) {}
|
||||
this.finalized = true;
|
||||
}
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ export class Server implements AsyncIterable<ServerRequest> {
|
|||
const writer = new BufWriter(conn);
|
||||
|
||||
while (!this.closing) {
|
||||
let request: ServerRequest | Deno.EOF;
|
||||
let request: ServerRequest | null;
|
||||
try {
|
||||
request = await readRequest(conn, reader);
|
||||
} catch (error) {
|
||||
|
@ -167,7 +167,7 @@ export class Server implements AsyncIterable<ServerRequest> {
|
|||
}
|
||||
break;
|
||||
}
|
||||
if (request == Deno.EOF) {
|
||||
if (request === null) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
assert,
|
||||
assertEquals,
|
||||
assertMatch,
|
||||
assertNotEOF,
|
||||
assertStrContains,
|
||||
assertThrowsAsync,
|
||||
} from "../testing/asserts.ts";
|
||||
|
@ -108,7 +107,7 @@ interface TotalReader extends Deno.Reader {
|
|||
}
|
||||
function totalReader(r: Deno.Reader): TotalReader {
|
||||
let _total = 0;
|
||||
async function read(p: Uint8Array): Promise<number | Deno.EOF> {
|
||||
async function read(p: Uint8Array): Promise<number | null> {
|
||||
const result = await r.read(p);
|
||||
if (typeof result === "number") {
|
||||
_total += result;
|
||||
|
@ -252,13 +251,13 @@ test("requestBodyReaderWithContentLength", async function (): Promise<void> {
|
|||
let offset = 0;
|
||||
while (offset < shortText.length) {
|
||||
const nread = await req.body.read(readBuf);
|
||||
assertNotEOF(nread);
|
||||
assert(nread !== null);
|
||||
const s = decode(readBuf.subarray(0, nread as number));
|
||||
assertEquals(shortText.substr(offset, nread as number), s);
|
||||
offset += nread as number;
|
||||
}
|
||||
const nread = await req.body.read(readBuf);
|
||||
assertEquals(nread, Deno.EOF);
|
||||
assertEquals(nread, null);
|
||||
}
|
||||
|
||||
// Larger than given buf
|
||||
|
@ -273,13 +272,13 @@ test("requestBodyReaderWithContentLength", async function (): Promise<void> {
|
|||
let offset = 0;
|
||||
while (offset < longText.length) {
|
||||
const nread = await req.body.read(readBuf);
|
||||
assertNotEOF(nread);
|
||||
assert(nread !== null);
|
||||
const s = decode(readBuf.subarray(0, nread as number));
|
||||
assertEquals(longText.substr(offset, nread as number), s);
|
||||
offset += nread as number;
|
||||
}
|
||||
const nread = await req.body.read(readBuf);
|
||||
assertEquals(nread, Deno.EOF);
|
||||
assertEquals(nread, null);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -307,13 +306,13 @@ test("requestBodyReaderWithTransferEncoding", async function (): Promise<void> {
|
|||
let offset = 0;
|
||||
while (offset < shortText.length) {
|
||||
const nread = await req.body.read(readBuf);
|
||||
assertNotEOF(nread);
|
||||
assert(nread !== null);
|
||||
const s = decode(readBuf.subarray(0, nread as number));
|
||||
assertEquals(shortText.substr(offset, nread as number), s);
|
||||
offset += nread as number;
|
||||
}
|
||||
const nread = await req.body.read(readBuf);
|
||||
assertEquals(nread, Deno.EOF);
|
||||
assertEquals(nread, null);
|
||||
}
|
||||
|
||||
// Larger than internal buf
|
||||
|
@ -340,13 +339,13 @@ test("requestBodyReaderWithTransferEncoding", async function (): Promise<void> {
|
|||
let offset = 0;
|
||||
while (offset < longText.length) {
|
||||
const nread = await req.body.read(readBuf);
|
||||
assertNotEOF(nread);
|
||||
assert(nread !== null);
|
||||
const s = decode(readBuf.subarray(0, nread as number));
|
||||
assertEquals(longText.substr(offset, nread as number), s);
|
||||
offset += nread as number;
|
||||
}
|
||||
const nread = await req.body.read(readBuf);
|
||||
assertEquals(nread, Deno.EOF);
|
||||
assertEquals(nread, null);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -372,7 +371,7 @@ test({
|
|||
try {
|
||||
const r = new TextProtoReader(new BufReader(p.stdout!));
|
||||
const s = await r.readLine();
|
||||
assert(s !== Deno.EOF && s.includes("server listening"));
|
||||
assert(s !== null && s.includes("server listening"));
|
||||
await delay(100);
|
||||
// Reqeusts to the server and immediately closes the connection
|
||||
const conn = await Deno.connect({ port: 4502 });
|
||||
|
@ -419,7 +418,7 @@ test({
|
|||
const r = new TextProtoReader(new BufReader(p.stdout!));
|
||||
const s = await r.readLine();
|
||||
assert(
|
||||
s !== Deno.EOF && s.includes("server listening"),
|
||||
s !== null && s.includes("server listening"),
|
||||
"server must be started"
|
||||
);
|
||||
// Requests to the server and immediately closes the connection
|
||||
|
@ -433,7 +432,8 @@ test({
|
|||
new TextEncoder().encode("GET / HTTP/1.0\r\n\r\n")
|
||||
);
|
||||
const res = new Uint8Array(100);
|
||||
const nread = assertNotEOF(await conn.read(res));
|
||||
const nread = await conn.read(res);
|
||||
assert(nread !== null);
|
||||
conn.close();
|
||||
const resStr = new TextDecoder().decode(res.subarray(0, nread));
|
||||
assert(resStr.includes("Hello HTTPS"));
|
||||
|
@ -476,7 +476,7 @@ test({
|
|||
);
|
||||
const res = new Uint8Array(100);
|
||||
const nread = await conn.read(res);
|
||||
assert(nread !== Deno.EOF);
|
||||
assert(nread !== null);
|
||||
const resStr = new TextDecoder().decode(res.subarray(0, nread));
|
||||
assertStrContains(resStr, "/hello");
|
||||
server.close();
|
||||
|
|
|
@ -83,7 +83,7 @@ export class BufReader implements Reader {
|
|||
// 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) {
|
||||
if (rr === null) {
|
||||
this.eof = true;
|
||||
return;
|
||||
}
|
||||
|
@ -120,8 +120,8 @@ export class BufReader implements 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;
|
||||
async read(p: Uint8Array): Promise<number | null> {
|
||||
let rr: number | null = p.byteLength;
|
||||
if (p.byteLength === 0) return rr;
|
||||
|
||||
if (this.r === this.w) {
|
||||
|
@ -129,7 +129,7 @@ export class BufReader implements Reader {
|
|||
// 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;
|
||||
const nread = rr ?? 0;
|
||||
assert(nread >= 0, "negative read");
|
||||
// if (rr.nread > 0) {
|
||||
// this.lastByte = p[rr.nread - 1];
|
||||
|
@ -143,7 +143,7 @@ export class BufReader implements Reader {
|
|||
this.r = 0;
|
||||
this.w = 0;
|
||||
rr = await this.rd.read(this.buf);
|
||||
if (rr === 0 || rr === Deno.EOF) return rr;
|
||||
if (rr === 0 || rr === null) return rr;
|
||||
assert(rr >= 0, "negative read");
|
||||
this.w += rr;
|
||||
}
|
||||
|
@ -161,7 +161,7 @@ export class BufReader implements Reader {
|
|||
* 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.
|
||||
* bytes available in the buffer, `readFull()` returns `null` 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
|
||||
|
@ -170,14 +170,14 @@ export class BufReader implements Reader {
|
|||
*
|
||||
* Ported from https://golang.org/pkg/io/#ReadFull
|
||||
*/
|
||||
async readFull(p: Uint8Array): Promise<Uint8Array | Deno.EOF> {
|
||||
async readFull(p: Uint8Array): Promise<Uint8Array | null> {
|
||||
let bytesRead = 0;
|
||||
while (bytesRead < p.length) {
|
||||
try {
|
||||
const rr = await this.read(p.subarray(bytesRead));
|
||||
if (rr === Deno.EOF) {
|
||||
if (rr === null) {
|
||||
if (bytesRead === 0) {
|
||||
return Deno.EOF;
|
||||
return null;
|
||||
} else {
|
||||
throw new PartialReadError();
|
||||
}
|
||||
|
@ -191,10 +191,10 @@ export class BufReader implements Reader {
|
|||
return p;
|
||||
}
|
||||
|
||||
/** Returns the next byte [0, 255] or `EOF`. */
|
||||
async readByte(): Promise<number | Deno.EOF> {
|
||||
/** Returns the next byte [0, 255] or `null`. */
|
||||
async readByte(): Promise<number | null> {
|
||||
while (this.r === this.w) {
|
||||
if (this.eof) return Deno.EOF;
|
||||
if (this.eof) return null;
|
||||
await this._fill(); // buffer is empty.
|
||||
}
|
||||
const c = this.buf[this.r];
|
||||
|
@ -207,17 +207,17 @@ export class BufReader implements Reader {
|
|||
* 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).
|
||||
* (often `null`).
|
||||
* 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> {
|
||||
async readString(delim: string): Promise<string | null> {
|
||||
if (delim.length !== 1) {
|
||||
throw new Error("Delimiter should be a single character");
|
||||
}
|
||||
const buffer = await this.readSlice(delim.charCodeAt(0));
|
||||
if (buffer == Deno.EOF) return Deno.EOF;
|
||||
if (buffer === null) return null;
|
||||
return new TextDecoder().decode(buffer);
|
||||
}
|
||||
|
||||
|
@ -237,14 +237,14 @@ export class BufReader implements Reader {
|
|||
* 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.
|
||||
* `readLine()` returns `null`.
|
||||
*
|
||||
* 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;
|
||||
async readLine(): Promise<ReadLineResult | null> {
|
||||
let line: Uint8Array | null;
|
||||
|
||||
try {
|
||||
line = await this.readSlice(LF);
|
||||
|
@ -277,8 +277,8 @@ export class BufReader implements Reader {
|
|||
return { line: partial, more: !this.eof };
|
||||
}
|
||||
|
||||
if (line === Deno.EOF) {
|
||||
return Deno.EOF;
|
||||
if (line === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (line.byteLength === 0) {
|
||||
|
@ -306,12 +306,12 @@ export class BufReader implements Reader {
|
|||
* 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`.
|
||||
* it returns `null`.
|
||||
*
|
||||
* 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> {
|
||||
async readSlice(delim: number): Promise<Uint8Array | null> {
|
||||
let s = 0; // search start index
|
||||
let slice: Uint8Array | undefined;
|
||||
|
||||
|
@ -328,7 +328,7 @@ export class BufReader implements Reader {
|
|||
// EOF?
|
||||
if (this.eof) {
|
||||
if (this.r === this.w) {
|
||||
return Deno.EOF;
|
||||
return null;
|
||||
}
|
||||
slice = this.buf.subarray(this.r, this.w);
|
||||
this.r = this.w;
|
||||
|
@ -367,13 +367,13 @@ export class BufReader implements Reader {
|
|||
*
|
||||
* 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`.
|
||||
* left in the buffer, it returns `null`.
|
||||
*
|
||||
* 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> {
|
||||
async peek(n: number): Promise<Uint8Array | null> {
|
||||
if (n < 0) {
|
||||
throw Error("negative count");
|
||||
}
|
||||
|
@ -390,7 +390,7 @@ export class BufReader implements Reader {
|
|||
}
|
||||
|
||||
if (avail === 0 && this.eof) {
|
||||
return Deno.EOF;
|
||||
return null;
|
||||
} else if (avail < n && this.eof) {
|
||||
return this.buf.subarray(this.r, this.r + avail);
|
||||
} else if (avail < n) {
|
||||
|
@ -656,7 +656,7 @@ export async function* readDelim(
|
|||
let matchIndex = 0;
|
||||
while (true) {
|
||||
const result = await reader.read(inspectArr);
|
||||
if (result === Deno.EOF) {
|
||||
if (result === null) {
|
||||
// Yield last chunk.
|
||||
yield inputBuffer.bytes();
|
||||
return;
|
||||
|
|
|
@ -5,12 +5,7 @@
|
|||
|
||||
const { Buffer } = Deno;
|
||||
type Reader = Deno.Reader;
|
||||
import {
|
||||
assert,
|
||||
assertEquals,
|
||||
fail,
|
||||
assertNotEOF,
|
||||
} from "../testing/asserts.ts";
|
||||
import { assert, assertEquals, fail } from "../testing/asserts.ts";
|
||||
import {
|
||||
BufReader,
|
||||
BufWriter,
|
||||
|
@ -30,7 +25,7 @@ async function readBytes(buf: BufReader): Promise<string> {
|
|||
let nb = 0;
|
||||
while (true) {
|
||||
const c = await buf.readByte();
|
||||
if (c === Deno.EOF) {
|
||||
if (c === null) {
|
||||
break; // EOF
|
||||
}
|
||||
b[nb] = c;
|
||||
|
@ -69,7 +64,7 @@ async function reads(buf: BufReader, m: number): Promise<string> {
|
|||
let nb = 0;
|
||||
while (true) {
|
||||
const result = await buf.read(b.subarray(nb, nb + m));
|
||||
if (result === Deno.EOF) {
|
||||
if (result === null) {
|
||||
break;
|
||||
}
|
||||
nb += result;
|
||||
|
@ -152,7 +147,8 @@ Deno.test("bufioBufferFull", async function (): Promise<void> {
|
|||
assertEquals(decoder.decode(err.partial), "And now, hello, ");
|
||||
}
|
||||
|
||||
const line = assertNotEOF(await buf.readSlice(charCode("!")));
|
||||
const line = await buf.readSlice(charCode("!"));
|
||||
assert(line !== null);
|
||||
const actual = decoder.decode(line);
|
||||
assertEquals(actual, "world!");
|
||||
});
|
||||
|
@ -161,14 +157,16 @@ Deno.test("bufioReadString", async function (): Promise<void> {
|
|||
const string = "And now, hello world!";
|
||||
const buf = new BufReader(stringsReader(string), MIN_READ_BUFFER_SIZE);
|
||||
|
||||
const line = assertNotEOF(await buf.readString(","));
|
||||
const line = await buf.readString(",");
|
||||
assert(line !== null);
|
||||
assertEquals(line, "And now,");
|
||||
assertEquals(line.length, 8);
|
||||
|
||||
const line2 = assertNotEOF(await buf.readString(","));
|
||||
const line2 = await buf.readString(",");
|
||||
assert(line2 !== null);
|
||||
assertEquals(line2, " hello world!");
|
||||
|
||||
assertEquals(await buf.readString(","), Deno.EOF);
|
||||
assertEquals(await buf.readString(","), null);
|
||||
|
||||
try {
|
||||
await buf.readString("deno");
|
||||
|
@ -192,7 +190,7 @@ const testOutput = encoder.encode("0123456789abcdefghijklmnopqrstuvwxy");
|
|||
class TestReader implements Reader {
|
||||
constructor(private data: Uint8Array, private stride: number) {}
|
||||
|
||||
read(buf: Uint8Array): Promise<number | Deno.EOF> {
|
||||
read(buf: Uint8Array): Promise<number | null> {
|
||||
let nread = this.stride;
|
||||
if (nread > this.data.byteLength) {
|
||||
nread = this.data.byteLength;
|
||||
|
@ -201,7 +199,7 @@ class TestReader implements Reader {
|
|||
nread = buf.byteLength;
|
||||
}
|
||||
if (nread === 0) {
|
||||
return Promise.resolve(Deno.EOF);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
copyBytes(buf as Uint8Array, this.data);
|
||||
this.data = this.data.subarray(nread);
|
||||
|
@ -216,7 +214,7 @@ async function testReadLine(input: Uint8Array): Promise<void> {
|
|||
const l = new BufReader(reader, input.byteLength + 1);
|
||||
while (true) {
|
||||
const r = await l.readLine();
|
||||
if (r === Deno.EOF) {
|
||||
if (r === null) {
|
||||
break;
|
||||
}
|
||||
const { line, more } = r;
|
||||
|
@ -253,10 +251,12 @@ Deno.test("bufioPeek", async function (): Promise<void> {
|
|||
MIN_READ_BUFFER_SIZE
|
||||
);
|
||||
|
||||
let actual = assertNotEOF(await buf.peek(1));
|
||||
let actual = await buf.peek(1);
|
||||
assert(actual !== null);
|
||||
assertEquals(decoder.decode(actual), "a");
|
||||
|
||||
actual = assertNotEOF(await buf.peek(4));
|
||||
actual = await buf.peek(4);
|
||||
assert(actual !== null);
|
||||
assertEquals(decoder.decode(actual), "abcd");
|
||||
|
||||
try {
|
||||
|
@ -271,33 +271,39 @@ Deno.test("bufioPeek", async function (): Promise<void> {
|
|||
await buf.read(p.subarray(0, 3));
|
||||
assertEquals(decoder.decode(p.subarray(0, 3)), "abc");
|
||||
|
||||
actual = assertNotEOF(await buf.peek(1));
|
||||
actual = await buf.peek(1);
|
||||
assert(actual !== null);
|
||||
assertEquals(decoder.decode(actual), "d");
|
||||
|
||||
actual = assertNotEOF(await buf.peek(1));
|
||||
actual = await buf.peek(1);
|
||||
assert(actual !== null);
|
||||
assertEquals(decoder.decode(actual), "d");
|
||||
|
||||
actual = assertNotEOF(await buf.peek(1));
|
||||
actual = await buf.peek(1);
|
||||
assert(actual !== null);
|
||||
assertEquals(decoder.decode(actual), "d");
|
||||
|
||||
actual = assertNotEOF(await buf.peek(2));
|
||||
actual = await buf.peek(2);
|
||||
assert(actual !== null);
|
||||
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);
|
||||
assert(res !== null);
|
||||
|
||||
actual = assertNotEOF(await buf.peek(4));
|
||||
actual = await buf.peek(4);
|
||||
assert(actual !== null);
|
||||
assertEquals(decoder.decode(actual), "ghij");
|
||||
|
||||
await buf.read(p);
|
||||
assertEquals(decoder.decode(p), "ghijklmnop");
|
||||
|
||||
actual = assertNotEOF(await buf.peek(0));
|
||||
actual = await buf.peek(0);
|
||||
assert(actual !== null);
|
||||
assertEquals(decoder.decode(actual), "");
|
||||
|
||||
const r = await buf.peek(1);
|
||||
assert(r === Deno.EOF);
|
||||
assert(r === null);
|
||||
/* TODO
|
||||
Test for issue 3022, not exposing a reader's error on a successful Peek.
|
||||
buf = NewReaderSize(dataAndEOFReader("abcd"), 32)
|
||||
|
@ -396,7 +402,8 @@ Deno.test("bufReaderReadFull", async function (): Promise<void> {
|
|||
const bufr = new BufReader(data, 3);
|
||||
{
|
||||
const buf = new Uint8Array(6);
|
||||
const r = assertNotEOF(await bufr.readFull(buf));
|
||||
const r = await bufr.readFull(buf);
|
||||
assert(r !== null);
|
||||
assertEquals(r, buf);
|
||||
assertEquals(dec.decode(buf), "Hello ");
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ type Reader = Deno.Reader;
|
|||
export class OneByteReader implements Reader {
|
||||
constructor(readonly r: Reader) {}
|
||||
|
||||
read(p: Uint8Array): Promise<number | Deno.EOF> {
|
||||
read(p: Uint8Array): Promise<number | null> {
|
||||
if (p.byteLength === 0) {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ export class OneByteReader implements Reader {
|
|||
export class HalfReader implements Reader {
|
||||
constructor(readonly r: Reader) {}
|
||||
|
||||
read(p: Uint8Array): Promise<number | Deno.EOF> {
|
||||
read(p: Uint8Array): Promise<number | null> {
|
||||
if (!(p instanceof Uint8Array)) {
|
||||
throw Error("expected Uint8Array");
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ export class TimeoutReader implements Reader {
|
|||
count = 0;
|
||||
constructor(readonly r: Reader) {}
|
||||
|
||||
read(p: Uint8Array): Promise<number | Deno.EOF> {
|
||||
read(p: Uint8Array): Promise<number | null> {
|
||||
this.count++;
|
||||
if (this.count === 2) {
|
||||
throw new Deno.errors.TimedOut();
|
||||
|
|
|
@ -21,13 +21,13 @@ export async function copyN(
|
|||
buf = new Uint8Array(size - bytesRead);
|
||||
}
|
||||
const result = await r.read(buf);
|
||||
const nread = result === Deno.EOF ? 0 : result;
|
||||
const nread = result ?? 0;
|
||||
bytesRead += nread;
|
||||
if (nread > 0) {
|
||||
const n = await dest.write(buf.slice(0, nread));
|
||||
assert(n === nread, "could not write");
|
||||
}
|
||||
if (result === Deno.EOF) {
|
||||
if (result === null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -35,31 +35,31 @@ export async function copyN(
|
|||
}
|
||||
|
||||
/** Read big endian 16bit short from BufReader */
|
||||
export async function readShort(buf: BufReader): Promise<number | Deno.EOF> {
|
||||
export async function readShort(buf: BufReader): Promise<number | null> {
|
||||
const high = await buf.readByte();
|
||||
if (high === Deno.EOF) return Deno.EOF;
|
||||
if (high === null) return null;
|
||||
const low = await buf.readByte();
|
||||
if (low === Deno.EOF) throw new Deno.errors.UnexpectedEof();
|
||||
if (low === null) throw new Deno.errors.UnexpectedEof();
|
||||
return (high << 8) | low;
|
||||
}
|
||||
|
||||
/** Read big endian 32bit integer from BufReader */
|
||||
export async function readInt(buf: BufReader): Promise<number | Deno.EOF> {
|
||||
export async function readInt(buf: BufReader): Promise<number | null> {
|
||||
const high = await readShort(buf);
|
||||
if (high === Deno.EOF) return Deno.EOF;
|
||||
if (high === null) return null;
|
||||
const low = await readShort(buf);
|
||||
if (low === Deno.EOF) throw new Deno.errors.UnexpectedEof();
|
||||
if (low === null) throw new Deno.errors.UnexpectedEof();
|
||||
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> {
|
||||
export async function readLong(buf: BufReader): Promise<number | null> {
|
||||
const high = await readInt(buf);
|
||||
if (high === Deno.EOF) return Deno.EOF;
|
||||
if (high === null) return null;
|
||||
const low = await readInt(buf);
|
||||
if (low === Deno.EOF) throw new Deno.errors.UnexpectedEof();
|
||||
if (low === null) throw new Deno.errors.UnexpectedEof();
|
||||
const big = (BigInt(high) << 32n) | BigInt(low);
|
||||
// We probably should provide a similar API that returns BigInt values.
|
||||
if (big > MAX_SAFE_INTEGER) {
|
||||
|
|
|
@ -17,7 +17,7 @@ class BinaryReader implements Reader {
|
|||
|
||||
constructor(private bytes: Uint8Array = new Uint8Array(0)) {}
|
||||
|
||||
read(p: Uint8Array): Promise<number | Deno.EOF> {
|
||||
read(p: Uint8Array): Promise<number | null> {
|
||||
p.set(this.bytes.subarray(this.index, p.byteLength));
|
||||
this.index += p.byteLength;
|
||||
return Promise.resolve(p.byteLength);
|
||||
|
|
|
@ -9,12 +9,12 @@ export class StringReader implements Reader {
|
|||
|
||||
constructor(private readonly s: string) {}
|
||||
|
||||
read(p: Uint8Array): Promise<number | Deno.EOF> {
|
||||
read(p: Uint8Array): Promise<number | null> {
|
||||
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 Promise.resolve(Deno.EOF);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return Promise.resolve(n);
|
||||
}
|
||||
|
@ -29,11 +29,11 @@ export class MultiReader implements Reader {
|
|||
this.readers = readers;
|
||||
}
|
||||
|
||||
async read(p: Uint8Array): Promise<number | Deno.EOF> {
|
||||
async read(p: Uint8Array): Promise<number | null> {
|
||||
const r = this.readers[this.currentIndex];
|
||||
if (!r) return Deno.EOF;
|
||||
if (!r) return null;
|
||||
const result = await r.read(p);
|
||||
if (result === Deno.EOF) {
|
||||
if (result === null) {
|
||||
this.currentIndex++;
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ test("ioStringReader", async function (): Promise<void> {
|
|||
const res0 = await r.read(new Uint8Array(6));
|
||||
assertEquals(res0, 6);
|
||||
const res1 = await r.read(new Uint8Array(6));
|
||||
assertEquals(res1, Deno.EOF);
|
||||
assertEquals(res1, null);
|
||||
});
|
||||
|
||||
test("ioStringReader", async function (): Promise<void> {
|
||||
|
@ -23,7 +23,7 @@ test("ioStringReader", async function (): Promise<void> {
|
|||
assertEquals(res2, 3);
|
||||
assertEquals(decode(buf), "def");
|
||||
const res3 = await r.read(buf);
|
||||
assertEquals(res3, Deno.EOF);
|
||||
assertEquals(res3, null);
|
||||
assertEquals(decode(buf), "def");
|
||||
});
|
||||
|
||||
|
|
|
@ -105,7 +105,7 @@ export function scanUntilBoundary(
|
|||
newLineDashBoundary: Uint8Array,
|
||||
total: number,
|
||||
eof: boolean
|
||||
): number | Deno.EOF {
|
||||
): number | null {
|
||||
if (total === 0) {
|
||||
// At beginning of body, allow dashBoundary.
|
||||
if (hasPrefix(buf, dashBoundary)) {
|
||||
|
@ -115,7 +115,7 @@ export function scanUntilBoundary(
|
|||
case 0:
|
||||
return 0;
|
||||
case 1:
|
||||
return Deno.EOF;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (hasPrefix(dashBoundary, buf)) {
|
||||
|
@ -132,7 +132,7 @@ export function scanUntilBoundary(
|
|||
case 0:
|
||||
return i;
|
||||
case 1:
|
||||
return i > 0 ? i : Deno.EOF;
|
||||
return i > 0 ? i : null;
|
||||
}
|
||||
}
|
||||
if (hasPrefix(newLineDashBoundary, buf)) {
|
||||
|
@ -151,12 +151,12 @@ export function scanUntilBoundary(
|
|||
}
|
||||
|
||||
class PartReader implements Reader, Closer {
|
||||
n: number | Deno.EOF = 0;
|
||||
n: number | null = 0;
|
||||
total = 0;
|
||||
|
||||
constructor(private mr: MultipartReader, public readonly headers: Headers) {}
|
||||
|
||||
async read(p: Uint8Array): Promise<number | Deno.EOF> {
|
||||
async read(p: Uint8Array): Promise<number | null> {
|
||||
const br = this.mr.bufReader;
|
||||
|
||||
// Read into buffer until we identify some data to return,
|
||||
|
@ -165,7 +165,7 @@ class PartReader implements Reader, Closer {
|
|||
while (this.n === 0) {
|
||||
peekLength = max(peekLength, br.buffered());
|
||||
const peekBuf = await br.peek(peekLength);
|
||||
if (peekBuf === Deno.EOF) {
|
||||
if (peekBuf === null) {
|
||||
throw new Deno.errors.UnexpectedEof();
|
||||
}
|
||||
const eof = peekBuf.length < peekLength;
|
||||
|
@ -183,8 +183,8 @@ class PartReader implements Reader, Closer {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.n === Deno.EOF) {
|
||||
return Deno.EOF;
|
||||
if (this.n === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nread = min(p.length, this.n);
|
||||
|
@ -288,7 +288,7 @@ export class MultipartReader {
|
|||
const buf = new Buffer(new Uint8Array(maxValueBytes));
|
||||
for (;;) {
|
||||
const p = await this.nextPart();
|
||||
if (p === Deno.EOF) {
|
||||
if (p === null) {
|
||||
break;
|
||||
}
|
||||
if (p.formName === "") {
|
||||
|
@ -354,7 +354,7 @@ export class MultipartReader {
|
|||
private currentPart: PartReader | undefined;
|
||||
private partsRead = 0;
|
||||
|
||||
private async nextPart(): Promise<PartReader | Deno.EOF> {
|
||||
private async nextPart(): Promise<PartReader | null> {
|
||||
if (this.currentPart) {
|
||||
this.currentPart.close();
|
||||
}
|
||||
|
@ -364,14 +364,14 @@ export class MultipartReader {
|
|||
let expectNewPart = false;
|
||||
for (;;) {
|
||||
const line = await this.bufReader.readSlice("\n".charCodeAt(0));
|
||||
if (line === Deno.EOF) {
|
||||
if (line === null) {
|
||||
throw new Deno.errors.UnexpectedEof();
|
||||
}
|
||||
if (this.isBoundaryDelimiterLine(line)) {
|
||||
this.partsRead++;
|
||||
const r = new TextProtoReader(this.bufReader);
|
||||
const headers = await r.readMIMEHeader();
|
||||
if (headers === Deno.EOF) {
|
||||
if (headers === null) {
|
||||
throw new Deno.errors.UnexpectedEof();
|
||||
}
|
||||
const np = new PartReader(this, headers);
|
||||
|
@ -379,7 +379,7 @@ export class MultipartReader {
|
|||
return np;
|
||||
}
|
||||
if (this.isFinalBoundary(line)) {
|
||||
return Deno.EOF;
|
||||
return null;
|
||||
}
|
||||
if (expectNewPart) {
|
||||
throw new Error(`expecting a new Part; got line ${line}`);
|
||||
|
|
|
@ -31,7 +31,7 @@ test("multipartScanUntilBoundary1", function (): void {
|
|||
0,
|
||||
true
|
||||
);
|
||||
assertEquals(n, Deno.EOF);
|
||||
assertEquals(n, null);
|
||||
});
|
||||
|
||||
test("multipartScanUntilBoundary2", function (): void {
|
||||
|
|
|
@ -379,8 +379,3 @@ export function unimplemented(msg?: string): never {
|
|||
export function unreachable(): never {
|
||||
throw new AssertionError("unreachable");
|
||||
}
|
||||
|
||||
export function assertNotEOF<T extends {}>(val: T | Deno.EOF): T {
|
||||
assertNotEquals(val, Deno.EOF);
|
||||
return val as T;
|
||||
}
|
||||
|
|
|
@ -22,9 +22,9 @@ export class TextProtoReader {
|
|||
/** readLine() reads a single line from the TextProtoReader,
|
||||
* eliding the final \n or \r\n from the returned string.
|
||||
*/
|
||||
async readLine(): Promise<string | Deno.EOF> {
|
||||
async readLine(): Promise<string | null> {
|
||||
const s = await this.readLineSlice();
|
||||
if (s === Deno.EOF) return Deno.EOF;
|
||||
if (s === null) return null;
|
||||
return str(s);
|
||||
}
|
||||
|
||||
|
@ -48,20 +48,20 @@ export class TextProtoReader {
|
|||
* "Long-Key": {"Even Longer Value"},
|
||||
* }
|
||||
*/
|
||||
async readMIMEHeader(): Promise<Headers | Deno.EOF> {
|
||||
async readMIMEHeader(): Promise<Headers | null> {
|
||||
const m = new Headers();
|
||||
let line: Uint8Array | undefined;
|
||||
|
||||
// The first line cannot start with a leading space.
|
||||
let buf = await this.r.peek(1);
|
||||
if (buf === Deno.EOF) {
|
||||
return Deno.EOF;
|
||||
if (buf === null) {
|
||||
return null;
|
||||
} else if (buf[0] == charCode(" ") || buf[0] == charCode("\t")) {
|
||||
line = (await this.readLineSlice()) as Uint8Array;
|
||||
}
|
||||
|
||||
buf = await this.r.peek(1);
|
||||
if (buf === Deno.EOF) {
|
||||
if (buf === null) {
|
||||
throw new Deno.errors.UnexpectedEof();
|
||||
} else if (buf[0] == charCode(" ") || buf[0] == charCode("\t")) {
|
||||
throw new Deno.errors.InvalidData(
|
||||
|
@ -71,7 +71,7 @@ export class TextProtoReader {
|
|||
|
||||
while (true) {
|
||||
const kv = await this.readLineSlice(); // readContinuedLineSlice
|
||||
if (kv === Deno.EOF) throw new Deno.errors.UnexpectedEof();
|
||||
if (kv === null) throw new Deno.errors.UnexpectedEof();
|
||||
if (kv.byteLength === 0) return m;
|
||||
|
||||
// Key ends at first colon
|
||||
|
@ -112,12 +112,12 @@ export class TextProtoReader {
|
|||
}
|
||||
}
|
||||
|
||||
async readLineSlice(): Promise<Uint8Array | Deno.EOF> {
|
||||
async readLineSlice(): Promise<Uint8Array | null> {
|
||||
// this.closeDot();
|
||||
let line: Uint8Array | undefined;
|
||||
while (true) {
|
||||
const r = await this.r.readLine();
|
||||
if (r === Deno.EOF) return Deno.EOF;
|
||||
if (r === null) return null;
|
||||
const { line: l, more } = r;
|
||||
|
||||
// Avoid the copy if the first call produced a full line.
|
||||
|
|
|
@ -6,12 +6,7 @@
|
|||
import { BufReader } from "../io/bufio.ts";
|
||||
import { TextProtoReader } from "./mod.ts";
|
||||
import { stringsReader } from "../io/util.ts";
|
||||
import {
|
||||
assert,
|
||||
assertEquals,
|
||||
assertThrows,
|
||||
assertNotEOF,
|
||||
} from "../testing/asserts.ts";
|
||||
import { assert, assertEquals, assertThrows } from "../testing/asserts.ts";
|
||||
const { test } = Deno;
|
||||
|
||||
function reader(s: string): TextProtoReader {
|
||||
|
@ -31,7 +26,7 @@ test({
|
|||
test("[textproto] ReadEmpty", async () => {
|
||||
const r = reader("");
|
||||
const m = await r.readMIMEHeader();
|
||||
assertEquals(m, Deno.EOF);
|
||||
assertEquals(m, null);
|
||||
});
|
||||
|
||||
test("[textproto] Reader", async () => {
|
||||
|
@ -43,7 +38,7 @@ test("[textproto] Reader", async () => {
|
|||
assertEquals(s, "line2");
|
||||
|
||||
s = await r.readLine();
|
||||
assert(s === Deno.EOF);
|
||||
assert(s === null);
|
||||
});
|
||||
|
||||
test({
|
||||
|
@ -53,7 +48,8 @@ test({
|
|||
"my-key: Value 1 \r\nLong-key: Even Longer Value\r\nmy-Key: " +
|
||||
"Value 2\r\n\n";
|
||||
const r = reader(input);
|
||||
const m = assertNotEOF(await r.readMIMEHeader());
|
||||
const m = await r.readMIMEHeader();
|
||||
assert(m !== null);
|
||||
assertEquals(m.get("My-Key"), "Value 1, Value 2");
|
||||
assertEquals(m.get("Long-key"), "Even Longer Value");
|
||||
},
|
||||
|
@ -64,7 +60,8 @@ test({
|
|||
async fn(): Promise<void> {
|
||||
const input = "Foo: bar\n\n";
|
||||
const r = reader(input);
|
||||
const m = assertNotEOF(await r.readMIMEHeader());
|
||||
const m = await r.readMIMEHeader();
|
||||
assert(m !== null);
|
||||
assertEquals(m.get("Foo"), "bar");
|
||||
},
|
||||
});
|
||||
|
@ -74,7 +71,8 @@ test({
|
|||
async fn(): Promise<void> {
|
||||
const input = ": bar\ntest-1: 1\n\n";
|
||||
const r = reader(input);
|
||||
const m = assertNotEOF(await r.readMIMEHeader());
|
||||
const m = await r.readMIMEHeader();
|
||||
assert(m !== null);
|
||||
assertEquals(m.get("Test-1"), "1");
|
||||
},
|
||||
});
|
||||
|
@ -89,7 +87,8 @@ test({
|
|||
}
|
||||
const sdata = data.join("");
|
||||
const r = reader(`Cookie: ${sdata}\r\n\r\n`);
|
||||
const m = assertNotEOF(await r.readMIMEHeader());
|
||||
const m = await r.readMIMEHeader();
|
||||
assert(m !== null);
|
||||
assertEquals(m.get("Cookie"), sdata);
|
||||
},
|
||||
});
|
||||
|
@ -106,7 +105,8 @@ test({
|
|||
"Audio Mode : None\r\n" +
|
||||
"Privilege : 127\r\n\r\n";
|
||||
const r = reader(input);
|
||||
const m = assertNotEOF(await r.readMIMEHeader());
|
||||
const m = await r.readMIMEHeader();
|
||||
assert(m !== null);
|
||||
assertEquals(m.get("Foo"), "bar");
|
||||
assertEquals(m.get("Content-Language"), "en");
|
||||
// Make sure we drop headers with trailing whitespace
|
||||
|
@ -174,7 +174,8 @@ test({
|
|||
"------WebKitFormBoundaryimeZ2Le9LjohiUiG--\r\n\n",
|
||||
];
|
||||
const r = reader(input.join(""));
|
||||
const m = assertNotEOF(await r.readMIMEHeader());
|
||||
const m = await r.readMIMEHeader();
|
||||
assert(m !== null);
|
||||
assertEquals(m.get("Accept"), "*/*");
|
||||
assertEquals(m.get("Content-Disposition"), 'form-data; name="test"');
|
||||
},
|
||||
|
|
|
@ -102,7 +102,7 @@ const tpr = new TextProtoReader(new BufReader(Deno.stdin));
|
|||
while (true) {
|
||||
await Deno.stdout.write(encode("> "));
|
||||
const line = await tpr.readLine();
|
||||
if (line === Deno.EOF) {
|
||||
if (line === null) {
|
||||
break;
|
||||
}
|
||||
if (line === "close") {
|
||||
|
|
|
@ -31,7 +31,7 @@ const tpr = new TextProtoReader(new BufReader(Deno.stdin));
|
|||
while (true) {
|
||||
await Deno.stdout.write(encode("> "));
|
||||
const line = await tpr.readLine();
|
||||
if (line === Deno.EOF) {
|
||||
if (line === null) {
|
||||
break;
|
||||
}
|
||||
if (line === "close") {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Sha1 } from "../util/sha1.ts";
|
|||
import { writeResponse } from "../http/io.ts";
|
||||
import { TextProtoReader } from "../textproto/mod.ts";
|
||||
import { Deferred, deferred } from "../util/async.ts";
|
||||
import { assertNotEOF } from "../testing/asserts.ts";
|
||||
import { assert } from "../testing/asserts.ts";
|
||||
import { concat } from "../bytes/mod.ts";
|
||||
import Conn = Deno.Conn;
|
||||
import Writer = Deno.Writer;
|
||||
|
@ -149,7 +149,8 @@ export async function writeFrame(
|
|||
* @throws `Error` Frame is invalid
|
||||
*/
|
||||
export async function readFrame(buf: BufReader): Promise<WebSocketFrame> {
|
||||
let b = assertNotEOF(await buf.readByte());
|
||||
let b = await buf.readByte();
|
||||
assert(b !== null);
|
||||
let isLastFrame = false;
|
||||
switch (b >>> 4) {
|
||||
case 0b1000:
|
||||
|
@ -163,25 +164,28 @@ export async function readFrame(buf: BufReader): Promise<WebSocketFrame> {
|
|||
}
|
||||
const opcode = b & 0x0f;
|
||||
// has_mask & payload
|
||||
b = assertNotEOF(await buf.readByte());
|
||||
b = await buf.readByte();
|
||||
assert(b !== null);
|
||||
const hasMask = b >>> 7;
|
||||
let payloadLength = b & 0b01111111;
|
||||
if (payloadLength === 126) {
|
||||
const l = assertNotEOF(await readShort(buf));
|
||||
const l = await readShort(buf);
|
||||
assert(l !== null);
|
||||
payloadLength = l;
|
||||
} else if (payloadLength === 127) {
|
||||
const l = assertNotEOF(await readLong(buf));
|
||||
const l = await readLong(buf);
|
||||
assert(l !== null);
|
||||
payloadLength = Number(l);
|
||||
}
|
||||
// mask
|
||||
let mask: Uint8Array | undefined;
|
||||
if (hasMask) {
|
||||
mask = new Uint8Array(4);
|
||||
assertNotEOF(await buf.readFull(mask));
|
||||
assert((await buf.readFull(mask)) !== null);
|
||||
}
|
||||
// payload
|
||||
const payload = new Uint8Array(payloadLength);
|
||||
assertNotEOF(await buf.readFull(payload));
|
||||
assert((await buf.readFull(payload)) !== null);
|
||||
return {
|
||||
isLastFrame,
|
||||
opcode,
|
||||
|
@ -479,7 +483,7 @@ export async function handshake(
|
|||
|
||||
const tpReader = new TextProtoReader(bufReader);
|
||||
const statusLine = await tpReader.readLine();
|
||||
if (statusLine === Deno.EOF) {
|
||||
if (statusLine === null) {
|
||||
throw new Deno.errors.UnexpectedEof();
|
||||
}
|
||||
const m = statusLine.match(/^(?<version>\S+) (?<statusCode>\S+) /);
|
||||
|
@ -497,7 +501,7 @@ export async function handshake(
|
|||
}
|
||||
|
||||
const responseHeaders = await tpReader.readMIMEHeader();
|
||||
if (responseHeaders === Deno.EOF) {
|
||||
if (responseHeaders === null) {
|
||||
throw new Deno.errors.UnexpectedEof();
|
||||
}
|
||||
|
||||
|
|
|
@ -278,7 +278,7 @@ function dummyConn(r: Reader, w: Writer): Conn {
|
|||
rid: -1,
|
||||
closeRead: (): void => {},
|
||||
closeWrite: (): void => {},
|
||||
read: (x): Promise<number | Deno.EOF> => r.read(x),
|
||||
read: (x): Promise<number | null> => r.read(x),
|
||||
write: (x): Promise<number> => w.write(x),
|
||||
close: (): void => {},
|
||||
localAddr: { transport: "tcp", hostname: "0.0.0.0", port: 0 },
|
||||
|
@ -335,8 +335,8 @@ test("[ws] createSecKeyHasCorrectLength", () => {
|
|||
test("[ws] WebSocket should throw `Deno.errors.ConnectionReset` when peer closed connection without close frame", async () => {
|
||||
const buf = new Buffer();
|
||||
const eofReader: Deno.Reader = {
|
||||
read(_: Uint8Array): Promise<number | Deno.EOF> {
|
||||
return Promise.resolve(Deno.EOF);
|
||||
read(_: Uint8Array): Promise<number | null> {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
};
|
||||
const conn = dummyConn(eofReader, buf);
|
||||
|
@ -353,8 +353,8 @@ test("[ws] WebSocket should throw `Deno.errors.ConnectionReset` when peer closed
|
|||
test("[ws] WebSocket shouldn't throw `Deno.errors.UnexpectedEof` on recive()", async () => {
|
||||
const buf = new Buffer();
|
||||
const eofReader: Deno.Reader = {
|
||||
read(_: Uint8Array): Promise<number | Deno.EOF> {
|
||||
return Promise.resolve(Deno.EOF);
|
||||
read(_: Uint8Array): Promise<number | null> {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
};
|
||||
const conn = dummyConn(eofReader, buf);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue