-
Notifications
You must be signed in to change notification settings - Fork 283
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(deser-lib): add serialization/deserialization module
Issue: HSM-236
- Loading branch information
1 parent
94d6afb
commit c9b715b
Showing
11 changed files
with
640 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
node_modules | ||
.idea | ||
public | ||
dist | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
node_modules/ | ||
.idea/ | ||
dist/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.nyc_output/ | ||
dist/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
printWidth: 120 | ||
singleQuote: true | ||
trailingComma: 'es5' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
] |
Oops, something went wrong.