Skip to content

Commit

Permalink
Merge branch 'HSM-627-update-deser-lib'
Browse files Browse the repository at this point in the history
  • Loading branch information
johnoliverdriscoll committed Jan 24, 2025
2 parents 979d800 + 99b9878 commit 745715b
Show file tree
Hide file tree
Showing 2 changed files with 10 additions and 269 deletions.
141 changes: 0 additions & 141 deletions modules/deser-lib/src/cbor.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -147,14 +20,10 @@ export function transform<T>(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<unknown>).sort(elementCompare);
return (value as unknown as Array<unknown>).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;
Expand All @@ -176,19 +45,9 @@ export function untransform<T>(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;
Expand Down
138 changes: 10 additions & 128 deletions modules/deser-lib/test/unit/deser-lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 () {
Expand All @@ -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;
Expand Down

0 comments on commit 745715b

Please sign in to comment.