diff --git a/packages/ethers/src.ts/utils.ts b/packages/ethers/src.ts/utils.ts index 5a77fca45..2bd124d0e 100644 --- a/packages/ethers/src.ts/utils.ts +++ b/packages/ethers/src.ts/utils.ts @@ -14,7 +14,7 @@ import { randomBytes } from "@ethersproject/random"; import { checkProperties, deepCopy, defineReadOnly, resolveProperties, shallowCopy } from "@ethersproject/properties"; import * as RLP from "@ethersproject/rlp"; import { computePublicKey, recoverPublicKey, SigningKey } from "@ethersproject/signing-key"; -import { formatBytes32String, parseBytes32String, toUtf8Bytes, toUtf8String } from "@ethersproject/strings"; +import { formatBytes32String, parseBytes32String, _toEscapedUtf8String, toUtf8Bytes, toUtf8String } from "@ethersproject/strings"; import { computeAddress, parse as parseTransaction, recoverAddress, serialize as serializeTransaction } from "@ethersproject/transactions"; import { commify, formatEther, parseEther, formatUnits, parseUnits } from "@ethersproject/units"; import { verifyMessage } from "@ethersproject/wallet"; @@ -81,6 +81,7 @@ export { hexDataLength, hexDataSlice, + _toEscapedUtf8String, toUtf8Bytes, toUtf8String, diff --git a/packages/strings/src.ts/index.ts b/packages/strings/src.ts/index.ts index eb162daba..c8a502d30 100644 --- a/packages/strings/src.ts/index.ts +++ b/packages/strings/src.ts/index.ts @@ -60,8 +60,10 @@ export function toUtf8Bytes(str: string, form: UnicodeNormalizationForm = Unicod }; +type ProcessFunc = (left: number, right?: number) => string; + // http://stackoverflow.com/questions/13356493/decode-utf-8-with-javascript#13691499 -export function toUtf8String(bytes: BytesLike, ignoreErrors?: boolean): string { +function processUtf8String(bytes: BytesLike, processFunc: ProcessFunc, ignoreErrors: boolean): string { bytes = arrayify(bytes); let result = ""; @@ -73,7 +75,7 @@ export function toUtf8String(bytes: BytesLike, ignoreErrors?: boolean): string { let c = bytes[i++]; // 0xxx xxxx if (c >> 7 === 0) { - result += String.fromCharCode(c); + result += processFunc(c); continue; } @@ -158,17 +160,55 @@ export function toUtf8String(bytes: BytesLike, ignoreErrors?: boolean): string { } if (res <= 0xffff) { - result += String.fromCharCode(res); + result += processFunc(res); continue; } res -= 0x10000; - result += String.fromCharCode(((res >> 10) & 0x3ff) + 0xd800, (res & 0x3ff) + 0xdc00); + result += processFunc(((res >> 10) & 0x3ff) + 0xd800, (res & 0x3ff) + 0xdc00); } return result; } +function escapeChar(value: number) { + let hex = ("0000" + value.toString(16)); + return "\\u" + hex.substring(hex.length - 4); +} + +export function _toEscapedUtf8String(bytes: BytesLike, ignoreErrors?: boolean): string { + return '"' + processUtf8String(bytes, (left, right) => { + if (right == null) { + if (left < 256) { + switch (left) { + case 8: return "\\b"; + case 9: return "\\t"; + case 10: return "\\n" + case 13: return "\\r"; + case 34: return "\\\""; + case 92: return "\\\\"; + } + + if (left >= 32 && left < 127) { + return String.fromCharCode(left); + } + } + + return escapeChar(left); + } + return escapeChar(left) + escapeChar(right); + }, ignoreErrors) + '"'; +} + +export function toUtf8String(bytes: BytesLike, ignoreErrors?: boolean): string { + return processUtf8String(bytes, (left, right) => { + if (right == null) { + return String.fromCharCode(left); + } + return String.fromCharCode(left, right); + }, ignoreErrors); +} + export function formatBytes32String(text: string): string { // Get the bytes diff --git a/packages/tests/src.ts/test-utils.ts b/packages/tests/src.ts/test-utils.ts index 1f47d5ca9..e68c907f4 100644 --- a/packages/tests/src.ts/test-utils.ts +++ b/packages/tests/src.ts/test-utils.ts @@ -26,10 +26,10 @@ describe('Test Contract Address Generation', function() { // @TODO: Mine a large collection of these from the blockchain - var getContractAddress = ethers.utils.getContractAddress; + let getContractAddress = ethers.utils.getContractAddress; // Transaction: 0x939aa17985bc2a52a0c1cba9497ef09e092355a805a8150e30e24b753bac6864 - var Tests = [ + let Tests = [ { address: '0x3474627D4F63A678266BC17171D87f8570936622', name: 'tx-0x939aa17985bc2a52a0c1cba9497ef09e092355a805a8150e30e24b753bac6864', @@ -229,11 +229,11 @@ describe('Test Solidity splitSignature', function() { it('splits a canonical signature', function() { this.timeout(120000); - var r = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; - var s = '0xcafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7e'; - for (var v = 27; v <= 28; v++) { - var signature = ethers.utils.concat([ r, s, [ v ] ]); - var sig = ethers.utils.splitSignature(signature); + let r = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; + let s = '0xcafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7e'; + for (let v = 27; v <= 28; v++) { + let signature = ethers.utils.concat([ r, s, [ v ] ]); + let sig = ethers.utils.splitSignature(signature); assert.equal(sig.r, r, 'split r correctly'); assert.equal(sig.s, s, 'split s correctly'); assert.equal(sig.v, v, 'split v correctly'); @@ -242,11 +242,11 @@ describe('Test Solidity splitSignature', function() { it('splits a legacy signature', function() { this.timeout(120000); - var r = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; - var s = '0xcafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7e'; - for (var v = 27; v <= 28; v++) { - var signature = ethers.utils.concat([ r, s, [ v - 27 ] ]); - var sig = ethers.utils.splitSignature(signature); + let r = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'; + let s = '0xcafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7ecafe1a7e'; + for (let v = 27; v <= 28; v++) { + let signature = ethers.utils.concat([ r, s, [ v - 27 ] ]); + let sig = ethers.utils.splitSignature(signature); assert.equal(sig.r, r, 'split r correctly'); assert.equal(sig.s, s, 'split s correctly'); assert.equal(sig.v, v, 'split v correctly'); @@ -259,16 +259,16 @@ describe('Test Base64 coder', function() { // https://en.wikipedia.org/wiki/Base64#Examples it('encodes and decodes the example from wikipedia', function() { this.timeout(120000); - var decodedText = 'Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.'; - var decoded = ethers.utils.toUtf8Bytes(decodedText); - var encoded = 'TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4='; + let decodedText = 'Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.'; + let decoded = ethers.utils.toUtf8Bytes(decodedText); + let encoded = 'TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlzIHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2YgdGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGludWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRoZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4='; assert.equal(ethers.utils.base64.encode(decoded), encoded, 'encodes to base64 string'); assert.equal(ethers.utils.toUtf8String(ethers.utils.base64.decode(encoded)), decodedText, 'decodes from base64 sstring'); }); }); describe('Test UTF-8 coder', function() { - var BadUTF = [ + let BadUTF = [ // See: https://en.wikipedia.org/wiki/UTF-8#Overlong_encodings { bytes: [ 0xF0,0x82, 0x82, 0xAC ], reason: 'overlong', name: 'wikipedia overlong encoded Euro sign' }, { bytes: [ 0xc0, 0x80 ], reason: 'overlong', name: '2-byte overlong - 0xc080' }, @@ -297,7 +297,7 @@ describe('Test UTF-8 coder', function() { BadUTF.forEach(function(test) { it('toUtf8String - ' + test.name, function() { assert.throws(function() { - var result = ethers.utils.toUtf8String(test.bytes); + let result = ethers.utils.toUtf8String(test.bytes); console.log('Result', result); }, function(error: Error) { return (error.message.split(';').pop().trim() === test.reason) @@ -317,8 +317,8 @@ describe('Test UTF-8 coder', function() { case 2: return String.fromCharCode(utils.randomNumber(seed + '-value', 0xdfff + 1, 0xffff)); case 3: - var left = utils.randomNumber(seed + '-value', 0xd800, 0xdbff + 1); - var right = utils.randomNumber(seed + '-value', 0xdc00, 0xdfff + 1); + let left = utils.randomNumber(seed + '-value', 0xd800, 0xdbff + 1); + let right = utils.randomNumber(seed + '-value', 0xdc00, 0xdfff + 1); return String.fromCharCode(left, right); } @@ -326,23 +326,25 @@ describe('Test UTF-8 coder', function() { } function randomString(seed: string) { - var length = utils.randomNumber(seed + '-length', 1, 5); - var str = ''; - for (var i = 0; i < length; i++) { + let length = utils.randomNumber(seed + '-length', 1, 5); + let str = ''; + for (let i = 0; i < length; i++) { str += randomChar(seed + '-char-' + i); } return str; } - for (var i = 0; i < 100000; i++) { - var seed = 'test-' + String(i); - var str = randomString(seed); + for (let i = 0; i < 100000; i++) { + let seed = 'test-' + String(i); + let str = randomString(seed); - var bytes = ethers.utils.toUtf8Bytes(str) - var str2 = ethers.utils.toUtf8String(bytes); + let bytes = ethers.utils.toUtf8Bytes(str) + let str2 = ethers.utils.toUtf8String(bytes); + let escaped = JSON.parse(ethers.utils._toEscapedUtf8String(bytes)); assert.ok(Buffer.from(str).equals(Buffer.from(bytes)), 'bytes not generated correctly - ' + bytes) assert.equal(str2, str, 'conversion not reflexive - ' + bytes); + assert.equal(escaped, str, 'conversion not reflexive - ' + bytes); } }); }); @@ -350,9 +352,9 @@ describe('Test UTF-8 coder', function() { describe('Test Bytes32String coder', function() { // @TODO: a LOT more test cases; generated from Solidity it("encodes an ens name", function() { - var str = "ricmoo.firefly.eth"; - var bytes32 = ethers.utils.formatBytes32String(str); - var str2 = ethers.utils.parseBytes32String(bytes32); + let str = "ricmoo.firefly.eth"; + let bytes32 = ethers.utils.formatBytes32String(str); + let str2 = ethers.utils.parseBytes32String(bytes32); assert.equal(bytes32, '0x7269636d6f6f2e66697265666c792e6574680000000000000000000000000000', 'formatted correctly'); assert.equal(str2, str, "parsed correctly"); }); @@ -361,8 +363,8 @@ describe('Test Bytes32String coder', function() { describe('Test BigNumber', function() { it("computes absoltue values", function() { function testAbs(test: { expected: string, value: string }) { - var value = ethers.BigNumber.from(test.value); - var expected = ethers.BigNumber.from(test.expected); + let value = ethers.BigNumber.from(test.value); + let expected = ethers.BigNumber.from(test.expected); assert.ok(value.abs().eq(expected), 'BigNumber.abs - ' + test.value); }