diff options
Diffstat (limited to 'textproto')
-rw-r--r-- | textproto/mod.ts | 150 | ||||
-rw-r--r-- | textproto/test.ts | 98 |
2 files changed, 248 insertions, 0 deletions
diff --git a/textproto/mod.ts b/textproto/mod.ts new file mode 100644 index 000000000..ee7647296 --- /dev/null +++ b/textproto/mod.ts @@ -0,0 +1,150 @@ +// Based on https://github.com/golang/go/blob/891682/src/net/textproto/ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import { BufReader, BufState } from "../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 class TextProtoReader { + constructor(readonly r: BufReader) {} + + /** readLine() reads a single line from the TextProtoReader, + * eliding the final \n or \r\n from the returned string. + */ + async readLine(): Promise<[string, BufState]> { + let [line, err] = await this.readLineSlice(); + return [str(line), err]; + } + + /** ReadMIMEHeader reads a MIME-style header from r. + * The header is a sequence of possibly continued Key: Value lines + * ending in a blank line. + * The returned map m maps CanonicalMIMEHeaderKey(key) to a + * sequence of values in the same order encountered in the input. + * + * For example, consider this input: + * + * My-Key: Value 1 + * Long-Key: Even + * Longer Value + * My-Key: Value 2 + * + * Given that input, ReadMIMEHeader returns the map: + * + * map[string][]string{ + * "My-Key": {"Value 1", "Value 2"}, + * "Long-Key": {"Even Longer Value"}, + * } + */ + async readMIMEHeader(): Promise<[Headers, BufState]> { + let m = new Headers(); + let line: Uint8Array; + + // The first line cannot start with a leading space. + let [buf, err] = await this.r.peek(1); + if (buf[0] == charCode(" ") || buf[0] == charCode("\t")) { + [line, err] = await this.readLineSlice(); + } + + [buf, err] = await this.r.peek(1); + if (err == null && (buf[0] == charCode(" ") || buf[0] == charCode("\t"))) { + throw new ProtocolError( + `malformed MIME header initial line: ${str(line)}` + ); + } + + while (true) { + let [kv, err] = await this.readLineSlice(); // readContinuedLineSlice + if (kv.byteLength == 0) { + return [m, err]; + } + + // Key ends at first colon; should not have trailing spaces + // but they appear in the wild, violating specs, so we remove + // them if present. + let i = kv.indexOf(charCode(":")); + if (i < 0) { + throw new ProtocolError(`malformed MIME header line: ${str(kv)}`); + } + let endKey = i; + while (endKey > 0 && kv[endKey - 1] == charCode(" ")) { + endKey--; + } + + //let key = canonicalMIMEHeaderKey(kv.subarray(0, endKey)); + let key = str(kv.subarray(0, endKey)); + + // As per RFC 7230 field-name is a token, tokens consist of one or more chars. + // We could return a ProtocolError here, but better to be liberal in what we + // accept, so if we get an empty key, skip it. + if (key == "") { + continue; + } + + // Skip initial spaces in value. + i++; // skip colon + while ( + i < kv.byteLength && + (kv[i] == charCode(" ") || kv[i] == charCode("\t")) + ) { + i++; + } + let value = str(kv.subarray(i)); + + m.append(key, value); + + if (err != null) { + throw err; + } + } + } + + async readLineSlice(): Promise<[Uint8Array, BufState]> { + // this.closeDot(); + let line: Uint8Array; + while (true) { + let [l, more, err] = await this.r.readLine(); + if (err != null) { + // Go's len(typed nil) works fine, but not in JS + return [new Uint8Array(0), err]; + } + // Avoid the copy if the first call produced a full line. + if (line == null && !more) { + return [l, null]; + } + line = append(line, l); + if (!more) { + break; + } + } + return [line, null]; + } +} + +export function append(a: Uint8Array, b: Uint8Array): Uint8Array { + if (a == null) { + return b; + } else { + const output = new Uint8Array(a.length + b.length); + output.set(a, 0); + output.set(b, a.length); + return output; + } +} diff --git a/textproto/test.ts b/textproto/test.ts new file mode 100644 index 000000000..0f8bee227 --- /dev/null +++ b/textproto/test.ts @@ -0,0 +1,98 @@ +// Based on https://github.com/golang/go/blob/891682/src/net/textproto/ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +import { BufReader } from "../io/bufio.ts"; +import { TextProtoReader, append } from "./mod.ts"; +import { stringsReader } from "../io/util.ts"; +import { test, assert, assertEqual } from "../testing/mod.ts"; + +function reader(s: string): TextProtoReader { + return new TextProtoReader(new BufReader(stringsReader(s))); +} + +test(async function textprotoReader() { + let r = reader("line1\nline2\n"); + let [s, err] = await r.readLine(); + assertEqual(s, "line1"); + assert(err == null); + + [s, err] = await r.readLine(); + assertEqual(s, "line2"); + assert(err == null); + + [s, err] = await r.readLine(); + assertEqual(s, ""); + assert(err == "EOF"); +}); + +/* +test(async function textprotoReadMIMEHeader() { + let r = reader("my-key: Value 1 \r\nLong-key: Even \n Longer Value\r\nmy-Key: Value 2\r\n\n"); + let [m, err] = await r.readMIMEHeader(); + + console.log("Got headers", m.toString()); + want := MIMEHeader{ + "My-Key": {"Value 1", "Value 2"}, + "Long-Key": {"Even Longer Value"}, + } + if !reflect.DeepEqual(m, want) || err != nil { + t.Fatalf("ReadMIMEHeader: %v, %v; want %v", m, err, want) + } +}); +*/ + +test(async function textprotoReadMIMEHeaderSingle() { + let r = reader("Foo: bar\n\n"); + let [m, err] = await r.readMIMEHeader(); + assertEqual(m.get("Foo"), "bar"); + assert(!err); +}); + +// Test that we read slightly-bogus MIME headers seen in the wild, +// with spaces before colons, and spaces in keys. +test(async function textprotoReadMIMEHeaderNonCompliant() { + // Invalid HTTP response header as sent by an Axis security + // camera: (this is handled by IE, Firefox, Chrome, curl, etc.) + let r = reader( + "Foo: bar\r\n" + + "Content-Language: en\r\n" + + "SID : 0\r\n" + + // TODO Re-enable Currently fails with: + // "TypeError: audio mode is not a legal HTTP header name" + // "Audio Mode : None\r\n" + + "Privilege : 127\r\n\r\n" + ); + let [m, err] = await r.readMIMEHeader(); + console.log(m.toString()); + assert(!err); + /* + let want = MIMEHeader{ + "Foo": {"bar"}, + "Content-Language": {"en"}, + "Sid": {"0"}, + "Audio Mode": {"None"}, + "Privilege": {"127"}, + } + if !reflect.DeepEqual(m, want) || err != nil { + t.Fatalf("ReadMIMEHeader =\n%v, %v; want:\n%v", m, err, want) + } + */ +}); + +test(async function textprotoAppend() { + const enc = new TextEncoder(); + const dec = new TextDecoder(); + const u1 = enc.encode("Hello "); + const u2 = enc.encode("World"); + const joined = append(u1, u2); + assertEqual(dec.decode(joined), "Hello World"); +}); + +test(async function textprotoReadEmpty() { + let r = reader(""); + let [m, err] = await r.readMIMEHeader(); + // Should not crash! + assertEqual(err, "EOF"); +}); |