From 2c4e9178f9dbc1e7340890e39119014c151dfa4c Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 9 Mar 2023 18:14:44 +0100 Subject: [PATCH] Migrate to string-based escaping implementation --- packages/amino/src/signdoc.spec.ts | 41 +++++++++++++++++++++---- packages/amino/src/signdoc.ts | 49 +++++++++++++----------------- 2 files changed, 56 insertions(+), 34 deletions(-) diff --git a/packages/amino/src/signdoc.spec.ts b/packages/amino/src/signdoc.spec.ts index 0ae5075d..e4c1261c 100644 --- a/packages/amino/src/signdoc.spec.ts +++ b/packages/amino/src/signdoc.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Random } from "@cosmjs/crypto"; -import { fromUtf8, toBech32, toUtf8 } from "@cosmjs/encoding"; +import { toBech32 } from "@cosmjs/encoding"; import { AminoMsg, escapeCharacters, makeSignDoc, sortedJsonStringify } from "./signdoc"; @@ -133,13 +133,42 @@ describe("encoding", () => { }); }); - describe("escape characters after utf8 encoding", () => { + describe("escapeCharacters", () => { it("works", () => { - const test = JSON.stringify({ memo: "ampersand:&,lt:<,gt:>" }); - const utf8Encoding = toUtf8(test); - const escapedEncoding = escapeCharacters(utf8Encoding); + // Unchanged originals + expect(escapeCharacters(`""`)).toEqual(`""`); + expect(escapeCharacters(`{}`)).toEqual(`{}`); + expect(escapeCharacters(`[]`)).toEqual(`[]`); + expect(escapeCharacters(`[123,null,"foo",[{}]]`)).toEqual(`[123,null,"foo",[{}]]`); + expect(escapeCharacters(`{"num":123}`)).toEqual(`{"num":123}`); + expect(escapeCharacters(`{"memo":"123"}`)).toEqual(`{"memo":"123"}`); + expect(escapeCharacters(`{"memo":"\\u0026"}`)).toEqual(`{"memo":"\\u0026"}`); - expect(JSON.parse(fromUtf8(utf8Encoding))).toEqual(JSON.parse(fromUtf8(escapedEncoding))); + // Escapes one + expect(escapeCharacters(`{"m":"with amp: &"}`)).toEqual(`{"m":"with amp: \\u0026"}`); + expect(escapeCharacters(`{"m":"with lt: <"}`)).toEqual(`{"m":"with lt: \\u003c"}`); + expect(escapeCharacters(`{"m":"with gt: >"}`)).toEqual(`{"m":"with gt: \\u003e"}`); + // Escapes multiple + expect(escapeCharacters(`{"m":"with amp: &&"}`)).toEqual(`{"m":"with amp: \\u0026\\u0026"}`); + expect(escapeCharacters(`{"m":"with lt: <<"}`)).toEqual(`{"m":"with lt: \\u003c\\u003c"}`); + expect(escapeCharacters(`{"m":"with gt: >>"}`)).toEqual(`{"m":"with gt: \\u003e\\u003e"}`); + expect(escapeCharacters(`{"m":"with all: &<>"}`)).toEqual(`{"m":"with all: \\u0026\\u003c\\u003e"}`); + }); + + it("escaped encoding can be decoded to the same document", () => { + const docs = [ + { memo: "ampersand:&,lt:<,gt:>", value: 123.421 }, + "", + 123, + ["foo", "ampersand:&,lt:<,gt:>"], + ]; + + for (const doc of docs) { + const normalEncoding = JSON.stringify(doc); + const escapedEncoding = escapeCharacters(normalEncoding); + expect(JSON.parse(escapedEncoding)).toEqual(JSON.parse(normalEncoding)); + expect(JSON.parse(escapedEncoding)).toEqual(doc); + } }); }); }); diff --git a/packages/amino/src/signdoc.ts b/packages/amino/src/signdoc.ts index d4417076..3a6f56c1 100644 --- a/packages/amino/src/signdoc.ts +++ b/packages/amino/src/signdoc.ts @@ -72,35 +72,28 @@ export function makeSignDoc( }; } -export function escapeCharacters(encodedArray: Uint8Array): Uint8Array { - const AmpersandUnicode = new Uint8Array([92, 117, 48, 48, 50, 54]); - const LtSignUnicode = new Uint8Array([92, 117, 48, 48, 51, 99]); - const GtSignUnicode = new Uint8Array([92, 117, 48, 48, 51, 101]); - - const AmpersandAscii = 38; - const LtSign = 60; // < - const GtSign = 62; // > - - const filteredIndex: number[] = []; - encodedArray.forEach((value, index) => { - if (value === AmpersandAscii || value === LtSign || value === GtSign) filteredIndex.push(index); - }); - - let result = new Uint8Array([...encodedArray]); - const reversedFilteredIndex = filteredIndex.reverse(); - reversedFilteredIndex.forEach((value) => { - let unicode = AmpersandUnicode; - if (result[value] === LtSign) { - unicode = LtSignUnicode; - } else if (result[value] === GtSign) { - unicode = GtSignUnicode; - } - result = new Uint8Array([...result.slice(0, value), ...unicode, ...result.slice(value + 1)]); - }); - - return result; +/** + * Takes a valid JSON document and performs the following escapings in string values: + * + * `&` -> `\u0026` + * `<` -> `\u003c` + * `>` -> `\u003e` + * + * Since those characters do not occur in other places of the JSON document, only + * string values are affected. + * + * If the input is invalid JSON, the behaviour is undefined. + */ +export function escapeCharacters(input: string): string { + // When we migrate to target es2021 or above, we can use replaceAll instead of global patterns. + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replaceAll + const amp = /&/g; + const lt = //g; + return input.replace(amp, "\\u0026").replace(lt, "\\u003c").replace(gt, "\\u003e"); } export function serializeSignDoc(signDoc: StdSignDoc): Uint8Array { - return escapeCharacters(toUtf8(sortedJsonStringify(signDoc))); + const serialized = escapeCharacters(sortedJsonStringify(signDoc)); + return toUtf8(serialized); }