Skip to content

Commit

Permalink
chore(deser-lib): remove canonical ordering
Browse files Browse the repository at this point in the history
TICKET: HSM-627
  • Loading branch information
johnoliverdriscoll committed Jan 23, 2025
1 parent 98b16cf commit 6880375
Show file tree
Hide file tree
Showing 2 changed files with 0 additions and 236 deletions.
135 changes: 0 additions & 135 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,10 +20,7 @@ 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);
Expand All @@ -176,11 +46,6 @@ 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);
Expand Down
101 changes: 0 additions & 101 deletions modules/deser-lib/test/unit/deser-lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,107 +13,6 @@ describe('deser-lib', function () {
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 Down

0 comments on commit 6880375

Please sign in to comment.