From 95c03088faa890604880dae770c4a720850f9275 Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Tue, 28 Nov 2023 08:07:57 -0600 Subject: [PATCH 01/16] feat(deser-lib): add serialization/deserialization module Issue: HSM-236 --- modules/deser-lib/.eslintignore | 5 + modules/deser-lib/.gitignore | 4 + modules/deser-lib/.mocharc.json | 9 + modules/deser-lib/.prettierignore | 2 + modules/deser-lib/.prettierrc.yml | 3 + modules/deser-lib/package.json | 45 ++++ modules/deser-lib/src/index.ts | 144 +++++++++++++ modules/deser-lib/test/fixtures.json | 96 +++++++++ modules/deser-lib/test/unit/deser-lib.ts | 257 +++++++++++++++++++++++ modules/deser-lib/tsconfig.json | 8 + 10 files changed, 573 insertions(+) create mode 100644 modules/deser-lib/.eslintignore create mode 100644 modules/deser-lib/.gitignore create mode 100644 modules/deser-lib/.mocharc.json create mode 100644 modules/deser-lib/.prettierignore create mode 100644 modules/deser-lib/.prettierrc.yml create mode 100644 modules/deser-lib/package.json create mode 100644 modules/deser-lib/src/index.ts create mode 100644 modules/deser-lib/test/fixtures.json create mode 100644 modules/deser-lib/test/unit/deser-lib.ts create mode 100644 modules/deser-lib/tsconfig.json diff --git a/modules/deser-lib/.eslintignore b/modules/deser-lib/.eslintignore new file mode 100644 index 0000000000..190f83e0df --- /dev/null +++ b/modules/deser-lib/.eslintignore @@ -0,0 +1,5 @@ +node_modules +.idea +public +dist + diff --git a/modules/deser-lib/.gitignore b/modules/deser-lib/.gitignore new file mode 100644 index 0000000000..1339102539 --- /dev/null +++ b/modules/deser-lib/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.idea/ +dist/ +yarn-error.log diff --git a/modules/deser-lib/.mocharc.json b/modules/deser-lib/.mocharc.json new file mode 100644 index 0000000000..08949c5c3d --- /dev/null +++ b/modules/deser-lib/.mocharc.json @@ -0,0 +1,9 @@ +{ + "require": ["ts-node/register", "should"], + "timeout": "20000", + "reporter": "min", + "reporter-option": ["cdn=true", "json=false"], + "exit": true, + "spec": ["test/unit/**/*.ts"], + "extension": [".js", ".ts"] +} diff --git a/modules/deser-lib/.prettierignore b/modules/deser-lib/.prettierignore new file mode 100644 index 0000000000..3a11d6af29 --- /dev/null +++ b/modules/deser-lib/.prettierignore @@ -0,0 +1,2 @@ +.nyc_output/ +dist/ diff --git a/modules/deser-lib/.prettierrc.yml b/modules/deser-lib/.prettierrc.yml new file mode 100644 index 0000000000..7c3d8dd32a --- /dev/null +++ b/modules/deser-lib/.prettierrc.yml @@ -0,0 +1,3 @@ +printWidth: 120 +singleQuote: true +trailingComma: 'es5' diff --git a/modules/deser-lib/package.json b/modules/deser-lib/package.json new file mode 100644 index 0000000000..f04f96d5cc --- /dev/null +++ b/modules/deser-lib/package.json @@ -0,0 +1,45 @@ +{ + "name": "deser-lib", + "version": "1.0.0", + "description": "BitGo serialization and deseralization library", + "main": "dist/src/index.js", + "scripts": { + "test": "yarn unit-test", + "unit-test": "nyc -- mocha --recursive test", + "build": "yarn tsc --build --incremental --verbose .", + "clean": "rm -r ./dist", + "fmt": "prettier --write .", + "check-fmt": "prettier --check .", + "lint": "eslint --quiet .", + "prepare": "npm run build", + "audit": "if [ \"$(npm --version | cut -d. -f1)\" -ge \"6\" ]; then npm audit; else echo \"npm >= 6 required to perform audit. skipping...\"; fi" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/BitGo/BitGoJS.git", + "directory": "modules/deser-lib" + }, + "author": "John Driscoll ", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/bitgo/bitgojs/issues" + }, + "homepage": "https://github.com/bitgo/bitgojs#readme", + "nyc": { + "extension": [ + ".ts" + ] + }, + "lint-staged": { + "*.{js,ts}": [ + "yarn prettier --write", + "yarn eslint --fix" + ] + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "cbor": "^9.0.1" + } +} diff --git a/modules/deser-lib/src/index.ts b/modules/deser-lib/src/index.ts new file mode 100644 index 0000000000..975d53b43c --- /dev/null +++ b/modules/deser-lib/src/index.ts @@ -0,0 +1,144 @@ +import { decodeFirstSync, encodeCanonical } from 'cbor'; + +/** Return a string describing value as a type. */ +function getType(value): string { + if (Array.isArray(value)) { + const types = value.map(getType); + if (!types.slice(1).every((value) => value === types[0])) { + throw new Error('Array elements are not of the same type'); + } + return JSON.stringify([types[0]]); + } + if (typeof value === 'object') { + const properties = Object.getOwnPropertyNames(value); + properties.sort(); + return JSON.stringify( + properties.reduce((acc, name) => { + acc[name] = getType(value[name]); + return acc; + }, {}) + ); + } + if (typeof value === 'string') { + if (value.startsWith('0x')) { + return 'bytes'; + } + return 'string'; + } + return JSON.stringify(typeof value); +} + +/** Compare two buffers for sorting. */ +function bufferCompare(a: Buffer, b: Buffer) { + let i = 0; + while (i < a.length && i < b.length && a[i] == b[i]) { + i++; + } + if (i === a.length && i === b.length) { + return 0; + } + if (i === a.length || i === b.length) { + return a.length - b.length; + } + return a[i] - b[i]; +} + +/** Compare two array elements for sorting. */ +function elementCompare(a: any, b: any) { + if (!('weight' in a) || !('weight' in b)) { + throw new Error('Array elements lack weight property'); + } + if (a.weight === b.weight) { + if (!('value' in a) || !('value' in b)) { + throw new Error('Array elements lack value property'); + } + const aVal = transform(a.value); + const bVal = transform(b.value); + if (!Buffer.isBuffer(aVal) && typeof aVal !== 'string' && typeof aVal !== 'number') { + throw new Error('Array element value cannot be compared'); + } + if (!Buffer.isBuffer(bVal) && typeof bVal !== 'string' && typeof bVal !== 'number') { + throw new Error('Array element value cannot be compared'); + } + if (typeof aVal === 'number' && typeof bVal === 'number') { + return aVal - bVal; + } + let aBuf, bBuf; + if (typeof aVal === 'number') { + aBuf = Buffer.from([aVal]); + } else { + aBuf = Buffer.from(aVal); + } + if (typeof bVal === 'number') { + bBuf = Buffer.from([bVal]); + } else { + bBuf = Buffer.from(bVal); + } + return bufferCompare(aBuf, bBuf); + } + return a.weight - b.weight; +} + +/** Transform value into its canonical, serializable form. */ +export function transform(value: any) { + if (typeof value === 'string') { + // Transform hex strings to buffers. + if (value.startsWith('0x')) { + if (!value.match(/^0x([0-9a-fA-F]{2})*$/)) { + throw new Error('0x prefixed string contains non-hex characters.'); + } + return Buffer.from(value.slice(2), 'hex'); + } + } else if (Array.isArray(value)) { + // Enforce array elemenst are same type. + getType(value); + value = value.slice(0); + value.sort(elementCompare).map(transform); + return value.map(transform); + } else if (typeof value === 'object') { + const properties = Object.getOwnPropertyNames(value); + properties.sort(); + return properties.reduce((acc, name) => { + acc[name] = transform(value[name]); + return acc; + }, {}); + } + return value; +} + +/** Untransform value into its human readable form. */ +export function untransform(value: any) { + if (Buffer.isBuffer(value)) { + return '0x' + value.toString('hex'); + } + if (Array.isArray(value) && value.length > 1) { + for (let i = 1; i < value.length; i++) { + if (value[i - 1].weight > value[i].weight) { + throw new Error('Array elements are not in canonical order'); + } + } + return value.map(untransform); + } else if (typeof value === 'object') { + const properties = Object.getOwnPropertyNames(value); + for (let i = 1; i < properties.length; i++) { + if (properties[i - 1].localeCompare(properties[i]) > 0) { + throw new Error('Object properties are not in caonical order'); + } + } + return properties.reduce((acc, name) => { + acc[name] = untransform(value[name]); + return acc; + }, {}); + } + return value; +} + +/** Serialize a value. */ +export function serialize(value: any): Buffer { + return encodeCanonical(transform(value)); +} + +/** Deserialize a value. */ +export function deserialize(value: Buffer) { + return untransform(decodeFirstSync(value)); +} diff --git a/modules/deser-lib/test/fixtures.json b/modules/deser-lib/test/fixtures.json new file mode 100644 index 0000000000..fd48fb708d --- /dev/null +++ b/modules/deser-lib/test/fixtures.json @@ -0,0 +1,96 @@ +[ + { + "deserialized": { + "keys": [ + { + "key": "0x010203", + "weight": 0 + }, + { + "key": "0x040506", + "weight": 1 + } + ] + }, + "serialized": "a1646b65797382a2636b6579430102036677656967687400a2636b6579430405066677656967687401" + }, + { + "deserialized": { + "a": "0xffffffff", + "b": "0x00000000", + "c": "0xffffffff", + "d": [ + { + "weight": 0 + }, + { + "weight": 1 + }, + { + "weight": 2 + }, + { + "weight": 3 + } + ] + }, + "serialized": "a4616144ffffffff61624400000000616344ffffffff616484a16677656967687400a16677656967687401a16677656967687402a16677656967687403" + }, + { + "deserialized": { + "a": [ + { + "value": "a", + "weight": 0 + }, + { + "value": "b", + "weight": 0 + }, + { + "value": "c", + "weight": 0 + } + ] + }, + "serialized": "a1616183a26576616c756561616677656967687400a26576616c756561626677656967687400a26576616c756561636677656967687400" + }, + { + "deserialized": { + "a": [ + { + "value": "0x0a", + "weight": 0 + }, + { + "value": "0x0b", + "weight": 0 + }, + { + "value": "0x0c", + "weight": 0 + } + ] + }, + "serialized": "a1616183a26576616c7565410a6677656967687400a26576616c7565410b6677656967687400a26576616c7565410c6677656967687400" + }, + { + "deserialized": { + "a": [ + { + "value": 1, + "weight": 0 + }, + { + "value": 2, + "weight": 0 + }, + { + "value": 3, + "weight": 0 + } + ] + }, + "serialized": "a1616183a26576616c7565016677656967687400a26576616c7565026677656967687400a26576616c7565036677656967687400" + } +] \ No newline at end of file diff --git a/modules/deser-lib/test/unit/deser-lib.ts b/modules/deser-lib/test/unit/deser-lib.ts new file mode 100644 index 0000000000..611f0e04f0 --- /dev/null +++ b/modules/deser-lib/test/unit/deser-lib.ts @@ -0,0 +1,257 @@ +import { transform, untransform, serialize, deserialize } from '../..'; +import * as fixtures from '../fixtures.json'; + +describe('deser-lib', function () { + describe('transform', function () { + it('orders object properties canonically', function () { + const res = transform({ b: 'second', a: 'first' }); + const properties = Object.getOwnPropertyNames(res); + properties[0].should.equal('a'); + properties[1].should.equal('b'); + res.a.should.equal('first'); + res.b.should.equal('second'); + }); + + describe('canonical ordering', function () { + it('orders by weight', function () { + const res = transform([{ weight: 2 }, { weight: 1 }]); + res[0].weight.should.equal(1); + res[1].weight.should.equal(2); + }); + + it('orders number values', function () { + const res = transform([ + { weight: 1, value: 2 }, + { weight: 1, value: 1 }, + ]); + res[0].value.should.equal(1); + res[1].value.should.equal(2); + }); + + it('orders string values', function () { + const res = transform([ + { weight: 1, value: 'b' }, + { weight: 1, value: 'a' }, + ]); + res[0].value.should.equal('a'); + res[1].value.should.equal('b'); + }); + + it('orders byte values', function () { + const res = transform([ + { weight: 1, value: '0x0b' }, + { weight: 1, value: '0x0a' }, + ]); + res[0].value.equals(Buffer.from([0x0a])).should.equal(true); + res[1].value.equals(Buffer.from([0x0b])).should.equal(true); + }); + + it('throws for elements without weight', function () { + (() => transform([{}, {}])).should.throw(); + }); + + it('throws for values that cannot be compared', function () { + (() => + transform([ + { weight: 1, value: {} }, + { weight: 1, value: 1 }, + ])).should.throw(); + }); + + it('throws for elements of mixed type', function () { + (() => + transform([ + { weight: 0, value: '0' }, + { weight: 0, value: 0 }, + ])).should.throw(); + }); + }); + + it('replaces prefixed hex strings with Buffers', function () { + const hex = '00010203'; + const res = transform({ value: '0x' + hex }); + Buffer.isBuffer(res.value).should.equal(true); + res.value.equals(Buffer.from(hex, 'hex')).should.equal(true); + }); + + it('preserves non-prefixed hex strings', function () { + const string = '00010203'; + const res = transform({ value: string }); + res.value.should.equal(string); + }); + + it('transforms object recursively', function () { + const res = transform({ value: { b: 'second', a: 'first' } }); + const properties = Object.getOwnPropertyNames(res.value); + properties[0].should.equal('a'); + properties[1].should.equal('b'); + res.value.a.should.equal('first'); + res.value.b.should.equal('second'); + }); + + it('transforms array recursively', function () { + const res = transform([{ weight: 0, value: { b: 'second', a: 'first' } }]); + const properties = Object.getOwnPropertyNames(res[0].value); + properties[0].should.equal('a'); + properties[1].should.equal('b'); + res[0].value.a.should.equal('first'); + res[0].value.b.should.equal('second'); + }); + }); + + describe('untransform', function () { + it('untransforms object', function () { + const res = untransform({ a: 'first', b: 'second' }); + const properties = Object.getOwnPropertyNames(res); + properties[0].should.equal('a'); + properties[1].should.equal('b'); + res.a.should.equal('first'); + res.b.should.equal('second'); + }); + + it('enforces canonical object property order', function () { + (() => untransform({ b: 'second', a: 'first' })).should.throw(); + }); + + it('enforces canonical array element order', function () { + (() => untransform([{ weight: 2 }, { weight: 1 }])).should.throw(); + }); + + it('replaces Buffers with prefixed hex strings', function () { + const hex = '00010203'; + const res = untransform({ value: Buffer.from(hex, 'hex') }); + res.value.should.equal('0x' + hex); + }); + + it('preserves non-prefixed hex strings', function () { + const string = '00010203'; + const res = untransform({ value: string }); + res.value.should.equal(string); + }); + + it('untransforms object recursively', function () { + const hex = '00010203'; + const res = untransform({ value: { value: Buffer.from(hex, 'hex') } }); + res.value.value.should.equal('0x' + hex); + }); + + it('untransforms array recursively', function () { + const hex = '00010203'; + const res = untransform([{ value: Buffer.from(hex, 'hex'), weight: 0 }]); + res[0].value.should.equal('0x' + hex); + }); + }); + + describe('fixtures', function () { + xit('creates test vectors', function () { + const { writeFileSync } = require('fs'); + const deserialized = [ + { + keys: [ + { + key: '0x010203', + weight: 0, + }, + { + key: '0x040506', + weight: 1, + }, + ], + }, + { + a: '0xffffffff', + b: '0x00000000', + c: '0xffffffff', + d: [ + { + weight: 0, + }, + { + weight: 1, + }, + { + weight: 2, + }, + { + weight: 3, + }, + ], + }, + { + a: [ + { + value: 'a', + weight: 0, + }, + { + value: 'b', + weight: 0, + }, + { + value: 'c', + weight: 0, + }, + ], + }, + { + a: [ + { + weight: 0, + value: '0x0a', + }, + { + weight: 0, + value: '0x0b', + }, + { + weight: 0, + value: '0x0c', + }, + ], + }, + { + a: [ + { + weight: 0, + value: 1, + }, + { + weight: 0, + value: 2, + }, + { + weight: 0, + value: 3, + }, + ], + }, + ]; + const serialized = deserialized.map((x) => serialize(x).toString('hex')); + writeFileSync( + 'test/fixtures.json', + JSON.stringify( + deserialized.map((deserialized, i) => ({ + deserialized: untransform(transform(deserialized)), + serialized: serialized[i], + })), + null, + 2 + ) + ); + }); + + for (let i = 0; i < fixtures.length; i++) { + it(`deserializes vector[${i}]`, function () { + const { deserialized, serialized } = fixtures[i]; + serialize(deserialized).equals(Buffer.from(serialized, 'hex')).should.equal(true); + }); + } + + for (let i = 0; i < fixtures.length; i++) { + it(`serializes vector[${i}]`, function () { + const { deserialized, serialized } = fixtures[i]; + JSON.stringify(deserialize(Buffer.from(serialized, 'hex'))).should.equal(JSON.stringify(deserialized)); + }); + } + }); +}); diff --git a/modules/deser-lib/tsconfig.json b/modules/deser-lib/tsconfig.json new file mode 100644 index 0000000000..fad11e1ed8 --- /dev/null +++ b/modules/deser-lib/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "." + }, + "include": ["src/**/*", "package.json"] +} From f20cd591ca04e7b4572e9ba892ffa947f0529098 Mon Sep 17 00:00:00 2001 From: Zahin Mohammad Date: Mon, 4 Dec 2023 11:28:41 -0500 Subject: [PATCH 02/16] fix(deser-lib): update yarn lockfile TICKET: HSM-236 --- yarn.lock | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index f65651cfcb..1c6b88c480 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7527,6 +7527,13 @@ catharsis@^0.9.0: dependencies: lodash "^4.17.15" +cbor@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/cbor/-/cbor-9.0.1.tgz#b16e393d4948d44758cd54ac6151379d443b37ae" + integrity sha512-/TQOWyamDxvVIv+DY9cOLNuABkoyz8K/F3QE56539pGVYohx0+MEA1f4lChFTX79dBTBS7R1PF6ovH7G+VtBfQ== + dependencies: + nofilter "^3.1.0" + chai@^4.3.6: version "4.3.7" resolved "https://registry.npmjs.org/chai/-/chai-4.3.7.tgz" @@ -14282,6 +14289,11 @@ node-releases@^2.0.8: resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz" integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== +nofilter@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/nofilter/-/nofilter-3.1.0.tgz#c757ba68801d41ff930ba2ec55bab52ca184aa66" + integrity sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g== + nopt@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz" @@ -18155,9 +18167,9 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.3.3, terser-webpack-plugi terser "^5.16.5" terser@^5.10.0, terser@^5.14.2, terser@^5.16.5: - version "5.24.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.24.0.tgz#4ae50302977bca4831ccc7b4fef63a3c04228364" - integrity sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw== + version "5.25.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.25.0.tgz#6579b4cca45b08bf0fdaa1a04605fd5860dfb2ac" + integrity sha512-we0I9SIsfvNUMP77zC9HG+MylwYYsGFSBG8qm+13oud2Yh+O104y614FRbyjpxys16jZwot72Fpi827YvGzuqg== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" From f2198e036a33319e50dce267edf1b756f1e6ad1c Mon Sep 17 00:00:00 2001 From: Zahin Mohammad Date: Mon, 4 Dec 2023 11:33:30 -0500 Subject: [PATCH 03/16] chore(deser-lib): fmt TICKET: HSM-236 --- modules/deser-lib/test/fixtures.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/deser-lib/test/fixtures.json b/modules/deser-lib/test/fixtures.json index fd48fb708d..940440a87c 100644 --- a/modules/deser-lib/test/fixtures.json +++ b/modules/deser-lib/test/fixtures.json @@ -93,4 +93,4 @@ }, "serialized": "a1616183a26576616c7565016677656967687400a26576616c7565026677656967687400a26576616c7565036677656967687400" } -] \ No newline at end of file +] From 8f5494e54196f28a6c557d073f7147297ca85a17 Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Mon, 4 Dec 2023 11:54:32 -0600 Subject: [PATCH 04/16] chore(deser-lib): add suggested changes to package.json TICKET: HSM-236 --- modules/deser-lib/package.json | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/modules/deser-lib/package.json b/modules/deser-lib/package.json index f04f96d5cc..1edaacaf2b 100644 --- a/modules/deser-lib/package.json +++ b/modules/deser-lib/package.json @@ -1,26 +1,25 @@ { - "name": "deser-lib", + "name": "@bitgo/deser-lib", "version": "1.0.0", "description": "BitGo serialization and deseralization library", - "main": "dist/src/index.js", + "main": "./dist/src/index.js", "scripts": { "test": "yarn unit-test", "unit-test": "nyc -- mocha --recursive test", "build": "yarn tsc --build --incremental --verbose .", - "clean": "rm -r ./dist", "fmt": "prettier --write .", "check-fmt": "prettier --check .", + "clean": "rm -r ./dist", "lint": "eslint --quiet .", - "prepare": "npm run build", - "audit": "if [ \"$(npm --version | cut -d. -f1)\" -ge \"6\" ]; then npm audit; else echo \"npm >= 6 required to perform audit. skipping...\"; fi" + "prepare": "npm run build" }, "repository": { "type": "git", "url": "git+https://github.com/BitGo/BitGoJS.git", "directory": "modules/deser-lib" }, - "author": "John Driscoll ", - "license": "Apache-2.0", + "author": "BitGo SDK Team ", + "license": "MIT", "bugs": { "url": "https://github.com/bitgo/bitgojs/issues" }, From 7da82190904f7c33f97d703c519558b7e9a20ac6 Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Mon, 4 Dec 2023 14:11:47 -0600 Subject: [PATCH 05/16] chore(deser-lib): changes based on review TICKET: HSM-236 --- modules/deser-lib/src/index.ts | 156 +++++++++++++++-------- modules/deser-lib/test/unit/deser-lib.ts | 91 ++++++++++--- 2 files changed, 175 insertions(+), 72 deletions(-) diff --git a/modules/deser-lib/src/index.ts b/modules/deser-lib/src/index.ts index 975d53b43c..2e8d812ac8 100644 --- a/modules/deser-lib/src/index.ts +++ b/modules/deser-lib/src/index.ts @@ -1,15 +1,22 @@ import { decodeFirstSync, encodeCanonical } from 'cbor'; -/** Return a string describing value as a type. */ -function getType(value): string { - if (Array.isArray(value)) { +/** + * Return a string describing value as a type. + * @param value - Any javascript value to type. + * @returns String describing value type. + */ +function getType(value: unknown): string { + if (value === null || value === undefined) { + return 'null'; + } + if (value instanceof Array) { const types = value.map(getType); if (!types.slice(1).every((value) => value === types[0])) { throw new Error('Array elements are not of the same type'); } return JSON.stringify([types[0]]); } - if (typeof value === 'object') { + if (value instanceof Object) { const properties = Object.getOwnPropertyNames(value); properties.sort(); return JSON.stringify( @@ -28,8 +35,13 @@ function getType(value): string { return JSON.stringify(typeof value); } -/** Compare two buffers for sorting. */ -function bufferCompare(a: Buffer, b: Buffer) { +/** + * Compare two buffers for sorting. + * @param a - left buffer to compare to right buffer. + * @param b - right buffer to compare to left buffer. + * @returns Negative if a < b, positive if b > a, 0 if equal. + */ +function bufferCompare(a: Buffer, b: Buffer): number { let i = 0; while (i < a.length && i < b.length && a[i] == b[i]) { i++; @@ -43,44 +55,68 @@ function bufferCompare(a: Buffer, b: Buffer) { return a[i] - b[i]; } -/** Compare two array elements for sorting. */ -function elementCompare(a: any, b: any) { - if (!('weight' in a) || !('weight' in b)) { - throw new Error('Array elements lack weight property'); +/** A sortable array element. */ +interface Sortable { + weight: number; + value?: unknown; +} + +/** + * Type check for sortable array element. + * @param value - Value to type check. + * @returns True if value is a sortable array element. + */ +function isSortable(value: unknown): value is Sortable { + return value instanceof Object && 'weight' in value; +} + +/** + * Compare two array elements for sorting. + * @param a - left element to compare to right element. + * @param b - right element to compare to left element. + * @returns Negative if a < b, positive if b > a, 0 if equal. + */ +function elementCompare(a: unknown, b: unknown): number { + if (!isSortable(a) || !isSortable(b)) { + throw new Error('Array elements must be sortable'); } if (a.weight === b.weight) { - if (!('value' in a) || !('value' in b)) { - throw new Error('Array elements lack value property'); - } - const aVal = transform(a.value); - const bVal = transform(b.value); - if (!Buffer.isBuffer(aVal) && typeof aVal !== 'string' && typeof aVal !== 'number') { - throw new Error('Array element value cannot be compared'); - } - if (!Buffer.isBuffer(bVal) && typeof bVal !== 'string' && typeof bVal !== 'number') { - throw new Error('Array element value cannot be compared'); - } - if (typeof aVal === 'number' && typeof bVal === 'number') { - return aVal - bVal; - } - let aBuf, bBuf; - if (typeof aVal === 'number') { - aBuf = Buffer.from([aVal]); - } else { - aBuf = Buffer.from(aVal); - } - if (typeof bVal === 'number') { - bBuf = Buffer.from([bVal]); - } else { - bBuf = Buffer.from(bVal); + if ('value' in a && 'value' in b) { + const aVal = transform(a.value); + const bVal = transform(b.value); + if ( + (!Buffer.isBuffer(aVal) && typeof aVal !== 'string' && typeof aVal !== 'number') || + (!Buffer.isBuffer(bVal) && typeof bVal !== 'string' && typeof bVal !== 'number') + ) { + throw new Error('Array element value cannot be compared'); + } + let aBuf, bBuf; + if (typeof aVal === 'number') { + aBuf = Buffer.from([aVal]); + } else { + aBuf = Buffer.from(aVal); + } + if (typeof bVal === 'number') { + bBuf = Buffer.from([bVal]); + } else { + bBuf = Buffer.from(bVal); + } + return bufferCompare(aBuf, bBuf); } - return bufferCompare(aBuf, bBuf); + throw new Error('Array elements must be sortable'); } return a.weight - b.weight; } -/** Transform value into its canonical, serializable form. */ -export function transform(value: any) { +/** + * Transform value into its canonical, serializable form. + * @param value - Value to transform. + * @returns Canonical, serializable form of value. + */ +export function transform(value: T): T | Buffer { + if (value === null || value === undefined) { + return value; + } if (typeof value === 'string') { // Transform hex strings to buffers. if (value.startsWith('0x')) { @@ -89,36 +125,40 @@ export function transform(value: any) { } return Buffer.from(value.slice(2), 'hex'); } - } else if (Array.isArray(value)) { - // Enforce array elemenst are same type. + } else if (value instanceof Array) { + // Enforce array elements are same type. getType(value); - value = value.slice(0); - value.sort(elementCompare).map(transform); - return value.map(transform); - } else if (typeof value === 'object') { + value = [...value] as unknown as T; + (value as unknown as Array).sort(elementCompare); + return (value as unknown as Array).map(transform) as unknown as T; + } else if (value instanceof Object) { const properties = Object.getOwnPropertyNames(value); properties.sort(); return properties.reduce((acc, name) => { acc[name] = transform(value[name]); return acc; - }, {}); + }, {}) as unknown as T; } return value; } -/** Untransform value into its human readable form. */ -export function untransform(value: any) { +/** + * Untransform value into its human readable form. + * @param value - Value to untransform. + * @returns Untransformed, human readable form of value. + */ +export function untransform(value: T): T | string { if (Buffer.isBuffer(value)) { return '0x' + value.toString('hex'); } - if (Array.isArray(value) && value.length > 1) { + if (value instanceof Array && value.length > 1) { for (let i = 1; i < value.length; i++) { if (value[i - 1].weight > value[i].weight) { throw new Error('Array elements are not in canonical order'); } } - return value.map(untransform); - } else if (typeof value === 'object') { + return value.map(untransform) as unknown as T; + } else if (value instanceof Object) { const properties = Object.getOwnPropertyNames(value); for (let i = 1; i < properties.length; i++) { if (properties[i - 1].localeCompare(properties[i]) > 0) { @@ -128,17 +168,25 @@ export function untransform(value: any) { return properties.reduce((acc, name) => { acc[name] = untransform(value[name]); return acc; - }, {}); + }, {}) as unknown as T; } return value; } -/** Serialize a value. */ -export function serialize(value: any): Buffer { +/** + * Serialize a value. + * @param value - Value to serialize. + * @returns Buffer representing serialized value. + */ +export function serialize(value: T): Buffer { return encodeCanonical(transform(value)); } -/** Deserialize a value. */ -export function deserialize(value: Buffer) { +/** + * Deserialize a value. + * @param value - Buffer to deserialize. + * @returns Deserialized value. + */ +export function deserialize(value: Buffer): unknown { return untransform(decodeFirstSync(value)); } diff --git a/modules/deser-lib/test/unit/deser-lib.ts b/modules/deser-lib/test/unit/deser-lib.ts index 611f0e04f0..e6839d3c0d 100644 --- a/modules/deser-lib/test/unit/deser-lib.ts +++ b/modules/deser-lib/test/unit/deser-lib.ts @@ -4,7 +4,7 @@ import * as fixtures from '../fixtures.json'; describe('deser-lib', function () { describe('transform', function () { it('orders object properties canonically', function () { - const res = transform({ b: 'second', a: 'first' }); + const res = transform({ b: 'second', a: 'first' }) as any; const properties = Object.getOwnPropertyNames(res); properties[0].should.equal('a'); properties[1].should.equal('b'); @@ -14,48 +14,94 @@ describe('deser-lib', function () { describe('canonical ordering', function () { it('orders by weight', function () { - const res = transform([{ weight: 2 }, { weight: 1 }]); + const res = transform([ + { weight: 2, value: null }, + { weight: 1, value: null }, + ]) as any; + res[0].weight.should.equal(1); + res[1].weight.should.equal(2); + }); + + it('groups equal elements', function () { + const res = transform([ + { + weight: 2, + value: 'b', + }, + { + weight: 1, + value: 'a', + }, + { + weight: 3, + value: 'c', + }, + { + weight: 2, + value: 'b', + }, + ]) as any; res[0].weight.should.equal(1); res[1].weight.should.equal(2); + res[2].weight.should.equal(2); + res[3].weight.should.equal(3); }); it('orders number values', function () { const res = transform([ { weight: 1, value: 2 }, { weight: 1, value: 1 }, - ]); + ]) as any; res[0].value.should.equal(1); res[1].value.should.equal(2); }); it('orders string values', function () { const res = transform([ - { weight: 1, value: 'b' }, - { weight: 1, value: 'a' }, - ]); - res[0].value.should.equal('a'); - res[1].value.should.equal('b'); + { weight: 1, value: 'ab' }, + { weight: 1, value: 'aa' }, + ]) as any; + res[0].value.should.equal('aa'); + res[1].value.should.equal('ab'); }); it('orders byte values', function () { const res = transform([ { weight: 1, value: '0x0b' }, { weight: 1, value: '0x0a' }, - ]); + ]) as any; res[0].value.equals(Buffer.from([0x0a])).should.equal(true); res[1].value.equals(Buffer.from([0x0b])).should.equal(true); }); + it('orders string values of different lengths', function () { + const res = transform([ + { weight: 1, value: 'ab' }, + { weight: 1, value: 'a' }, + ]) as any; + res[0].value.should.equal('a'); + res[1].value.should.equal('ab'); + }); + it('throws for elements without weight', function () { (() => transform([{}, {}])).should.throw(); }); + it('throws for elements without value', function () { + (() => transform([{ weight: 1 }, { weight: 1 }])).should.throw(); + }); + it('throws for values that cannot be compared', function () { (() => transform([ { weight: 1, value: {} }, { weight: 1, value: 1 }, ])).should.throw(); + (() => + transform([ + { weight: 1, value: undefined }, + { weight: 1, value: null }, + ])).should.throw(); }); it('throws for elements of mixed type', function () { @@ -67,21 +113,26 @@ describe('deser-lib', function () { }); }); + it('preserves null values', function () { + const res = transform({ value: null }) as any; + res.should.have.property('value').which.is.null(); + }); + it('replaces prefixed hex strings with Buffers', function () { const hex = '00010203'; - const res = transform({ value: '0x' + hex }); + const res = transform({ value: '0x' + hex }) as any; Buffer.isBuffer(res.value).should.equal(true); res.value.equals(Buffer.from(hex, 'hex')).should.equal(true); }); it('preserves non-prefixed hex strings', function () { const string = '00010203'; - const res = transform({ value: string }); + const res = transform({ value: string }) as any; res.value.should.equal(string); }); it('transforms object recursively', function () { - const res = transform({ value: { b: 'second', a: 'first' } }); + const res = transform({ value: { b: 'second', a: 'first' } }) as any; const properties = Object.getOwnPropertyNames(res.value); properties[0].should.equal('a'); properties[1].should.equal('b'); @@ -90,18 +141,22 @@ describe('deser-lib', function () { }); it('transforms array recursively', function () { - const res = transform([{ weight: 0, value: { b: 'second', a: 'first' } }]); + const res = transform([{ weight: 0, value: { b: 'second', a: 'first' } }]) as any; const properties = Object.getOwnPropertyNames(res[0].value); properties[0].should.equal('a'); properties[1].should.equal('b'); res[0].value.a.should.equal('first'); res[0].value.b.should.equal('second'); }); + + it('throws for invalid hex strings', function () { + (() => transform('0x0g')).should.throw(); + }); }); describe('untransform', function () { it('untransforms object', function () { - const res = untransform({ a: 'first', b: 'second' }); + const res = untransform({ a: 'first', b: 'second' }) as any; const properties = Object.getOwnPropertyNames(res); properties[0].should.equal('a'); properties[1].should.equal('b'); @@ -119,25 +174,25 @@ describe('deser-lib', function () { it('replaces Buffers with prefixed hex strings', function () { const hex = '00010203'; - const res = untransform({ value: Buffer.from(hex, 'hex') }); + const res = untransform({ value: Buffer.from(hex, 'hex') }) as any; res.value.should.equal('0x' + hex); }); it('preserves non-prefixed hex strings', function () { const string = '00010203'; - const res = untransform({ value: string }); + const res = untransform({ value: string }) as any; res.value.should.equal(string); }); it('untransforms object recursively', function () { const hex = '00010203'; - const res = untransform({ value: { value: Buffer.from(hex, 'hex') } }); + const res = untransform({ value: { value: Buffer.from(hex, 'hex') } }) as any; res.value.value.should.equal('0x' + hex); }); it('untransforms array recursively', function () { const hex = '00010203'; - const res = untransform([{ value: Buffer.from(hex, 'hex'), weight: 0 }]); + const res = untransform([{ value: Buffer.from(hex, 'hex'), weight: 0 }]) as any; res[0].value.should.equal('0x' + hex); }); }); From cb1d616ae81d7a69191705944bb42345ac00fe3f Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Mon, 4 Dec 2023 14:19:19 -0600 Subject: [PATCH 06/16] chore(deser-lib): move current code to cbor namespace TICKET: HSM-236 --- modules/deser-lib/src/cbor.ts | 192 ++++++ modules/deser-lib/src/index.ts | 193 +----- .../deser-lib/test/{ => cbor}/fixtures.json | 2 +- modules/deser-lib/test/unit/deser-lib.ts | 558 +++++++++--------- 4 files changed, 474 insertions(+), 471 deletions(-) create mode 100644 modules/deser-lib/src/cbor.ts rename modules/deser-lib/test/{ => cbor}/fixtures.json (99%) diff --git a/modules/deser-lib/src/cbor.ts b/modules/deser-lib/src/cbor.ts new file mode 100644 index 0000000000..2e8d812ac8 --- /dev/null +++ b/modules/deser-lib/src/cbor.ts @@ -0,0 +1,192 @@ +import { decodeFirstSync, encodeCanonical } from 'cbor'; + +/** + * Return a string describing value as a type. + * @param value - Any javascript value to type. + * @returns String describing value type. + */ +function getType(value: unknown): string { + if (value === null || value === undefined) { + return 'null'; + } + if (value instanceof Array) { + const types = value.map(getType); + if (!types.slice(1).every((value) => value === types[0])) { + throw new Error('Array elements are not of the same type'); + } + return JSON.stringify([types[0]]); + } + if (value instanceof Object) { + const properties = Object.getOwnPropertyNames(value); + properties.sort(); + return JSON.stringify( + properties.reduce((acc, name) => { + acc[name] = getType(value[name]); + return acc; + }, {}) + ); + } + if (typeof value === 'string') { + if (value.startsWith('0x')) { + return 'bytes'; + } + return 'string'; + } + return JSON.stringify(typeof value); +} + +/** + * Compare two buffers for sorting. + * @param a - left buffer to compare to right buffer. + * @param b - right buffer to compare to left buffer. + * @returns Negative if a < b, positive if b > a, 0 if equal. + */ +function bufferCompare(a: Buffer, b: Buffer): number { + let i = 0; + while (i < a.length && i < b.length && a[i] == b[i]) { + i++; + } + if (i === a.length && i === b.length) { + return 0; + } + if (i === a.length || i === b.length) { + return a.length - b.length; + } + return a[i] - b[i]; +} + +/** A sortable array element. */ +interface Sortable { + weight: number; + value?: unknown; +} + +/** + * Type check for sortable array element. + * @param value - Value to type check. + * @returns True if value is a sortable array element. + */ +function isSortable(value: unknown): value is Sortable { + return value instanceof Object && 'weight' in value; +} + +/** + * Compare two array elements for sorting. + * @param a - left element to compare to right element. + * @param b - right element to compare to left element. + * @returns Negative if a < b, positive if b > a, 0 if equal. + */ +function elementCompare(a: unknown, b: unknown): number { + if (!isSortable(a) || !isSortable(b)) { + throw new Error('Array elements must be sortable'); + } + if (a.weight === b.weight) { + if ('value' in a && 'value' in b) { + const aVal = transform(a.value); + const bVal = transform(b.value); + if ( + (!Buffer.isBuffer(aVal) && typeof aVal !== 'string' && typeof aVal !== 'number') || + (!Buffer.isBuffer(bVal) && typeof bVal !== 'string' && typeof bVal !== 'number') + ) { + throw new Error('Array element value cannot be compared'); + } + let aBuf, bBuf; + if (typeof aVal === 'number') { + aBuf = Buffer.from([aVal]); + } else { + aBuf = Buffer.from(aVal); + } + if (typeof bVal === 'number') { + bBuf = Buffer.from([bVal]); + } else { + bBuf = Buffer.from(bVal); + } + return bufferCompare(aBuf, bBuf); + } + throw new Error('Array elements must be sortable'); + } + return a.weight - b.weight; +} + +/** + * Transform value into its canonical, serializable form. + * @param value - Value to transform. + * @returns Canonical, serializable form of value. + */ +export function transform(value: T): T | Buffer { + if (value === null || value === undefined) { + return value; + } + if (typeof value === 'string') { + // Transform hex strings to buffers. + if (value.startsWith('0x')) { + if (!value.match(/^0x([0-9a-fA-F]{2})*$/)) { + throw new Error('0x prefixed string contains non-hex characters.'); + } + return Buffer.from(value.slice(2), 'hex'); + } + } else if (value instanceof Array) { + // Enforce array elements are same type. + getType(value); + value = [...value] as unknown as T; + (value as unknown as Array).sort(elementCompare); + return (value as unknown as Array).map(transform) as unknown as T; + } else if (value instanceof Object) { + const properties = Object.getOwnPropertyNames(value); + properties.sort(); + return properties.reduce((acc, name) => { + acc[name] = transform(value[name]); + return acc; + }, {}) as unknown as T; + } + return value; +} + +/** + * Untransform value into its human readable form. + * @param value - Value to untransform. + * @returns Untransformed, human readable form of value. + */ +export function untransform(value: T): T | string { + if (Buffer.isBuffer(value)) { + return '0x' + value.toString('hex'); + } + if (value instanceof Array && value.length > 1) { + for (let i = 1; i < value.length; i++) { + if (value[i - 1].weight > value[i].weight) { + throw new Error('Array elements are not in canonical order'); + } + } + return value.map(untransform) as unknown as T; + } else if (value instanceof Object) { + const properties = Object.getOwnPropertyNames(value); + for (let i = 1; i < properties.length; i++) { + if (properties[i - 1].localeCompare(properties[i]) > 0) { + throw new Error('Object properties are not in caonical order'); + } + } + return properties.reduce((acc, name) => { + acc[name] = untransform(value[name]); + return acc; + }, {}) as unknown as T; + } + return value; +} + +/** + * Serialize a value. + * @param value - Value to serialize. + * @returns Buffer representing serialized value. + */ +export function serialize(value: T): Buffer { + return encodeCanonical(transform(value)); +} + +/** + * Deserialize a value. + * @param value - Buffer to deserialize. + * @returns Deserialized value. + */ +export function deserialize(value: Buffer): unknown { + return untransform(decodeFirstSync(value)); +} diff --git a/modules/deser-lib/src/index.ts b/modules/deser-lib/src/index.ts index 2e8d812ac8..269d09902d 100644 --- a/modules/deser-lib/src/index.ts +++ b/modules/deser-lib/src/index.ts @@ -1,192 +1 @@ -import { decodeFirstSync, encodeCanonical } from 'cbor'; - -/** - * Return a string describing value as a type. - * @param value - Any javascript value to type. - * @returns String describing value type. - */ -function getType(value: unknown): string { - if (value === null || value === undefined) { - return 'null'; - } - if (value instanceof Array) { - const types = value.map(getType); - if (!types.slice(1).every((value) => value === types[0])) { - throw new Error('Array elements are not of the same type'); - } - return JSON.stringify([types[0]]); - } - if (value instanceof Object) { - const properties = Object.getOwnPropertyNames(value); - properties.sort(); - return JSON.stringify( - properties.reduce((acc, name) => { - acc[name] = getType(value[name]); - return acc; - }, {}) - ); - } - if (typeof value === 'string') { - if (value.startsWith('0x')) { - return 'bytes'; - } - return 'string'; - } - return JSON.stringify(typeof value); -} - -/** - * Compare two buffers for sorting. - * @param a - left buffer to compare to right buffer. - * @param b - right buffer to compare to left buffer. - * @returns Negative if a < b, positive if b > a, 0 if equal. - */ -function bufferCompare(a: Buffer, b: Buffer): number { - let i = 0; - while (i < a.length && i < b.length && a[i] == b[i]) { - i++; - } - if (i === a.length && i === b.length) { - return 0; - } - if (i === a.length || i === b.length) { - return a.length - b.length; - } - return a[i] - b[i]; -} - -/** A sortable array element. */ -interface Sortable { - weight: number; - value?: unknown; -} - -/** - * Type check for sortable array element. - * @param value - Value to type check. - * @returns True if value is a sortable array element. - */ -function isSortable(value: unknown): value is Sortable { - return value instanceof Object && 'weight' in value; -} - -/** - * Compare two array elements for sorting. - * @param a - left element to compare to right element. - * @param b - right element to compare to left element. - * @returns Negative if a < b, positive if b > a, 0 if equal. - */ -function elementCompare(a: unknown, b: unknown): number { - if (!isSortable(a) || !isSortable(b)) { - throw new Error('Array elements must be sortable'); - } - if (a.weight === b.weight) { - if ('value' in a && 'value' in b) { - const aVal = transform(a.value); - const bVal = transform(b.value); - if ( - (!Buffer.isBuffer(aVal) && typeof aVal !== 'string' && typeof aVal !== 'number') || - (!Buffer.isBuffer(bVal) && typeof bVal !== 'string' && typeof bVal !== 'number') - ) { - throw new Error('Array element value cannot be compared'); - } - let aBuf, bBuf; - if (typeof aVal === 'number') { - aBuf = Buffer.from([aVal]); - } else { - aBuf = Buffer.from(aVal); - } - if (typeof bVal === 'number') { - bBuf = Buffer.from([bVal]); - } else { - bBuf = Buffer.from(bVal); - } - return bufferCompare(aBuf, bBuf); - } - throw new Error('Array elements must be sortable'); - } - return a.weight - b.weight; -} - -/** - * Transform value into its canonical, serializable form. - * @param value - Value to transform. - * @returns Canonical, serializable form of value. - */ -export function transform(value: T): T | Buffer { - if (value === null || value === undefined) { - return value; - } - if (typeof value === 'string') { - // Transform hex strings to buffers. - if (value.startsWith('0x')) { - if (!value.match(/^0x([0-9a-fA-F]{2})*$/)) { - throw new Error('0x prefixed string contains non-hex characters.'); - } - return Buffer.from(value.slice(2), 'hex'); - } - } else if (value instanceof Array) { - // Enforce array elements are same type. - getType(value); - value = [...value] as unknown as T; - (value as unknown as Array).sort(elementCompare); - return (value as unknown as Array).map(transform) as unknown as T; - } else if (value instanceof Object) { - const properties = Object.getOwnPropertyNames(value); - properties.sort(); - return properties.reduce((acc, name) => { - acc[name] = transform(value[name]); - return acc; - }, {}) as unknown as T; - } - return value; -} - -/** - * Untransform value into its human readable form. - * @param value - Value to untransform. - * @returns Untransformed, human readable form of value. - */ -export function untransform(value: T): T | string { - if (Buffer.isBuffer(value)) { - return '0x' + value.toString('hex'); - } - if (value instanceof Array && value.length > 1) { - for (let i = 1; i < value.length; i++) { - if (value[i - 1].weight > value[i].weight) { - throw new Error('Array elements are not in canonical order'); - } - } - return value.map(untransform) as unknown as T; - } else if (value instanceof Object) { - const properties = Object.getOwnPropertyNames(value); - for (let i = 1; i < properties.length; i++) { - if (properties[i - 1].localeCompare(properties[i]) > 0) { - throw new Error('Object properties are not in caonical order'); - } - } - return properties.reduce((acc, name) => { - acc[name] = untransform(value[name]); - return acc; - }, {}) as unknown as T; - } - return value; -} - -/** - * Serialize a value. - * @param value - Value to serialize. - * @returns Buffer representing serialized value. - */ -export function serialize(value: T): Buffer { - return encodeCanonical(transform(value)); -} - -/** - * Deserialize a value. - * @param value - Buffer to deserialize. - * @returns Deserialized value. - */ -export function deserialize(value: Buffer): unknown { - return untransform(decodeFirstSync(value)); -} +export * as cbor from './cbor'; diff --git a/modules/deser-lib/test/fixtures.json b/modules/deser-lib/test/cbor/fixtures.json similarity index 99% rename from modules/deser-lib/test/fixtures.json rename to modules/deser-lib/test/cbor/fixtures.json index 940440a87c..fd48fb708d 100644 --- a/modules/deser-lib/test/fixtures.json +++ b/modules/deser-lib/test/cbor/fixtures.json @@ -93,4 +93,4 @@ }, "serialized": "a1616183a26576616c7565016677656967687400a26576616c7565026677656967687400a26576616c7565036677656967687400" } -] +] \ No newline at end of file diff --git a/modules/deser-lib/test/unit/deser-lib.ts b/modules/deser-lib/test/unit/deser-lib.ts index e6839d3c0d..8c5306aeee 100644 --- a/modules/deser-lib/test/unit/deser-lib.ts +++ b/modules/deser-lib/test/unit/deser-lib.ts @@ -1,312 +1,314 @@ -import { transform, untransform, serialize, deserialize } from '../..'; -import * as fixtures from '../fixtures.json'; +import { cbor } from '../..'; +import * as cborFixtures from '../cbor/fixtures.json'; describe('deser-lib', function () { - describe('transform', function () { - it('orders object properties canonically', function () { - const res = transform({ b: 'second', a: 'first' }) as any; - const properties = Object.getOwnPropertyNames(res); - properties[0].should.equal('a'); - properties[1].should.equal('b'); - res.a.should.equal('first'); - res.b.should.equal('second'); - }); - - describe('canonical ordering', function () { - it('orders by weight', function () { - const res = transform([ - { weight: 2, value: null }, - { weight: 1, value: null }, - ]) as any; - res[0].weight.should.equal(1); - res[1].weight.should.equal(2); + describe('cbor', function () { + describe('transform', function () { + it('orders object properties canonically', function () { + const res = cbor.transform({ b: 'second', a: 'first' }) as any; + const properties = Object.getOwnPropertyNames(res); + properties[0].should.equal('a'); + properties[1].should.equal('b'); + res.a.should.equal('first'); + res.b.should.equal('second'); }); - it('groups equal elements', function () { - const res = transform([ - { - weight: 2, - value: 'b', - }, - { - weight: 1, - value: 'a', - }, - { - weight: 3, - value: 'c', - }, - { - weight: 2, - value: 'b', - }, - ]) as any; - res[0].weight.should.equal(1); - res[1].weight.should.equal(2); - res[2].weight.should.equal(2); - res[3].weight.should.equal(3); - }); + describe('canonical ordering', function () { + it('orders by weight', function () { + const res = cbor.transform([ + { weight: 2, value: null }, + { weight: 1, value: null }, + ]) as any; + res[0].weight.should.equal(1); + res[1].weight.should.equal(2); + }); - it('orders number values', function () { - const res = transform([ - { weight: 1, value: 2 }, - { weight: 1, value: 1 }, - ]) as any; - res[0].value.should.equal(1); - res[1].value.should.equal(2); - }); + it('groups equal elements', function () { + const res = cbor.transform([ + { + weight: 2, + value: 'b', + }, + { + weight: 1, + value: 'a', + }, + { + weight: 3, + value: 'c', + }, + { + weight: 2, + value: 'b', + }, + ]) as any; + res[0].weight.should.equal(1); + res[1].weight.should.equal(2); + res[2].weight.should.equal(2); + res[3].weight.should.equal(3); + }); - it('orders string values', function () { - const res = transform([ - { weight: 1, value: 'ab' }, - { weight: 1, value: 'aa' }, - ]) as any; - res[0].value.should.equal('aa'); - res[1].value.should.equal('ab'); - }); + it('orders number values', function () { + const res = cbor.transform([ + { weight: 1, value: 2 }, + { weight: 1, value: 1 }, + ]) as any; + res[0].value.should.equal(1); + res[1].value.should.equal(2); + }); - it('orders byte values', function () { - const res = transform([ - { weight: 1, value: '0x0b' }, - { weight: 1, value: '0x0a' }, - ]) as any; - res[0].value.equals(Buffer.from([0x0a])).should.equal(true); - res[1].value.equals(Buffer.from([0x0b])).should.equal(true); - }); + it('orders string values', function () { + const res = cbor.transform([ + { weight: 1, value: 'ab' }, + { weight: 1, value: 'aa' }, + ]) as any; + res[0].value.should.equal('aa'); + res[1].value.should.equal('ab'); + }); - it('orders string values of different lengths', function () { - const res = transform([ - { weight: 1, value: 'ab' }, - { weight: 1, value: 'a' }, - ]) as any; - res[0].value.should.equal('a'); - res[1].value.should.equal('ab'); - }); + it('orders byte values', function () { + const res = cbor.transform([ + { weight: 1, value: '0x0b' }, + { weight: 1, value: '0x0a' }, + ]) as any; + res[0].value.equals(Buffer.from([0x0a])).should.equal(true); + res[1].value.equals(Buffer.from([0x0b])).should.equal(true); + }); - it('throws for elements without weight', function () { - (() => transform([{}, {}])).should.throw(); - }); + it('orders string values of different lengths', function () { + const res = cbor.transform([ + { weight: 1, value: 'ab' }, + { weight: 1, value: 'a' }, + ]) as any; + res[0].value.should.equal('a'); + res[1].value.should.equal('ab'); + }); + + it('throws for elements without weight', function () { + (() => cbor.transform([{}, {}])).should.throw(); + }); + + it('throws for elements without value', function () { + (() => cbor.transform([{ weight: 1 }, { weight: 1 }])).should.throw(); + }); + + it('throws for values that cannot be compared', function () { + (() => + cbor.transform([ + { weight: 1, value: {} }, + { weight: 1, value: 1 }, + ])).should.throw(); + (() => + cbor.transform([ + { weight: 1, value: undefined }, + { weight: 1, value: null }, + ])).should.throw(); + }); - it('throws for elements without value', function () { - (() => transform([{ weight: 1 }, { weight: 1 }])).should.throw(); + it('throws for elements of mixed type', function () { + (() => + cbor.transform([ + { weight: 0, value: '0' }, + { weight: 0, value: 0 }, + ])).should.throw(); + }); }); - it('throws for values that cannot be compared', function () { - (() => - transform([ - { weight: 1, value: {} }, - { weight: 1, value: 1 }, - ])).should.throw(); - (() => - transform([ - { weight: 1, value: undefined }, - { weight: 1, value: null }, - ])).should.throw(); + it('preserves null values', function () { + const res = cbor.transform({ value: null }) as any; + res.should.have.property('value').which.is.null(); }); - it('throws for elements of mixed type', function () { - (() => - transform([ - { weight: 0, value: '0' }, - { weight: 0, value: 0 }, - ])).should.throw(); + it('replaces prefixed hex strings with Buffers', function () { + const hex = '00010203'; + const res = cbor.transform({ value: '0x' + hex }) as any; + Buffer.isBuffer(res.value).should.equal(true); + res.value.equals(Buffer.from(hex, 'hex')).should.equal(true); }); - }); - it('preserves null values', function () { - const res = transform({ value: null }) as any; - res.should.have.property('value').which.is.null(); - }); + it('preserves non-prefixed hex strings', function () { + const string = '00010203'; + const res = cbor.transform({ value: string }) as any; + res.value.should.equal(string); + }); - it('replaces prefixed hex strings with Buffers', function () { - const hex = '00010203'; - const res = transform({ value: '0x' + hex }) as any; - Buffer.isBuffer(res.value).should.equal(true); - res.value.equals(Buffer.from(hex, 'hex')).should.equal(true); - }); + it('transforms object recursively', function () { + const res = cbor.transform({ value: { b: 'second', a: 'first' } }) as any; + const properties = Object.getOwnPropertyNames(res.value); + properties[0].should.equal('a'); + properties[1].should.equal('b'); + res.value.a.should.equal('first'); + res.value.b.should.equal('second'); + }); - it('preserves non-prefixed hex strings', function () { - const string = '00010203'; - const res = transform({ value: string }) as any; - res.value.should.equal(string); - }); + it('transforms array recursively', function () { + const res = cbor.transform([{ weight: 0, value: { b: 'second', a: 'first' } }]) as any; + const properties = Object.getOwnPropertyNames(res[0].value); + properties[0].should.equal('a'); + properties[1].should.equal('b'); + res[0].value.a.should.equal('first'); + res[0].value.b.should.equal('second'); + }); - it('transforms object recursively', function () { - const res = transform({ value: { b: 'second', a: 'first' } }) as any; - const properties = Object.getOwnPropertyNames(res.value); - properties[0].should.equal('a'); - properties[1].should.equal('b'); - res.value.a.should.equal('first'); - res.value.b.should.equal('second'); + it('throws for invalid hex strings', function () { + (() => cbor.transform('0x0g')).should.throw(); + }); }); - it('transforms array recursively', function () { - const res = transform([{ weight: 0, value: { b: 'second', a: 'first' } }]) as any; - const properties = Object.getOwnPropertyNames(res[0].value); - properties[0].should.equal('a'); - properties[1].should.equal('b'); - res[0].value.a.should.equal('first'); - res[0].value.b.should.equal('second'); - }); + describe('untransform', function () { + it('untransforms object', function () { + const res = cbor.untransform({ a: 'first', b: 'second' }) as any; + const properties = Object.getOwnPropertyNames(res); + properties[0].should.equal('a'); + properties[1].should.equal('b'); + res.a.should.equal('first'); + res.b.should.equal('second'); + }); - it('throws for invalid hex strings', function () { - (() => transform('0x0g')).should.throw(); - }); - }); + it('enforces canonical object property order', function () { + (() => cbor.untransform({ b: 'second', a: 'first' })).should.throw(); + }); - describe('untransform', function () { - it('untransforms object', function () { - const res = untransform({ a: 'first', b: 'second' }) as any; - const properties = Object.getOwnPropertyNames(res); - properties[0].should.equal('a'); - properties[1].should.equal('b'); - res.a.should.equal('first'); - res.b.should.equal('second'); - }); + it('enforces canonical array element order', function () { + (() => cbor.untransform([{ weight: 2 }, { weight: 1 }])).should.throw(); + }); - it('enforces canonical object property order', function () { - (() => untransform({ b: 'second', a: 'first' })).should.throw(); - }); + it('replaces Buffers with prefixed hex strings', function () { + const hex = '00010203'; + const res = cbor.untransform({ value: Buffer.from(hex, 'hex') }) as any; + res.value.should.equal('0x' + hex); + }); - it('enforces canonical array element order', function () { - (() => untransform([{ weight: 2 }, { weight: 1 }])).should.throw(); - }); + it('preserves non-prefixed hex strings', function () { + const string = '00010203'; + const res = cbor.untransform({ value: string }) as any; + res.value.should.equal(string); + }); - it('replaces Buffers with prefixed hex strings', function () { - const hex = '00010203'; - const res = untransform({ value: Buffer.from(hex, 'hex') }) as any; - res.value.should.equal('0x' + hex); - }); + it('untransforms object recursively', function () { + const hex = '00010203'; + const res = cbor.untransform({ value: { value: Buffer.from(hex, 'hex') } }) as any; + res.value.value.should.equal('0x' + hex); + }); - it('preserves non-prefixed hex strings', function () { - const string = '00010203'; - const res = untransform({ value: string }) as any; - res.value.should.equal(string); + it('untransforms array recursively', function () { + const hex = '00010203'; + const res = cbor.untransform([{ value: Buffer.from(hex, 'hex'), weight: 0 }]) as any; + res[0].value.should.equal('0x' + hex); + }); }); - it('untransforms object recursively', function () { - const hex = '00010203'; - const res = untransform({ value: { value: Buffer.from(hex, 'hex') } }) as any; - res.value.value.should.equal('0x' + hex); - }); + describe('fixtures', function () { + xit('creates test vectors', function () { + const { writeFileSync } = require('fs'); + const deserialized = [ + { + keys: [ + { + key: '0x010203', + weight: 0, + }, + { + key: '0x040506', + weight: 1, + }, + ], + }, + { + a: '0xffffffff', + b: '0x00000000', + c: '0xffffffff', + d: [ + { + weight: 0, + }, + { + weight: 1, + }, + { + weight: 2, + }, + { + weight: 3, + }, + ], + }, + { + a: [ + { + value: 'a', + weight: 0, + }, + { + value: 'b', + weight: 0, + }, + { + value: 'c', + weight: 0, + }, + ], + }, + { + a: [ + { + weight: 0, + value: '0x0a', + }, + { + weight: 0, + value: '0x0b', + }, + { + weight: 0, + value: '0x0c', + }, + ], + }, + { + a: [ + { + weight: 0, + value: 1, + }, + { + weight: 0, + value: 2, + }, + { + weight: 0, + value: 3, + }, + ], + }, + ]; + const serialized = deserialized.map((x) => cbor.serialize(x).toString('hex')); + writeFileSync( + 'test/cbor/fixtures.json', + JSON.stringify( + deserialized.map((deserialized, i) => ({ + deserialized: cbor.untransform(cbor.transform(deserialized)), + serialized: serialized[i], + })), + null, + 2 + ) + ); + }); - it('untransforms array recursively', function () { - const hex = '00010203'; - const res = untransform([{ value: Buffer.from(hex, 'hex'), weight: 0 }]) as any; - res[0].value.should.equal('0x' + hex); - }); - }); + for (let i = 0; i < cborFixtures.length; i++) { + it(`deserializes vector[${i}]`, function () { + const { deserialized, serialized } = cborFixtures[i]; + cbor.serialize(deserialized).equals(Buffer.from(serialized, 'hex')).should.equal(true); + }); + } - describe('fixtures', function () { - xit('creates test vectors', function () { - const { writeFileSync } = require('fs'); - const deserialized = [ - { - keys: [ - { - key: '0x010203', - weight: 0, - }, - { - key: '0x040506', - weight: 1, - }, - ], - }, - { - a: '0xffffffff', - b: '0x00000000', - c: '0xffffffff', - d: [ - { - weight: 0, - }, - { - weight: 1, - }, - { - weight: 2, - }, - { - weight: 3, - }, - ], - }, - { - a: [ - { - value: 'a', - weight: 0, - }, - { - value: 'b', - weight: 0, - }, - { - value: 'c', - weight: 0, - }, - ], - }, - { - a: [ - { - weight: 0, - value: '0x0a', - }, - { - weight: 0, - value: '0x0b', - }, - { - weight: 0, - value: '0x0c', - }, - ], - }, - { - a: [ - { - weight: 0, - value: 1, - }, - { - weight: 0, - value: 2, - }, - { - weight: 0, - value: 3, - }, - ], - }, - ]; - const serialized = deserialized.map((x) => serialize(x).toString('hex')); - writeFileSync( - 'test/fixtures.json', - JSON.stringify( - deserialized.map((deserialized, i) => ({ - deserialized: untransform(transform(deserialized)), - serialized: serialized[i], - })), - null, - 2 - ) - ); + for (let i = 0; i < cborFixtures.length; i++) { + it(`serializes vector[${i}]`, function () { + const { deserialized, serialized } = cborFixtures[i]; + JSON.stringify(cbor.deserialize(Buffer.from(serialized, 'hex'))).should.equal(JSON.stringify(deserialized)); + }); + } }); - - for (let i = 0; i < fixtures.length; i++) { - it(`deserializes vector[${i}]`, function () { - const { deserialized, serialized } = fixtures[i]; - serialize(deserialized).equals(Buffer.from(serialized, 'hex')).should.equal(true); - }); - } - - for (let i = 0; i < fixtures.length; i++) { - it(`serializes vector[${i}]`, function () { - const { deserialized, serialized } = fixtures[i]; - JSON.stringify(deserialize(Buffer.from(serialized, 'hex'))).should.equal(JSON.stringify(deserialized)); - }); - } }); }); From 95cd39476895b2fd3e766683eb5e7129c200d516 Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Tue, 5 Dec 2023 13:49:12 -0600 Subject: [PATCH 07/16] feat(deser-lib): support escaped strings TICKET: HSM-236 --- modules/deser-lib/src/cbor.ts | 9 ++- modules/deser-lib/src/index.ts | 2 +- modules/deser-lib/test/unit/deser-lib.ts | 72 ++++++++++++++---------- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/modules/deser-lib/src/cbor.ts b/modules/deser-lib/src/cbor.ts index 2e8d812ac8..43eeba43d2 100644 --- a/modules/deser-lib/src/cbor.ts +++ b/modules/deser-lib/src/cbor.ts @@ -124,6 +124,8 @@ export function transform(value: T): T | Buffer { throw new Error('0x prefixed string contains non-hex characters.'); } return Buffer.from(value.slice(2), 'hex'); + } else if (value.startsWith('\\0x')) { + return value.slice(1) as unknown as T; } } else if (value instanceof Array) { // Enforce array elements are same type. @@ -150,8 +152,11 @@ export function transform(value: T): T | Buffer { export function untransform(value: T): T | string { if (Buffer.isBuffer(value)) { return '0x' + value.toString('hex'); - } - if (value instanceof Array && value.length > 1) { + } else if (typeof value === 'string') { + if (value.startsWith('0x')) { + return '\\' + value; + } + } else if (value instanceof Array && value.length > 1) { for (let i = 1; i < value.length; i++) { if (value[i - 1].weight > value[i].weight) { throw new Error('Array elements are not in canonical order'); diff --git a/modules/deser-lib/src/index.ts b/modules/deser-lib/src/index.ts index 269d09902d..646abe6760 100644 --- a/modules/deser-lib/src/index.ts +++ b/modules/deser-lib/src/index.ts @@ -1 +1 @@ -export * as cbor from './cbor'; +export * as Cbor from './cbor'; diff --git a/modules/deser-lib/test/unit/deser-lib.ts b/modules/deser-lib/test/unit/deser-lib.ts index 8c5306aeee..a7c48bfede 100644 --- a/modules/deser-lib/test/unit/deser-lib.ts +++ b/modules/deser-lib/test/unit/deser-lib.ts @@ -1,11 +1,11 @@ -import { cbor } from '../..'; +import { Cbor } from '../..'; import * as cborFixtures from '../cbor/fixtures.json'; describe('deser-lib', function () { describe('cbor', function () { describe('transform', function () { it('orders object properties canonically', function () { - const res = cbor.transform({ b: 'second', a: 'first' }) as any; + const res = Cbor.transform({ b: 'second', a: 'first' }) as any; const properties = Object.getOwnPropertyNames(res); properties[0].should.equal('a'); properties[1].should.equal('b'); @@ -15,7 +15,7 @@ describe('deser-lib', function () { describe('canonical ordering', function () { it('orders by weight', function () { - const res = cbor.transform([ + const res = Cbor.transform([ { weight: 2, value: null }, { weight: 1, value: null }, ]) as any; @@ -24,7 +24,7 @@ describe('deser-lib', function () { }); it('groups equal elements', function () { - const res = cbor.transform([ + const res = Cbor.transform([ { weight: 2, value: 'b', @@ -49,7 +49,7 @@ describe('deser-lib', function () { }); it('orders number values', function () { - const res = cbor.transform([ + const res = Cbor.transform([ { weight: 1, value: 2 }, { weight: 1, value: 1 }, ]) as any; @@ -58,7 +58,7 @@ describe('deser-lib', function () { }); it('orders string values', function () { - const res = cbor.transform([ + const res = Cbor.transform([ { weight: 1, value: 'ab' }, { weight: 1, value: 'aa' }, ]) as any; @@ -67,7 +67,7 @@ describe('deser-lib', function () { }); it('orders byte values', function () { - const res = cbor.transform([ + const res = Cbor.transform([ { weight: 1, value: '0x0b' }, { weight: 1, value: '0x0a' }, ]) as any; @@ -76,7 +76,7 @@ describe('deser-lib', function () { }); it('orders string values of different lengths', function () { - const res = cbor.transform([ + const res = Cbor.transform([ { weight: 1, value: 'ab' }, { weight: 1, value: 'a' }, ]) as any; @@ -85,21 +85,21 @@ describe('deser-lib', function () { }); it('throws for elements without weight', function () { - (() => cbor.transform([{}, {}])).should.throw(); + (() => Cbor.transform([{}, {}])).should.throw(); }); it('throws for elements without value', function () { - (() => cbor.transform([{ weight: 1 }, { weight: 1 }])).should.throw(); + (() => Cbor.transform([{ weight: 1 }, { weight: 1 }])).should.throw(); }); it('throws for values that cannot be compared', function () { (() => - cbor.transform([ + Cbor.transform([ { weight: 1, value: {} }, { weight: 1, value: 1 }, ])).should.throw(); (() => - cbor.transform([ + Cbor.transform([ { weight: 1, value: undefined }, { weight: 1, value: null }, ])).should.throw(); @@ -107,7 +107,7 @@ describe('deser-lib', function () { it('throws for elements of mixed type', function () { (() => - cbor.transform([ + Cbor.transform([ { weight: 0, value: '0' }, { weight: 0, value: 0 }, ])).should.throw(); @@ -115,25 +115,31 @@ describe('deser-lib', function () { }); it('preserves null values', function () { - const res = cbor.transform({ value: null }) as any; + const res = Cbor.transform({ value: null }) as any; res.should.have.property('value').which.is.null(); }); it('replaces prefixed hex strings with Buffers', function () { const hex = '00010203'; - const res = cbor.transform({ value: '0x' + hex }) as any; + const res = Cbor.transform({ value: '0x' + hex }) as any; Buffer.isBuffer(res.value).should.equal(true); res.value.equals(Buffer.from(hex, 'hex')).should.equal(true); }); it('preserves non-prefixed hex strings', function () { const string = '00010203'; - const res = cbor.transform({ value: string }) as any; + const res = Cbor.transform({ value: string }) as any; + res.value.should.equal(string); + }); + + it('preserves escaped strings', function () { + const string = '0xPrefixedString'; + const res = Cbor.transform({ value: '\\' + string }) as any; res.value.should.equal(string); }); it('transforms object recursively', function () { - const res = cbor.transform({ value: { b: 'second', a: 'first' } }) as any; + const res = Cbor.transform({ value: { b: 'second', a: 'first' } }) as any; const properties = Object.getOwnPropertyNames(res.value); properties[0].should.equal('a'); properties[1].should.equal('b'); @@ -142,7 +148,7 @@ describe('deser-lib', function () { }); it('transforms array recursively', function () { - const res = cbor.transform([{ weight: 0, value: { b: 'second', a: 'first' } }]) as any; + const res = Cbor.transform([{ weight: 0, value: { b: 'second', a: 'first' } }]) as any; const properties = Object.getOwnPropertyNames(res[0].value); properties[0].should.equal('a'); properties[1].should.equal('b'); @@ -151,13 +157,13 @@ describe('deser-lib', function () { }); it('throws for invalid hex strings', function () { - (() => cbor.transform('0x0g')).should.throw(); + (() => Cbor.transform('0x0g')).should.throw(); }); }); describe('untransform', function () { it('untransforms object', function () { - const res = cbor.untransform({ a: 'first', b: 'second' }) as any; + const res = Cbor.untransform({ a: 'first', b: 'second' }) as any; const properties = Object.getOwnPropertyNames(res); properties[0].should.equal('a'); properties[1].should.equal('b'); @@ -166,34 +172,40 @@ describe('deser-lib', function () { }); it('enforces canonical object property order', function () { - (() => cbor.untransform({ b: 'second', a: 'first' })).should.throw(); + (() => Cbor.untransform({ b: 'second', a: 'first' })).should.throw(); }); it('enforces canonical array element order', function () { - (() => cbor.untransform([{ weight: 2 }, { weight: 1 }])).should.throw(); + (() => Cbor.untransform([{ weight: 2 }, { weight: 1 }])).should.throw(); }); it('replaces Buffers with prefixed hex strings', function () { const hex = '00010203'; - const res = cbor.untransform({ value: Buffer.from(hex, 'hex') }) as any; + const res = Cbor.untransform({ value: Buffer.from(hex, 'hex') }) as any; res.value.should.equal('0x' + hex); }); it('preserves non-prefixed hex strings', function () { const string = '00010203'; - const res = cbor.untransform({ value: string }) as any; + const res = Cbor.untransform({ value: string }) as any; res.value.should.equal(string); }); + it('escapes prefixed string', function () { + const string = '0xPrefixedString'; + const res = Cbor.untransform({ value: string }) as any; + res.value.should.equal('\\' + string); + }); + it('untransforms object recursively', function () { const hex = '00010203'; - const res = cbor.untransform({ value: { value: Buffer.from(hex, 'hex') } }) as any; + const res = Cbor.untransform({ value: { value: Buffer.from(hex, 'hex') } }) as any; res.value.value.should.equal('0x' + hex); }); it('untransforms array recursively', function () { const hex = '00010203'; - const res = cbor.untransform([{ value: Buffer.from(hex, 'hex'), weight: 0 }]) as any; + const res = Cbor.untransform([{ value: Buffer.from(hex, 'hex'), weight: 0 }]) as any; res[0].value.should.equal('0x' + hex); }); }); @@ -282,12 +294,12 @@ describe('deser-lib', function () { ], }, ]; - const serialized = deserialized.map((x) => cbor.serialize(x).toString('hex')); + const serialized = deserialized.map((x) => Cbor.serialize(x).toString('hex')); writeFileSync( 'test/cbor/fixtures.json', JSON.stringify( deserialized.map((deserialized, i) => ({ - deserialized: cbor.untransform(cbor.transform(deserialized)), + deserialized: Cbor.untransform(Cbor.transform(deserialized)), serialized: serialized[i], })), null, @@ -299,14 +311,14 @@ describe('deser-lib', function () { for (let i = 0; i < cborFixtures.length; i++) { it(`deserializes vector[${i}]`, function () { const { deserialized, serialized } = cborFixtures[i]; - cbor.serialize(deserialized).equals(Buffer.from(serialized, 'hex')).should.equal(true); + Cbor.serialize(deserialized).equals(Buffer.from(serialized, 'hex')).should.equal(true); }); } for (let i = 0; i < cborFixtures.length; i++) { it(`serializes vector[${i}]`, function () { const { deserialized, serialized } = cborFixtures[i]; - JSON.stringify(cbor.deserialize(Buffer.from(serialized, 'hex'))).should.equal(JSON.stringify(deserialized)); + JSON.stringify(Cbor.deserialize(Buffer.from(serialized, 'hex'))).should.equal(JSON.stringify(deserialized)); }); } }); From ada42aa8e17c385a7a848bc438ce18b7d8b14718 Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Wed, 6 Dec 2023 08:36:24 -0600 Subject: [PATCH 08/16] chore(deser-lib): fix numeric comparison TICKET: HSM-236 --- modules/deser-lib/src/cbor.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/modules/deser-lib/src/cbor.ts b/modules/deser-lib/src/cbor.ts index 43eeba43d2..1df42b78da 100644 --- a/modules/deser-lib/src/cbor.ts +++ b/modules/deser-lib/src/cbor.ts @@ -70,6 +70,22 @@ function isSortable(value: unknown): value is Sortable { return value instanceof Object && 'weight' in value; } +/** + * Convert number to base 256 and return as a big-endian Buffer. + * @param value - Value to convert. + * @returs Buffer representation of the number. + */ +function numberToBufferBE(value: number): Buffer { + const byteCount = Math.floor((value.toString(2).length + 7) / 8); + const buffer = Buffer.alloc(byteCount); + let i = 0; + while (value) { + buffer[i++] = value % 256; + value = Math.floor(value / 256); + } + return buffer.reverse(); +} + /** * Compare two array elements for sorting. * @param a - left element to compare to right element. @@ -92,12 +108,12 @@ function elementCompare(a: unknown, b: unknown): number { } let aBuf, bBuf; if (typeof aVal === 'number') { - aBuf = Buffer.from([aVal]); + aBuf = numberToBufferBE(aVal); } else { aBuf = Buffer.from(aVal); } if (typeof bVal === 'number') { - bBuf = Buffer.from([bVal]); + bBuf = numberToBufferBE(bVal); } else { bBuf = Buffer.from(bVal); } From 6b18328723bbdad285cd9f27d16d5dd0019ccd5e Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Wed, 6 Dec 2023 09:20:39 -0600 Subject: [PATCH 09/16] chore(deser-lib): use fixed size buffer for integer conversion TICKET: HSM-236 --- modules/deser-lib/src/cbor.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/deser-lib/src/cbor.ts b/modules/deser-lib/src/cbor.ts index 1df42b78da..00a1945758 100644 --- a/modules/deser-lib/src/cbor.ts +++ b/modules/deser-lib/src/cbor.ts @@ -77,10 +77,11 @@ function isSortable(value: unknown): value is Sortable { */ function numberToBufferBE(value: number): Buffer { const byteCount = Math.floor((value.toString(2).length + 7) / 8); - const buffer = Buffer.alloc(byteCount); + const buffer = Buffer.alloc(8); + const offset = 8 - byteCount; let i = 0; while (value) { - buffer[i++] = value % 256; + buffer[offset + i++] = value % 256; value = Math.floor(value / 256); } return buffer.reverse(); From 4a6b2e443000297a7e666b00ae7366092b0de028 Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Wed, 6 Dec 2023 09:49:59 -0600 Subject: [PATCH 10/16] chore(deser-lib): normalize numbers before converting to buffer TICKET: HSM-236 --- modules/deser-lib/src/cbor.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modules/deser-lib/src/cbor.ts b/modules/deser-lib/src/cbor.ts index 00a1945758..4b187c18a1 100644 --- a/modules/deser-lib/src/cbor.ts +++ b/modules/deser-lib/src/cbor.ts @@ -76,8 +76,11 @@ function isSortable(value: unknown): value is Sortable { * @returs Buffer representation of the number. */ function numberToBufferBE(value: number): Buffer { + // Normalize value so that negative numbers aren't compared higher + // than positive numbers when accounting for two's complement. + value += Math.pow(2, 52); const byteCount = Math.floor((value.toString(2).length + 7) / 8); - const buffer = Buffer.alloc(8); + const buffer = Buffer.alloc(9); const offset = 8 - byteCount; let i = 0; while (value) { From a53605852d1959c7589ed7a71b5da94d52367333 Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Wed, 6 Dec 2023 15:11:01 -0600 Subject: [PATCH 11/16] chor(deser-lib): addressing review feedback TICKET: HSM-236 --- modules/deser-lib/package.json | 1 + modules/deser-lib/src/cbor.ts | 48 +++++++++++++++++----------------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/modules/deser-lib/package.json b/modules/deser-lib/package.json index 1edaacaf2b..62234a3e20 100644 --- a/modules/deser-lib/package.json +++ b/modules/deser-lib/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "BitGo serialization and deseralization library", "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", "scripts": { "test": "yarn unit-test", "unit-test": "nyc -- mocha --recursive test", diff --git a/modules/deser-lib/src/cbor.ts b/modules/deser-lib/src/cbor.ts index 4b187c18a1..ee0a7371a2 100644 --- a/modules/deser-lib/src/cbor.ts +++ b/modules/deser-lib/src/cbor.ts @@ -56,10 +56,10 @@ function bufferCompare(a: Buffer, b: Buffer): number { } /** A sortable array element. */ -interface Sortable { +type Sortable = { weight: number; value?: unknown; -} +}; /** * Type check for sortable array element. @@ -101,29 +101,29 @@ function elementCompare(a: unknown, b: unknown): number { throw new Error('Array elements must be sortable'); } if (a.weight === b.weight) { - if ('value' in a && 'value' in b) { - const aVal = transform(a.value); - const bVal = transform(b.value); - if ( - (!Buffer.isBuffer(aVal) && typeof aVal !== 'string' && typeof aVal !== 'number') || - (!Buffer.isBuffer(bVal) && typeof bVal !== 'string' && typeof bVal !== 'number') - ) { - throw new Error('Array element value cannot be compared'); - } - let aBuf, bBuf; - if (typeof aVal === 'number') { - aBuf = numberToBufferBE(aVal); - } else { - aBuf = Buffer.from(aVal); - } - if (typeof bVal === 'number') { - bBuf = numberToBufferBE(bVal); - } else { - bBuf = Buffer.from(bVal); - } - return bufferCompare(aBuf, bBuf); + if (a.value === undefined && b.value === undefined) { + throw new Error('Array elements must be sortable'); } - throw new Error('Array elements must be sortable'); + const aVal = transform(a.value); + const bVal = transform(b.value); + if ( + (!Buffer.isBuffer(aVal) && typeof aVal !== 'string' && typeof aVal !== 'number') || + (!Buffer.isBuffer(bVal) && typeof bVal !== 'string' && typeof bVal !== 'number') + ) { + throw new Error('Array element value cannot be compared'); + } + let aBuf, bBuf; + if (typeof aVal === 'number') { + aBuf = numberToBufferBE(aVal); + } else { + aBuf = Buffer.from(aVal); + } + if (typeof bVal === 'number') { + bBuf = numberToBufferBE(bVal); + } else { + bBuf = Buffer.from(bVal); + } + return bufferCompare(aBuf, bBuf); } return a.weight - b.weight; } From a7fd73f2f62aef5eb14428db210ad3743f04491f Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Fri, 8 Dec 2023 08:56:53 -0600 Subject: [PATCH 12/16] chore(deser-lib): don't include leading zeros in int buffer TICKET: HSM-236 --- modules/deser-lib/src/cbor.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/deser-lib/src/cbor.ts b/modules/deser-lib/src/cbor.ts index ee0a7371a2..3896037ad7 100644 --- a/modules/deser-lib/src/cbor.ts +++ b/modules/deser-lib/src/cbor.ts @@ -80,11 +80,10 @@ function numberToBufferBE(value: number): Buffer { // than positive numbers when accounting for two's complement. value += Math.pow(2, 52); const byteCount = Math.floor((value.toString(2).length + 7) / 8); - const buffer = Buffer.alloc(9); - const offset = 8 - byteCount; + const buffer = Buffer.alloc(byteCount); let i = 0; while (value) { - buffer[offset + i++] = value % 256; + buffer[i++] = value % 256; value = Math.floor(value / 256); } return buffer.reverse(); From 58a893784103d2a60e9dc692ee13964635f70ecb Mon Sep 17 00:00:00 2001 From: Zahin Mohammad Date: Tue, 12 Dec 2023 10:07:34 -0500 Subject: [PATCH 13/16] chore(deser-lib): fmt TICKET: HSM-236 --- modules/deser-lib/test/cbor/fixtures.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/deser-lib/test/cbor/fixtures.json b/modules/deser-lib/test/cbor/fixtures.json index fd48fb708d..940440a87c 100644 --- a/modules/deser-lib/test/cbor/fixtures.json +++ b/modules/deser-lib/test/cbor/fixtures.json @@ -93,4 +93,4 @@ }, "serialized": "a1616183a26576616c7565016677656967687400a26576616c7565026677656967687400a26576616c7565036677656967687400" } -] \ No newline at end of file +] From 4c6716ef4f594fa3bf9841c53b485332f3aafc58 Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Tue, 9 Jan 2024 09:55:50 -0600 Subject: [PATCH 14/16] chore(deser-lib): changes requested from review TICKET: HSM-236 --- modules/deser-lib/src/cbor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/deser-lib/src/cbor.ts b/modules/deser-lib/src/cbor.ts index 3896037ad7..8876d9e4e0 100644 --- a/modules/deser-lib/src/cbor.ts +++ b/modules/deser-lib/src/cbor.ts @@ -67,13 +67,13 @@ type Sortable = { * @returns True if value is a sortable array element. */ function isSortable(value: unknown): value is Sortable { - return value instanceof Object && 'weight' in value; + return value instanceof Object && 'weight' in value && typeof (value as Sortable).weight === 'number'; } /** * Convert number to base 256 and return as a big-endian Buffer. * @param value - Value to convert. - * @returs Buffer representation of the number. + * @returns Buffer representation of the number. */ function numberToBufferBE(value: number): Buffer { // Normalize value so that negative numbers aren't compared higher From 75425c8e1418edd6635594e84f436f05a2a8939a Mon Sep 17 00:00:00 2001 From: Zahin Mohammad Date: Wed, 10 Jan 2024 12:38:28 -0500 Subject: [PATCH 15/16] chore(root): update CODEOWNERS for deser-lib TICKET: HSM-236 --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 42bde240cc..0f2cd98241 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -75,6 +75,7 @@ /modules/sdk-api/ @BitGo/custody-experience @BitGo/wallet-platform /modules/sdk-core/ @BitGo/wallet-platform @BitGo/hsm /modules/sdk-lib-mpc/ @BitGo/wallet-platform @BitGo/hsm +/modules/deser-lib/ @BitGo/wallet-platform @BitGo/hsm /modules/sdk-rpc-wrapper @BitGo/ethalt-team /modules/sdk-test/ @BitGo/custody-experience @BitGo/wallet-platform /modules/sdk-unified-wallet @BitGo/ethalt-team From c9d7a38ffa55381e7668722cab40c6b9739bbb8f Mon Sep 17 00:00:00 2001 From: Zahin Mohammad Date: Wed, 10 Jan 2024 12:44:42 -0500 Subject: [PATCH 16/16] chore(deser-lib): add README TICKET: HSM-236 --- modules/deser-lib/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 modules/deser-lib/README.md diff --git a/modules/deser-lib/README.md b/modules/deser-lib/README.md new file mode 100644 index 0000000000..b3f4aec4ca --- /dev/null +++ b/modules/deser-lib/README.md @@ -0,0 +1,3 @@ +# Deser Lib + +This library will be used to centralize all the serialization and de-serialization schemes used in the bitgojs modules.