diff --git a/encoding/hex.ts b/encoding/hex.ts new file mode 100644 index 000000000000..83c88ac7836b --- /dev/null +++ b/encoding/hex.ts @@ -0,0 +1,137 @@ +// Ported from Go +// https://github.com/golang/go/blob/go1.12.5/src/encoding/hex/hex.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. +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. + +const hextable = new TextEncoder().encode("0123456789abcdef"); + +export function errInvalidByte(byte: number): Error { + return new Error( + "encoding/hex: invalid byte: " + + new TextDecoder().decode(new Uint8Array([byte])) + ); +} + +export function errLength(): Error { + return new Error("encoding/hex: odd length hex string"); +} + +// fromHexChar converts a hex character into its value and a success flag. +function fromHexChar(byte: number): [number, boolean] { + switch (true) { + case 48 <= byte && byte <= 57: // '0' <= byte && byte <= '9' + return [byte - 48, true]; + case 97 <= byte && byte <= 102: // 'a' <= byte && byte <= 'f' + return [byte - 97 + 10, true]; + case 65 <= byte && byte <= 70: // 'A' <= byte && byte <= 'F' + return [byte - 65 + 10, true]; + } + return [0, false]; +} + +/** + * EncodedLen returns the length of an encoding of n source bytes. Specifically, it returns n * 2. + * @param n + */ +export function encodedLen(n: number): number { + return n * 2; +} + +/** + * Encode encodes `src` into `encodedLen(src.length)` bytes of `dst`. + * As a convenience, it returns the number of bytes written to `dst` + * but this value is always `encodedLen(src.length)`. + * Encode implements hexadecimal encoding. + * @param dst + * @param src + */ +export function encode(dst: Uint8Array, src: Uint8Array): number { + const srcLength = encodedLen(src.length); + if (dst.length !== srcLength) { + throw new Error("Out of index."); + } + for (let i = 0; i < src.length; i++) { + const v = src[i]; + dst[i * 2] = hextable[v >> 4]; + dst[i * 2 + 1] = hextable[v & 0x0f]; + } + return srcLength; +} + +/** + * EncodeToString returns the hexadecimal encoding of `src`. + * @param src + */ +export function encodeToString(src: Uint8Array): string { + const dest = new Uint8Array(encodedLen(src.length)); + encode(dest, src); + return new TextDecoder().decode(dest); +} + +/** + * Decode decodes `src` into `decodedLen(src.length)` bytes + * returning the actual number of bytes written to `dst`. + * Decode expects that `src` contains only hexadecimal characters and that `src` has even length. + * If the input is malformed, Decode returns the number of bytes decoded before the error. + * @param dst + * @param src + */ +export function decode( + dst: Uint8Array, + src: Uint8Array +): [number, Error | void] { + var i = 0; + for (; i < Math.floor(src.length / 2); i++) { + const [a, aOK] = fromHexChar(src[i * 2]); + if (!aOK) { + return [i, errInvalidByte(src[i * 2])]; + } + const [b, bOK] = fromHexChar(src[i * 2 + 1]); + if (!bOK) { + return [i, errInvalidByte(src[i * 2 + 1])]; + } + + dst[i] = (a << 4) | b; + } + + if (src.length % 2 == 1) { + // Check for invalid char before reporting bad length, + // since the invalid char (if present) is an earlier problem. + const [, ok] = fromHexChar(src[i * 2]); + if (!ok) { + return [i, errInvalidByte(src[i * 2])]; + } + return [i, errLength()]; + } + + return [i, undefined]; +} + +/** + * DecodedLen returns the length of a decoding of `x` source bytes. Specifically, it returns `x / 2`. + * @param x + */ +export function decodedLen(x: number): number { + return Math.floor(x / 2); +} + +/** + * DecodeString returns the bytes represented by the hexadecimal string `s`. + * DecodeString expects that src contains only hexadecimal characters and that src has even length. + * If the input is malformed, DecodeString will throws an error. + * @param s the `string` need to decode to `Uint8Array` + */ +export function decodeString(s: string): Uint8Array { + const src = new TextEncoder().encode(s); + // We can use the source slice itself as the destination + // because the decode loop increments by one and then the 'seen' byte is not used anymore. + const [n, err] = decode(src, src); + + if (err) { + throw err; + } + + return src.slice(0, n); +} diff --git a/encoding/hex_test.ts b/encoding/hex_test.ts new file mode 100644 index 000000000000..5ea81ebdbcd6 --- /dev/null +++ b/encoding/hex_test.ts @@ -0,0 +1,182 @@ +// Ported from Go +// https://github.com/golang/go/blob/go1.12.5/src/encoding/hex/hex.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. +// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import { test, runIfMain } from "../testing/mod.ts"; +import { assertEquals, assertThrows } from "../testing/asserts.ts"; + +import { + encodedLen, + encode, + encodeToString, + decodedLen, + decode, + decodeString, + errLength, + errInvalidByte +} from "./hex.ts"; + +function toByte(s: string): number { + return new TextEncoder().encode(s)[0]; +} + +const testCases = [ + // encoded(hex) / decoded(Uint8Array) + ["", []], + ["0001020304050607", [0, 1, 2, 3, 4, 5, 6, 7]], + ["08090a0b0c0d0e0f", [8, 9, 10, 11, 12, 13, 14, 15]], + ["f0f1f2f3f4f5f6f7", [0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7]], + ["f8f9fafbfcfdfeff", [0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff]], + ["67", Array.from(new TextEncoder().encode("g"))], + ["e3a1", [0xe3, 0xa1]] +]; + +const errCases = [ + // encoded(hex) / error + ["", "", undefined], + ["0", "", errLength()], + ["zd4aa", "", errInvalidByte(toByte("z"))], + ["d4aaz", "\xd4\xaa", errInvalidByte(toByte("z"))], + ["30313", "01", errLength()], + ["0g", "", errInvalidByte(new TextEncoder().encode("g")[0])], + ["00gg", "\x00", errInvalidByte(new TextEncoder().encode("g")[0])], + ["0\x01", "", errInvalidByte(new TextEncoder().encode("\x01")[0])], + ["ffeed", "\xff\xee", errLength()] +]; + +test({ + name: "[encoding.hex] encodedLen", + fn(): void { + assertEquals(encodedLen(0), 0); + assertEquals(encodedLen(1), 2); + assertEquals(encodedLen(2), 4); + assertEquals(encodedLen(3), 6); + assertEquals(encodedLen(4), 8); + } +}); + +test({ + name: "[encoding.hex] encode", + fn(): void { + { + const srcStr = "abc"; + const src = new TextEncoder().encode(srcStr); + const dest = new Uint8Array(encodedLen(src.length)); + const int = encode(dest, src); + assertEquals(src, new Uint8Array([97, 98, 99])); + assertEquals(int, 6); + } + + { + const srcStr = "abc"; + const src = new TextEncoder().encode(srcStr); + const dest = new Uint8Array(2); // out of index + assertThrows( + (): void => { + encode(dest, src); + }, + Error, + "Out of index." + ); + } + + for (const [enc, dec] of testCases) { + const dest = new Uint8Array(encodedLen(dec.length)); + const src = new Uint8Array(dec as number[]); + const n = encode(dest, src); + assertEquals(dest.length, n); + assertEquals(new TextDecoder().decode(dest), enc); + } + } +}); + +test({ + name: "[encoding.hex] encodeToString", + fn(): void { + for (const [enc, dec] of testCases) { + assertEquals(encodeToString(new Uint8Array(dec as number[])), enc); + } + } +}); + +test({ + name: "[encoding.hex] decodedLen", + fn(): void { + assertEquals(decodedLen(0), 0); + assertEquals(decodedLen(2), 1); + assertEquals(decodedLen(4), 2); + assertEquals(decodedLen(6), 3); + assertEquals(decodedLen(8), 4); + } +}); + +test({ + name: "[encoding.hex] decode", + fn(): void { + // Case for decoding uppercase hex characters, since + // Encode always uses lowercase. + const extraTestcase = [ + ["F8F9FAFBFCFDFEFF", [0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff]] + ]; + + const cases = testCases.concat(extraTestcase); + + for (const [enc, dec] of cases) { + const dest = new Uint8Array(decodedLen(enc.length)); + const src = new TextEncoder().encode(enc as string); + const [, err] = decode(dest, src); + assertEquals(err, undefined); + assertEquals(Array.from(dest), Array.from(dec as number[])); + } + } +}); + +test({ + name: "[encoding.hex] decodeString", + fn(): void { + for (const [enc, dec] of testCases) { + const dst = decodeString(enc as string); + + assertEquals(dec, Array.from(dst)); + } + } +}); + +test({ + name: "[encoding.hex] decode error", + fn(): void { + for (const [input, output, expectedErr] of errCases) { + const out = new Uint8Array((input as string).length + 10); + const [n, err] = decode(out, new TextEncoder().encode(input as string)); + assertEquals( + new TextDecoder("ascii").decode(out.slice(0, n)), + output as string + ); + assertEquals(err, expectedErr); + } + } +}); + +test({ + name: "[encoding.hex] decodeString error", + fn(): void { + for (const [input, output, expectedErr] of errCases) { + if (expectedErr) { + assertThrows( + (): void => { + decodeString(input as string); + }, + Error, + (expectedErr as Error).message + ); + } else { + const out = decodeString(input as string); + assertEquals(new TextDecoder("ascii").decode(out), output as string); + } + } + } +}); + +runIfMain(import.meta); diff --git a/encoding/test.ts b/encoding/test.ts index e7f779c866f6..c7a1c9716007 100644 --- a/encoding/test.ts +++ b/encoding/test.ts @@ -1,3 +1,4 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. +import "./hex_test.ts"; import "./toml_test.ts"; import "./csv_test.ts"; diff --git a/test.ts b/test.ts index c1381cb27609..5747be1c5e7b 100755 --- a/test.ts +++ b/test.ts @@ -22,6 +22,7 @@ import "./testing/test.ts"; import "./textproto/test.ts"; import "./util/test.ts"; import "./ws/test.ts"; +import "./encoding/test.ts"; import { xrun } from "./prettier/util.ts"; import { red, green } from "./colors/mod.ts";