Skip to content

Commit

Permalink
chore(deser-lib): move current code to cbor namespace
Browse files Browse the repository at this point in the history
TICKET: HSM-236
  • Loading branch information
johnoliverdriscoll committed Dec 4, 2023
1 parent 7324754 commit f8c9839
Show file tree
Hide file tree
Showing 4 changed files with 474 additions and 471 deletions.
192 changes: 192 additions & 0 deletions modules/deser-lib/src/cbor.ts
Original file line number Diff line number Diff line change
@@ -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<T>(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<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;
}, {}) 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<T>(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<T>(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));
}
193 changes: 1 addition & 192 deletions modules/deser-lib/src/index.ts
Original file line number Diff line number Diff line change
@@ -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<T>(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<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;
}, {}) 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<T>(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<T>(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';
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,4 @@
},
"serialized": "a1616183a26576616c7565016677656967687400a26576616c7565026677656967687400a26576616c7565036677656967687400"
}
]
]
Loading

0 comments on commit f8c9839

Please sign in to comment.