|
| 1 | +import { decodeFirstSync, encodeCanonical } from 'cbor'; |
| 2 | + |
| 3 | +/** |
| 4 | + * Return a string describing value as a type. |
| 5 | + * @param value - Any javascript value to type. |
| 6 | + * @returns String describing value type. |
| 7 | + */ |
| 8 | +function getType(value: unknown): string { |
| 9 | + if (value === null || value === undefined) { |
| 10 | + return 'null'; |
| 11 | + } |
| 12 | + if (value instanceof Array) { |
| 13 | + const types = value.map(getType); |
| 14 | + if (!types.slice(1).every((value) => value === types[0])) { |
| 15 | + throw new Error('Array elements are not of the same type'); |
| 16 | + } |
| 17 | + return JSON.stringify([types[0]]); |
| 18 | + } |
| 19 | + if (value instanceof Object) { |
| 20 | + const properties = Object.getOwnPropertyNames(value); |
| 21 | + properties.sort(); |
| 22 | + return JSON.stringify( |
| 23 | + properties.reduce((acc, name) => { |
| 24 | + acc[name] = getType(value[name]); |
| 25 | + return acc; |
| 26 | + }, {}) |
| 27 | + ); |
| 28 | + } |
| 29 | + if (typeof value === 'string') { |
| 30 | + if (value.startsWith('0x')) { |
| 31 | + return 'bytes'; |
| 32 | + } |
| 33 | + return 'string'; |
| 34 | + } |
| 35 | + return JSON.stringify(typeof value); |
| 36 | +} |
| 37 | + |
| 38 | +/** |
| 39 | + * Compare two buffers for sorting. |
| 40 | + * @param a - left buffer to compare to right buffer. |
| 41 | + * @param b - right buffer to compare to left buffer. |
| 42 | + * @returns Negative if a < b, positive if b > a, 0 if equal. |
| 43 | + */ |
| 44 | +function bufferCompare(a: Buffer, b: Buffer): number { |
| 45 | + let i = 0; |
| 46 | + while (i < a.length && i < b.length && a[i] == b[i]) { |
| 47 | + i++; |
| 48 | + } |
| 49 | + if (i === a.length && i === b.length) { |
| 50 | + return 0; |
| 51 | + } |
| 52 | + if (i === a.length || i === b.length) { |
| 53 | + return a.length - b.length; |
| 54 | + } |
| 55 | + return a[i] - b[i]; |
| 56 | +} |
| 57 | + |
| 58 | +/** A sortable array element. */ |
| 59 | +type Sortable = { |
| 60 | + weight: number; |
| 61 | + value?: unknown; |
| 62 | +}; |
| 63 | + |
| 64 | +/** |
| 65 | + * Type check for sortable array element. |
| 66 | + * @param value - Value to type check. |
| 67 | + * @returns True if value is a sortable array element. |
| 68 | + */ |
| 69 | +function isSortable(value: unknown): value is Sortable { |
| 70 | + return value instanceof Object && 'weight' in value && typeof (value as Sortable).weight === 'number'; |
| 71 | +} |
| 72 | + |
| 73 | +/** |
| 74 | + * Convert number to base 256 and return as a big-endian Buffer. |
| 75 | + * @param value - Value to convert. |
| 76 | + * @returns Buffer representation of the number. |
| 77 | + */ |
| 78 | +function numberToBufferBE(value: number): Buffer { |
| 79 | + // Normalize value so that negative numbers aren't compared higher |
| 80 | + // than positive numbers when accounting for two's complement. |
| 81 | + value += Math.pow(2, 52); |
| 82 | + const byteCount = Math.floor((value.toString(2).length + 7) / 8); |
| 83 | + const buffer = Buffer.alloc(byteCount); |
| 84 | + let i = 0; |
| 85 | + while (value) { |
| 86 | + buffer[i++] = value % 256; |
| 87 | + value = Math.floor(value / 256); |
| 88 | + } |
| 89 | + return buffer.reverse(); |
| 90 | +} |
| 91 | + |
| 92 | +/** |
| 93 | + * Compare two array elements for sorting. |
| 94 | + * @param a - left element to compare to right element. |
| 95 | + * @param b - right element to compare to left element. |
| 96 | + * @returns Negative if a < b, positive if b > a, 0 if equal. |
| 97 | + */ |
| 98 | +function elementCompare(a: unknown, b: unknown): number { |
| 99 | + if (!isSortable(a) || !isSortable(b)) { |
| 100 | + throw new Error('Array elements must be sortable'); |
| 101 | + } |
| 102 | + if (a.weight === b.weight) { |
| 103 | + if (a.value === undefined && b.value === undefined) { |
| 104 | + throw new Error('Array elements must be sortable'); |
| 105 | + } |
| 106 | + const aVal = transform(a.value); |
| 107 | + const bVal = transform(b.value); |
| 108 | + if ( |
| 109 | + (!Buffer.isBuffer(aVal) && typeof aVal !== 'string' && typeof aVal !== 'number') || |
| 110 | + (!Buffer.isBuffer(bVal) && typeof bVal !== 'string' && typeof bVal !== 'number') |
| 111 | + ) { |
| 112 | + throw new Error('Array element value cannot be compared'); |
| 113 | + } |
| 114 | + let aBuf, bBuf; |
| 115 | + if (typeof aVal === 'number') { |
| 116 | + aBuf = numberToBufferBE(aVal); |
| 117 | + } else { |
| 118 | + aBuf = Buffer.from(aVal); |
| 119 | + } |
| 120 | + if (typeof bVal === 'number') { |
| 121 | + bBuf = numberToBufferBE(bVal); |
| 122 | + } else { |
| 123 | + bBuf = Buffer.from(bVal); |
| 124 | + } |
| 125 | + return bufferCompare(aBuf, bBuf); |
| 126 | + } |
| 127 | + return a.weight - b.weight; |
| 128 | +} |
| 129 | + |
| 130 | +/** |
| 131 | + * Transform value into its canonical, serializable form. |
| 132 | + * @param value - Value to transform. |
| 133 | + * @returns Canonical, serializable form of value. |
| 134 | + */ |
| 135 | +export function transform<T>(value: T): T | Buffer { |
| 136 | + if (value === null || value === undefined) { |
| 137 | + return value; |
| 138 | + } |
| 139 | + if (typeof value === 'string') { |
| 140 | + // Transform hex strings to buffers. |
| 141 | + if (value.startsWith('0x')) { |
| 142 | + if (!value.match(/^0x([0-9a-fA-F]{2})*$/)) { |
| 143 | + throw new Error('0x prefixed string contains non-hex characters.'); |
| 144 | + } |
| 145 | + return Buffer.from(value.slice(2), 'hex'); |
| 146 | + } else if (value.startsWith('\\0x')) { |
| 147 | + return value.slice(1) as unknown as T; |
| 148 | + } |
| 149 | + } else if (value instanceof Array) { |
| 150 | + // Enforce array elements are same type. |
| 151 | + getType(value); |
| 152 | + value = [...value] as unknown as T; |
| 153 | + (value as unknown as Array<unknown>).sort(elementCompare); |
| 154 | + return (value as unknown as Array<unknown>).map(transform) as unknown as T; |
| 155 | + } else if (value instanceof Object) { |
| 156 | + const properties = Object.getOwnPropertyNames(value); |
| 157 | + properties.sort(); |
| 158 | + return properties.reduce((acc, name) => { |
| 159 | + acc[name] = transform(value[name]); |
| 160 | + return acc; |
| 161 | + }, {}) as unknown as T; |
| 162 | + } |
| 163 | + return value; |
| 164 | +} |
| 165 | + |
| 166 | +/** |
| 167 | + * Untransform value into its human readable form. |
| 168 | + * @param value - Value to untransform. |
| 169 | + * @returns Untransformed, human readable form of value. |
| 170 | + */ |
| 171 | +export function untransform<T>(value: T): T | string { |
| 172 | + if (Buffer.isBuffer(value)) { |
| 173 | + return '0x' + value.toString('hex'); |
| 174 | + } else if (typeof value === 'string') { |
| 175 | + if (value.startsWith('0x')) { |
| 176 | + return '\\' + value; |
| 177 | + } |
| 178 | + } else if (value instanceof Array && value.length > 1) { |
| 179 | + for (let i = 1; i < value.length; i++) { |
| 180 | + if (value[i - 1].weight > value[i].weight) { |
| 181 | + throw new Error('Array elements are not in canonical order'); |
| 182 | + } |
| 183 | + } |
| 184 | + return value.map(untransform) as unknown as T; |
| 185 | + } else if (value instanceof Object) { |
| 186 | + const properties = Object.getOwnPropertyNames(value); |
| 187 | + for (let i = 1; i < properties.length; i++) { |
| 188 | + if (properties[i - 1].localeCompare(properties[i]) > 0) { |
| 189 | + throw new Error('Object properties are not in caonical order'); |
| 190 | + } |
| 191 | + } |
| 192 | + return properties.reduce((acc, name) => { |
| 193 | + acc[name] = untransform(value[name]); |
| 194 | + return acc; |
| 195 | + }, {}) as unknown as T; |
| 196 | + } |
| 197 | + return value; |
| 198 | +} |
| 199 | + |
| 200 | +/** |
| 201 | + * Serialize a value. |
| 202 | + * @param value - Value to serialize. |
| 203 | + * @returns Buffer representing serialized value. |
| 204 | + */ |
| 205 | +export function serialize<T>(value: T): Buffer { |
| 206 | + return encodeCanonical(transform(value)); |
| 207 | +} |
| 208 | + |
| 209 | +/** |
| 210 | + * Deserialize a value. |
| 211 | + * @param value - Buffer to deserialize. |
| 212 | + * @returns Deserialized value. |
| 213 | + */ |
| 214 | +export function deserialize(value: Buffer): unknown { |
| 215 | + return untransform(decodeFirstSync(value)); |
| 216 | +} |
0 commit comments