BREAKING(std/encoding/hex): simplify API (#6690)

This commit is contained in:
Marcos Casagrande 2020-07-09 22:50:19 +02:00 committed by GitHub
parent 634d6af7a1
commit dc6b3ef714
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 56 additions and 118 deletions

View file

@ -18,17 +18,16 @@ export function errLength(): Error {
return new Error("encoding/hex: odd length hex string"); return new Error("encoding/hex: odd length hex string");
} }
// fromHexChar converts a hex character into its value and a success flag. // fromHexChar converts a hex character into its value.
function fromHexChar(byte: number): [number, boolean] { function fromHexChar(byte: number): number {
switch (true) { // '0' <= byte && byte <= '9'
case 48 <= byte && byte <= 57: // '0' <= byte && byte <= '9' if (48 <= byte && byte <= 57) return byte - 48;
return [byte - 48, true]; // 'a' <= byte && byte <= 'f'
case 97 <= byte && byte <= 102: // 'a' <= byte && byte <= 'f' if (97 <= byte && byte <= 102) return byte - 97 + 10;
return [byte - 97 + 10, true]; // 'A' <= byte && byte <= 'F'
case 65 <= byte && byte <= 70: // 'A' <= byte && byte <= 'F' if (65 <= byte && byte <= 70) return byte - 65 + 10;
return [byte - 65 + 10, true];
} throw errInvalidByte(byte);
return [0, false];
} }
/** /**
@ -41,24 +40,17 @@ export function encodedLen(n: number): number {
} }
/** /**
* Encode encodes `src` into `encodedLen(src.length)` bytes of `dst`. * Encode encodes `src` into `encodedLen(src.length)` bytes.
* 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 * @param src
*/ */
export function encode(src: Uint8Array, dst: Uint8Array): number { export function encode(src: Uint8Array): Uint8Array {
const srcLength = encodedLen(src.length); const dst = new Uint8Array(encodedLen(src.length));
if (dst.length !== srcLength) { for (let i = 0; i < dst.length; i++) {
throw new Error("Out of index.");
}
for (let i = 0; i < src.length; i++) {
const v = src[i]; const v = src[i];
dst[i * 2] = hextable[v >> 4]; dst[i * 2] = hextable[v >> 4];
dst[i * 2 + 1] = hextable[v & 0x0f]; dst[i * 2 + 1] = hextable[v & 0x0f];
} }
return srcLength; return dst;
} }
/** /**
@ -66,78 +58,49 @@ export function encode(src: Uint8Array, dst: Uint8Array): number {
* @param src * @param src
*/ */
export function encodeToString(src: Uint8Array): string { export function encodeToString(src: Uint8Array): string {
const dest = new Uint8Array(encodedLen(src.length)); return new TextDecoder().decode(encode(src));
encode(src, dest);
return new TextDecoder().decode(dest);
} }
/** /**
* Decode decodes `src` into `decodedLen(src.length)` bytes * Decode decodes `src` into `decodedLen(src.length)` bytes
* returning the actual number of bytes written to `dst`. * If the input is malformed an error will be thrown
* 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. * the error.
* @param dst
* @param src * @param src
*/ */
export function decode( export function decode(src: Uint8Array): Uint8Array {
src: Uint8Array, const dst = new Uint8Array(decodedLen(src.length));
dst: Uint8Array for (let i = 0; i < dst.length; i++) {
): [number, Error | void] { const a = fromHexChar(src[i * 2]);
let i = 0; const b = fromHexChar(src[i * 2 + 1]);
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; dst[i] = (a << 4) | b;
} }
if (src.length % 2 == 1) { if (src.length % 2 == 1) {
// Check for invalid char before reporting bad length, // Check for invalid char before reporting bad length,
// since the invalid char (if present) is an earlier problem. // since the invalid char (if present) is an earlier problem.
const [, ok] = fromHexChar(src[i * 2]); fromHexChar(src[dst.length * 2]);
if (!ok) { throw errLength();
return [i, errInvalidByte(src[i * 2])];
}
return [i, errLength()];
} }
return [i, undefined]; return dst;
} }
/** /**
* DecodedLen returns the length of a decoding of `x` source bytes. * DecodedLen returns the length of decoding `x` source bytes.
* Specifically, it returns `x / 2`. * Specifically, it returns `x / 2`.
* @param x * @param x
*/ */
export function decodedLen(x: number): number { export function decodedLen(x: number): number {
return Math.floor(x / 2); return x >>> 1;
} }
/** /**
* DecodeString returns the bytes represented by the hexadecimal string `s`. * DecodeString returns the bytes represented by the hexadecimal string `s`.
* DecodeString expects that src contains only hexadecimal characters and that * DecodeString expects that src contains only hexadecimal characters and that
* src has even length. * src has even length.
* If the input is malformed, DecodeString will throws an error. * If the input is malformed, DecodeString will throw an error.
* @param s the `string` need to decode to `Uint8Array` * @param s the `string` to decode to `Uint8Array`
*/ */
export function decodeString(s: string): Uint8Array { export function decodeString(s: string): Uint8Array {
const src = new TextEncoder().encode(s); return decode(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);
} }

View file

@ -34,15 +34,14 @@ const testCases = [
const errCases = [ const errCases = [
// encoded(hex) / error // encoded(hex) / error
["", "", undefined], ["0", errLength()],
["0", "", errLength()], ["zd4aa", errInvalidByte(toByte("z"))],
["zd4aa", "", errInvalidByte(toByte("z"))], ["d4aaz", errInvalidByte(toByte("z"))],
["d4aaz", "\xd4\xaa", errInvalidByte(toByte("z"))], ["30313", errLength()],
["30313", "01", errLength()], ["0g", errInvalidByte(new TextEncoder().encode("g")[0])],
["0g", "", errInvalidByte(new TextEncoder().encode("g")[0])], ["00gg", errInvalidByte(new TextEncoder().encode("g")[0])],
["00gg", "\x00", errInvalidByte(new TextEncoder().encode("g")[0])], ["0\x01", errInvalidByte(new TextEncoder().encode("\x01")[0])],
["0\x01", "", errInvalidByte(new TextEncoder().encode("\x01")[0])], ["ffeed", errLength()],
["ffeed", "\xff\xee", errLength()],
]; ];
Deno.test({ Deno.test({
@ -62,30 +61,15 @@ Deno.test({
{ {
const srcStr = "abc"; const srcStr = "abc";
const src = new TextEncoder().encode(srcStr); const src = new TextEncoder().encode(srcStr);
const dest = new Uint8Array(encodedLen(src.length)); const dest = encode(src);
const int = encode(src, dest);
assertEquals(src, new Uint8Array([97, 98, 99])); assertEquals(src, new Uint8Array([97, 98, 99]));
assertEquals(int, 6); assertEquals(dest.length, 6);
}
{
const srcStr = "abc";
const src = new TextEncoder().encode(srcStr);
const dest = new Uint8Array(2); // out of index
assertThrows(
(): void => {
encode(src, dest);
},
Error,
"Out of index."
);
} }
for (const [enc, dec] of testCases) { for (const [enc, dec] of testCases) {
const dest = new Uint8Array(encodedLen(dec.length));
const src = new Uint8Array(dec as number[]); const src = new Uint8Array(dec as number[]);
const n = encode(src, dest); const dest = encode(src);
assertEquals(dest.length, n); assertEquals(dest.length, src.length * 2);
assertEquals(new TextDecoder().decode(dest), enc); assertEquals(new TextDecoder().decode(dest), enc);
} }
}, },
@ -123,10 +107,8 @@ Deno.test({
const cases = testCases.concat(extraTestcase); const cases = testCases.concat(extraTestcase);
for (const [enc, dec] of cases) { for (const [enc, dec] of cases) {
const dest = new Uint8Array(decodedLen(enc.length));
const src = new TextEncoder().encode(enc as string); const src = new TextEncoder().encode(enc as string);
const [, err] = decode(src, dest); const dest = decode(src);
assertEquals(err, undefined);
assertEquals(Array.from(dest), Array.from(dec as number[])); assertEquals(Array.from(dest), Array.from(dec as number[]));
} }
}, },
@ -146,14 +128,12 @@ Deno.test({
Deno.test({ Deno.test({
name: "[encoding.hex] decode error", name: "[encoding.hex] decode error",
fn(): void { fn(): void {
for (const [input, output, expectedErr] of errCases) { for (const [input, expectedErr] of errCases) {
const out = new Uint8Array((input as string).length + 10); assertThrows(
const [n, err] = decode(new TextEncoder().encode(input as string), out); () => decode(new TextEncoder().encode(input as string)),
assertEquals( Error,
new TextDecoder("ascii").decode(out.slice(0, n)), (expectedErr as Error).message
output as string
); );
assertEquals(err, expectedErr);
} }
}, },
}); });
@ -161,19 +141,14 @@ Deno.test({
Deno.test({ Deno.test({
name: "[encoding.hex] decodeString error", name: "[encoding.hex] decodeString error",
fn(): void { fn(): void {
for (const [input, output, expectedErr] of errCases) { for (const [input, expectedErr] of errCases) {
if (expectedErr) { assertThrows(
assertThrows( (): void => {
(): void => { decodeString(input as string);
decodeString(input as string); },
}, Error,
Error, (expectedErr as Error).message
(expectedErr as Error).message );
);
} else {
const out = decodeString(input as string);
assertEquals(new TextDecoder("ascii").decode(out), output as string);
}
} }
}, },
}); });