diff options
Diffstat (limited to 'std/textproto')
-rw-r--r-- | std/textproto/mod.ts | 174 | ||||
-rw-r--r-- | std/textproto/reader_test.ts | 184 | ||||
-rw-r--r-- | std/textproto/test.ts | 17 |
3 files changed, 375 insertions, 0 deletions
diff --git a/std/textproto/mod.ts b/std/textproto/mod.ts new file mode 100644 index 000000000..22153d17c --- /dev/null +++ b/std/textproto/mod.ts @@ -0,0 +1,174 @@ +// Based on https://github.com/golang/go/blob/891682/src/net/textproto/ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import { BufReader, UnexpectedEOFError } from "../io/bufio.ts"; +import { charCode } from "../io/util.ts"; + +const asciiDecoder = new TextDecoder(); +function str(buf: Uint8Array): string { + if (buf == null) { + return ""; + } else { + return asciiDecoder.decode(buf); + } +} + +export class ProtocolError extends Error { + constructor(msg: string) { + super(msg); + this.name = "ProtocolError"; + } +} + +export function append(a: Uint8Array, b: Uint8Array): Uint8Array { + if (a == null) { + return b; + } else { + const output = new Uint8Array(a.length + b.length); + output.set(a, 0); + output.set(b, a.length); + return output; + } +} + +export class TextProtoReader { + constructor(readonly r: BufReader) {} + + /** readLine() reads a single line from the TextProtoReader, + * eliding the final \n or \r\n from the returned string. + */ + async readLine(): Promise<string | Deno.EOF> { + const s = await this.readLineSlice(); + if (s === Deno.EOF) return Deno.EOF; + return str(s); + } + + /** ReadMIMEHeader reads a MIME-style header from r. + * The header is a sequence of possibly continued Key: Value lines + * ending in a blank line. + * The returned map m maps CanonicalMIMEHeaderKey(key) to a + * sequence of values in the same order encountered in the input. + * + * For example, consider this input: + * + * My-Key: Value 1 + * Long-Key: Even + * Longer Value + * My-Key: Value 2 + * + * Given that input, ReadMIMEHeader returns the map: + * + * map[string][]string{ + * "My-Key": {"Value 1", "Value 2"}, + * "Long-Key": {"Even Longer Value"}, + * } + */ + async readMIMEHeader(): Promise<Headers | Deno.EOF> { + const m = new Headers(); + let line: Uint8Array; + + // The first line cannot start with a leading space. + let buf = await this.r.peek(1); + if (buf === Deno.EOF) { + return Deno.EOF; + } 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) { + throw new UnexpectedEOFError(); + } else if (buf[0] == charCode(" ") || buf[0] == charCode("\t")) { + throw new ProtocolError( + `malformed MIME header initial line: ${str(line!)}` + ); + } + + while (true) { + const kv = await this.readLineSlice(); // readContinuedLineSlice + if (kv === Deno.EOF) throw new UnexpectedEOFError(); + if (kv.byteLength === 0) return m; + + // Key ends at first colon; should not have trailing spaces + // but they appear in the wild, violating specs, so we remove + // them if present. + let i = kv.indexOf(charCode(":")); + if (i < 0) { + throw new ProtocolError(`malformed MIME header line: ${str(kv)}`); + } + let endKey = i; + while (endKey > 0 && kv[endKey - 1] == charCode(" ")) { + endKey--; + } + + //let key = canonicalMIMEHeaderKey(kv.subarray(0, endKey)); + const key = str(kv.subarray(0, endKey)); + + // As per RFC 7230 field-name is a token, + // tokens consist of one or more chars. + // We could return a ProtocolError here, + // but better to be liberal in what we + // accept, so if we get an empty key, skip it. + if (key == "") { + continue; + } + + // Skip initial spaces in value. + i++; // skip colon + while ( + i < kv.byteLength && + (kv[i] == charCode(" ") || kv[i] == charCode("\t")) + ) { + i++; + } + const value = str(kv.subarray(i)); + + // In case of invalid header we swallow the error + // example: "Audio Mode" => invalid due to space in the key + try { + m.append(key, value); + } catch {} + } + } + + async readLineSlice(): Promise<Uint8Array | Deno.EOF> { + // this.closeDot(); + let line: Uint8Array; + while (true) { + const r = await this.r.readLine(); + if (r === Deno.EOF) return Deno.EOF; + const { line: l, more } = r; + + // Avoid the copy if the first call produced a full line. + if (!line! && !more) { + // TODO(ry): + // This skipSpace() is definitely misplaced, but I don't know where it + // comes from nor how to fix it. + if (this.skipSpace(l) === 0) { + return new Uint8Array(0); + } + return l; + } + + // @ts-ignore + line = append(line, l); + if (!more) { + break; + } + } + return line; + } + + skipSpace(l: Uint8Array): number { + let n = 0; + for (let i = 0; i < l.length; i++) { + if (l[i] === charCode(" ") || l[i] === charCode("\t")) { + continue; + } + n++; + } + return n; + } +} diff --git a/std/textproto/reader_test.ts b/std/textproto/reader_test.ts new file mode 100644 index 000000000..fe842e0e2 --- /dev/null +++ b/std/textproto/reader_test.ts @@ -0,0 +1,184 @@ +// Based on https://github.com/golang/go/blob/master/src/net/textproto/reader_test.go +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import { BufReader } from "../io/bufio.ts"; +import { TextProtoReader, ProtocolError } from "./mod.ts"; +import { stringsReader } from "../io/util.ts"; +import { + assert, + assertEquals, + assertNotEquals, + assertThrows +} from "../testing/asserts.ts"; +import { test, runIfMain } from "../testing/mod.ts"; + +function assertNotEOF<T extends {}>(val: T | Deno.EOF): T { + assertNotEquals(val, Deno.EOF); + return val as T; +} + +function reader(s: string): TextProtoReader { + return new TextProtoReader(new BufReader(stringsReader(s))); +} +// test({ +// name: "[textproto] Reader : DotBytes", +// async fn(): Promise<void> { +// const input = +// "dotlines\r\n.foo\r\n..bar\n...baz\nquux\r\n\r\n.\r\nanot.her\r\n"; +// } +// }); + +test(async function textprotoReadEmpty(): Promise<void> { + const r = reader(""); + const m = await r.readMIMEHeader(); + assertEquals(m, Deno.EOF); +}); + +test(async function textprotoReader(): Promise<void> { + const r = reader("line1\nline2\n"); + let s = await r.readLine(); + assertEquals(s, "line1"); + + s = await r.readLine(); + assertEquals(s, "line2"); + + s = await r.readLine(); + assert(s === Deno.EOF); +}); + +test({ + name: "[textproto] Reader : MIME Header", + async fn(): Promise<void> { + const input = + "my-key: Value 1 \r\nLong-key: Even Longer Value\r\nmy-Key: " + + "Value 2\r\n\n"; + const r = reader(input); + const m = assertNotEOF(await r.readMIMEHeader()); + assertEquals(m.get("My-Key"), "Value 1, Value 2"); + assertEquals(m.get("Long-key"), "Even Longer Value"); + } +}); + +test({ + name: "[textproto] Reader : MIME Header Single", + async fn(): Promise<void> { + const input = "Foo: bar\n\n"; + const r = reader(input); + const m = assertNotEOF(await r.readMIMEHeader()); + assertEquals(m.get("Foo"), "bar"); + } +}); + +test({ + name: "[textproto] Reader : MIME Header No Key", + async fn(): Promise<void> { + const input = ": bar\ntest-1: 1\n\n"; + const r = reader(input); + const m = assertNotEOF(await r.readMIMEHeader()); + assertEquals(m.get("Test-1"), "1"); + } +}); + +test({ + name: "[textproto] Reader : Large MIME Header", + async fn(): Promise<void> { + const data: string[] = []; + // Go test is 16*1024. But seems it can't handle more + for (let i = 0; i < 1024; i++) { + data.push("x"); + } + const sdata = data.join(""); + const r = reader(`Cookie: ${sdata}\r\n\r\n`); + const m = assertNotEOF(await r.readMIMEHeader()); + assertEquals(m.get("Cookie"), sdata); + } +}); + +// Test that we read slightly-bogus MIME headers seen in the wild, +// with spaces before colons, and spaces in keys. +test({ + name: "[textproto] Reader : MIME Header Non compliant", + async fn(): Promise<void> { + const input = + "Foo: bar\r\n" + + "Content-Language: en\r\n" + + "SID : 0\r\n" + + "Audio Mode : None\r\n" + + "Privilege : 127\r\n\r\n"; + const r = reader(input); + const m = assertNotEOF(await r.readMIMEHeader()); + assertEquals(m.get("Foo"), "bar"); + assertEquals(m.get("Content-Language"), "en"); + assertEquals(m.get("SID"), "0"); + assertEquals(m.get("Privilege"), "127"); + // Not a legal http header + assertThrows((): void => { + assertEquals(m.get("Audio Mode"), "None"); + }); + } +}); + +test({ + name: "[textproto] Reader : MIME Header Malformed", + async fn(): Promise<void> { + const input = [ + "No colon first line\r\nFoo: foo\r\n\r\n", + " No colon first line with leading space\r\nFoo: foo\r\n\r\n", + "\tNo colon first line with leading tab\r\nFoo: foo\r\n\r\n", + " First: line with leading space\r\nFoo: foo\r\n\r\n", + "\tFirst: line with leading tab\r\nFoo: foo\r\n\r\n", + "Foo: foo\r\nNo colon second line\r\n\r\n" + ]; + const r = reader(input.join("")); + + let err; + try { + await r.readMIMEHeader(); + } catch (e) { + err = e; + } + assert(err instanceof ProtocolError); + } +}); + +test({ + name: "[textproto] Reader : MIME Header Trim Continued", + async fn(): Promise<void> { + const input = + "" + // for code formatting purpose. + "a:\n" + + " 0 \r\n" + + "b:1 \t\r\n" + + "c: 2\r\n" + + " 3\t\n" + + " \t 4 \r\n\n"; + const r = reader(input); + let err; + try { + await r.readMIMEHeader(); + } catch (e) { + err = e; + } + assert(err instanceof ProtocolError); + } +}); + +test({ + name: "[textproto] #409 issue : multipart form boundary", + async fn(): Promise<void> { + const input = [ + "Accept: */*\r\n", + 'Content-Disposition: form-data; name="test"\r\n', + " \r\n", + "------WebKitFormBoundaryimeZ2Le9LjohiUiG--\r\n\n" + ]; + const r = reader(input.join("")); + const m = assertNotEOF(await r.readMIMEHeader()); + assertEquals(m.get("Accept"), "*/*"); + assertEquals(m.get("Content-Disposition"), 'form-data; name="test"'); + } +}); + +runIfMain(import.meta); diff --git a/std/textproto/test.ts b/std/textproto/test.ts new file mode 100644 index 000000000..bdb929369 --- /dev/null +++ b/std/textproto/test.ts @@ -0,0 +1,17 @@ +// Based on https://github.com/golang/go/blob/891682/src/net/textproto/ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import { append } from "./mod.ts"; +import { assertEquals } from "../testing/asserts.ts"; +import { test } from "../testing/mod.ts"; + +test(async function textprotoAppend(): Promise<void> { + const enc = new TextEncoder(); + const dec = new TextDecoder(); + const u1 = enc.encode("Hello "); + const u2 = enc.encode("World"); + const joined = append(u1, u2); + assertEquals(dec.decode(joined), "Hello World"); +}); |