From 99b98785bbda186df59216796f2b5eb16c763179 Mon Sep 17 00:00:00 2001 From: John Driscoll Date: Thu, 23 Jan 2025 16:54:38 -0600 Subject: [PATCH] chore(deser-lib): remove canonical ordering TICKET: HSM-627 --- modules/deser-lib/src/cbor.ts | 141 ----------------------- modules/deser-lib/test/unit/deser-lib.ts | 138 ++-------------------- 2 files changed, 10 insertions(+), 269 deletions(-) diff --git a/modules/deser-lib/src/cbor.ts b/modules/deser-lib/src/cbor.ts index a26299eb34..951e2ff33c 100644 --- a/modules/deser-lib/src/cbor.ts +++ b/modules/deser-lib/src/cbor.ts @@ -1,132 +1,5 @@ import { decodeFirstSync, encode } 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 as string).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. */ -type 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 && typeof (value as Sortable).weight === 'number'; -} - -/** - * Convert number to base 256 and return as a big-endian Buffer. - * @param value - Value to convert. - * @returns 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(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. - * @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 (a.value === undefined && b.value === undefined) { - 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; -} - /** * Transform value into its canonical, serializable form. * @param value - Value to transform. @@ -147,14 +20,10 @@ export function transform(value: T): T | Buffer { return value.slice(1) as unknown as T; } } 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; @@ -176,19 +45,9 @@ export function untransform(value: T): T | string { 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'); - } - } 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 canonical order'); - } - } return properties.reduce((acc, name) => { acc[name] = untransform(value[name]); return acc; diff --git a/modules/deser-lib/test/unit/deser-lib.ts b/modules/deser-lib/test/unit/deser-lib.ts index a7c48bfede..885a37b993 100644 --- a/modules/deser-lib/test/unit/deser-lib.ts +++ b/modules/deser-lib/test/unit/deser-lib.ts @@ -4,116 +4,6 @@ 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 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 = 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('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 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 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 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('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 of mixed type', function () { - (() => - Cbor.transform([ - { weight: 0, value: '0' }, - { weight: 0, value: 0 }, - ])).should.throw(); - }); - }); - it('preserves null values', function () { const res = Cbor.transform({ value: null }) as any; res.should.have.property('value').which.is.null(); @@ -139,21 +29,21 @@ describe('deser-lib', function () { }); it('transforms object recursively', function () { - const res = Cbor.transform({ value: { b: 'second', a: 'first' } }) as any; + const res = Cbor.transform({ value: { b: 'first', a: 'second' } }) 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'); + properties[0].should.equal('b'); + properties[1].should.equal('a'); + res.value.b.should.equal('first'); + res.value.a.should.equal('second'); }); 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: 'first', a: 'second' } }]) 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'); + properties[0].should.equal('b'); + properties[1].should.equal('a'); + res[0].value.b.should.equal('first'); + res[0].value.a.should.equal('second'); }); it('throws for invalid hex strings', function () { @@ -171,14 +61,6 @@ describe('deser-lib', function () { res.b.should.equal('second'); }); - it('enforces canonical object property order', function () { - (() => Cbor.untransform({ b: 'second', a: 'first' })).should.throw(); - }); - - it('enforces canonical array element order', function () { - (() => 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;