Skip to content

Commit

Permalink
feat(deser-lib): add serialization/deserialization module
Browse files Browse the repository at this point in the history
Issue: HSM-236
  • Loading branch information
johnoliverdriscoll committed Nov 29, 2023
1 parent 94d6afb commit c9b715b
Show file tree
Hide file tree
Showing 11 changed files with 640 additions and 0 deletions.
5 changes: 5 additions & 0 deletions modules/deser-lib/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules
.idea
public
dist

3 changes: 3 additions & 0 deletions modules/deser-lib/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.idea/
dist/
9 changes: 9 additions & 0 deletions modules/deser-lib/.mocharc.json
Original file line number Diff line number Diff line change
@@ -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"]
}
2 changes: 2 additions & 0 deletions modules/deser-lib/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.nyc_output/
dist/
3 changes: 3 additions & 0 deletions modules/deser-lib/.prettierrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
printWidth: 120
singleQuote: true
trailingComma: 'es5'
42 changes: 42 additions & 0 deletions modules/deser-lib/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"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 <[email protected]>",
"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"
}
}
144 changes: 144 additions & 0 deletions modules/deser-lib/src/index.ts
Original file line number Diff line number Diff line change
@@ -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));
}
96 changes: 96 additions & 0 deletions modules/deser-lib/test/fixtures.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
Loading

0 comments on commit c9b715b

Please sign in to comment.