├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .npmrc ├── README.md ├── package.json ├── src ├── constants.js ├── decoder.js ├── encoder.js ├── index.js └── special.js └── test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | root: true, 5 | extends: 'airbnb-base', 6 | parser: 'babel-eslint', 7 | parserOptions: { 8 | ecmaVersion: 2020, 9 | sourceType: 'script', 10 | }, 11 | env: { 12 | es6: true, 13 | node: true, 14 | }, 15 | overrides: [ 16 | { 17 | files: ['*.jsx'], 18 | parserOptions: { 19 | sourceType: 'module', 20 | ecmaFeatures: { jsx: true }, 21 | }, 22 | }, 23 | { 24 | files: ['*.mjs'], 25 | parserOptions: { sourceType: 'module' }, 26 | env: { 27 | node: true, 28 | }, 29 | rules: { 30 | 'no-restricted-globals': ['error', 'require'], 31 | }, 32 | }, 33 | { 34 | files: ['*.web.js'], 35 | env: { browser: true }, 36 | }, 37 | ], 38 | rules: { 39 | 'strict': ['error', 'global'], 40 | 'indent': ['error', 2, { 41 | SwitchCase: 1, 42 | FunctionDeclaration: { 43 | parameters: 'first', 44 | }, 45 | FunctionExpression: { 46 | parameters: 'first', 47 | }, 48 | CallExpression: { 49 | arguments: 'first', 50 | }, 51 | }], 52 | 'no-bitwise': 'off', 53 | 'no-iterator': 'off', 54 | 'global-require': 'off', 55 | 'quote-props': ['error', 'consistent-as-needed'], 56 | 'brace-style': ['error', '1tbs', { allowSingleLine: false }], 57 | 'curly': ['error', 'all'], 58 | 'no-param-reassign': 'off', 59 | 'arrow-parens': ['error', 'always'], 60 | 'no-multi-assign': 'off', 61 | 'no-underscore-dangle': 'off', 62 | 'no-restricted-syntax': 'off', 63 | 'object-curly-newline': 'off', 64 | 'prefer-const': ['error', { destructuring: 'all' }], 65 | 'class-methods-use-this': 'off', 66 | 'implicit-arrow-linebreak': 'off', 67 | 'lines-between-class-members': 'off', 68 | 'import/no-dynamic-require': 'off', 69 | 'import/no-extraneous-dependencies': ['error', { 70 | devDependencies: true, 71 | }], 72 | 'import/extensions': 'off', 73 | 'import/prefer-default-export': 'off', 74 | 'max-classes-per-file': 'off', 75 | }, 76 | globals: { 77 | WebAssembly: false, 78 | BigInt: false, 79 | BigInt64Array: false, 80 | BigUint64Array: false, 81 | URL: false, 82 | Atomics: false, 83 | SharedArrayBuffer: false, 84 | globalThis: false, 85 | FinalizationRegistry: false, 86 | WeakRef: false, 87 | queueMicrotask: false, 88 | }, 89 | }; 90 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [devsnek] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # earl 2 | 3 | Pure JavaScript ETF encoder/decoder 4 | 5 | ```js 6 | const { pack, unpack } = require('@earl'); 7 | 8 | const buf = pack({ a: 1n }); 9 | 10 | console.log(unpack(buf)); // { a : 1n } 11 | ``` 12 | 13 | ### Additional APIs for Erlang/OTP Compat 14 | 15 | #### `packTuple` 16 | 17 | ```js 18 | const { packTuple } = require('@earl'); 19 | 20 | const buf = packTuple([1, 2, 3]); // uses TUPLE_EXT instead of LIST_EXT 21 | ``` 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@devsnek/earl", 3 | "version": "1.0.0", 4 | "description": "JavaScript library for Erlang External Term Format", 5 | "main": "src/index.js", 6 | "devDependencies": { 7 | "@devsnek/fuzzy": "github:devsnek/fuzzy", 8 | "babel-eslint": "^10.1.0", 9 | "erlpack": "github:discordapp/erlpack", 10 | "eslint": "^6.1.0", 11 | "eslint-config-airbnb-base": "^14.0.0", 12 | "eslint-plugin-import": "^2.20.1" 13 | }, 14 | "scripts": { 15 | "test": "node ./node_modules/.bin/jest" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/devsnek/earl.git" 20 | }, 21 | "author": "devsnek", 22 | "license": "MIT", 23 | "gypfile": true, 24 | "bugs": { 25 | "url": "https://github.com/devsnek/earl/issues" 26 | }, 27 | "homepage": "https://github.com/devsnek/earl#readme" 28 | } 29 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | FORMAT_VERSION: 131, 5 | 6 | NEW_FLOAT_EXT: 70, 7 | BIT_BINARY_EXT: 77, 8 | NEW_PID_EXT: 88, 9 | NEWER_REFERENCE_EXT: 90, 10 | SMALL_INTEGER_EXT: 97, 11 | INTEGER_EXT: 98, 12 | FLOAT_EXT: 99, 13 | ATOM_EXT: 100, 14 | REFERENCE_EXT: 101, 15 | PORT_EXT: 102, 16 | PID_EXT: 103, 17 | SMALL_TUPLE_EXT: 104, 18 | LARGE_TUPLE_EXT: 105, 19 | NIL_EXT: 106, 20 | STRING_EXT: 107, 21 | LIST_EXT: 108, 22 | BINARY_EXT: 109, 23 | SMALL_BIG_EXT: 110, 24 | LARGE_BIG_EXT: 111, 25 | NEW_FUN_EXT: 112, 26 | EXPORT_EXT: 113, 27 | NEW_REFERENCE_EXT: 114, 28 | SMALL_ATOM_EXT: 115, 29 | ATOM_UTF8_EXT: 118, 30 | SMALL_ATOM_UTF8_EXT: 119, 31 | MAP_EXT: 116, 32 | FUN_EXT: 117, 33 | COMPRESSED: 80, 34 | }; 35 | -------------------------------------------------------------------------------- /src/decoder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | FORMAT_VERSION, 5 | 6 | NEW_FLOAT_EXT, 7 | // BIT_BINARY_EXT, 8 | SMALL_INTEGER_EXT, 9 | INTEGER_EXT, 10 | FLOAT_EXT, 11 | ATOM_EXT, 12 | ATOM_UTF8_EXT, 13 | // REFERENCE_EXT, 14 | // PORT_EXT, 15 | NEW_PID_EXT, 16 | // PID_EXT, 17 | SMALL_TUPLE_EXT, 18 | LARGE_TUPLE_EXT, 19 | NIL_EXT, 20 | STRING_EXT, 21 | LIST_EXT, 22 | BINARY_EXT, 23 | SMALL_BIG_EXT, 24 | LARGE_BIG_EXT, 25 | NEW_FUN_EXT, 26 | // EXPORT_EXT, 27 | // NEW_REFERENCE_EXT, 28 | NEWER_REFERENCE_EXT, 29 | SMALL_ATOM_EXT, 30 | SMALL_ATOM_UTF8_EXT, 31 | MAP_EXT, 32 | // FUN_EXT, 33 | // COMPRESSED, 34 | } = require('./constants'); 35 | const { Reference, Pid, ImproperList } = require('./special'); 36 | 37 | const textDecoder = new TextDecoder(); 38 | 39 | const processAtom = (atom, atomToString) => { 40 | if (atom === 'nil' || atom === 'null') { 41 | return null; 42 | } 43 | 44 | if (atom === 'true') { 45 | return true; 46 | } 47 | 48 | if (atom === 'false') { 49 | return false; 50 | } 51 | 52 | if (atomToString) { 53 | if (!atom) { 54 | return undefined; 55 | } 56 | return atom; 57 | } 58 | return Symbol(atom); 59 | }; 60 | 61 | module.exports = class Decoder { 62 | constructor(buffer, { bigintToString, atomToString, mapToObject } = {}) { 63 | if (ArrayBuffer.isView(buffer)) { 64 | this.buffer = buffer; 65 | this.view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); 66 | } else { 67 | this.buffer = new Uint8Array(buffer); 68 | this.view = new DataView(buffer); 69 | } 70 | this.offset = 0; 71 | 72 | this.bigintToString = bigintToString; 73 | this.atomToString = atomToString; 74 | this.mapToObject = mapToObject; 75 | 76 | const version = this.read8(); 77 | if (version !== FORMAT_VERSION) { 78 | throw new Error(`invalid version header ${version}`); 79 | } 80 | } 81 | 82 | read8() { 83 | const val = this.view.getUint8(this.offset); 84 | this.offset += 1; 85 | return val; 86 | } 87 | 88 | readi8() { 89 | const val = this.view.getInt8(this.offset); 90 | this.offset += 1; 91 | return val; 92 | } 93 | 94 | read16() { 95 | const val = this.view.getUint16(this.offset); 96 | this.offset += 2; 97 | return val; 98 | } 99 | 100 | read32() { 101 | const val = this.view.getUint32(this.offset); 102 | this.offset += 4; 103 | return val; 104 | } 105 | 106 | readi32() { 107 | const val = this.view.getInt32(this.offset); 108 | this.offset += 4; 109 | return val; 110 | } 111 | 112 | readDouble() { 113 | const val = this.view.getFloat64(this.offset); 114 | this.offset += 8; 115 | return val; 116 | } 117 | 118 | readString(length) { 119 | const sub = this.buffer.subarray(this.offset, this.offset + length); 120 | const str = textDecoder.decode(sub); 121 | this.offset += length; 122 | return str; 123 | } 124 | 125 | decodeArray(length) { 126 | const array = []; 127 | for (let i = 0; i < length; i += 1) { 128 | array.push(this.unpack()); 129 | } 130 | return array; 131 | } 132 | 133 | decodeBigNumber(digits) { 134 | const sign = this.read8(); 135 | 136 | let value = 0; 137 | let b = 1; 138 | 139 | for (let i = 0; i < digits; i += 1) { 140 | const digit = this.read8(); 141 | value += digit * b; 142 | b <<= 8; 143 | } 144 | 145 | if (digits < 4) { 146 | if (sign === 0) { 147 | return value; 148 | } 149 | 150 | const isSignBitAvailable = (value & (1 << 31)) === 0; 151 | if (isSignBitAvailable) { 152 | return -value; 153 | } 154 | } 155 | 156 | return sign === 0 ? value : -value; 157 | } 158 | 159 | decodeBigInt(digits) { 160 | const sign = this.read8(); 161 | 162 | let value = 0n; 163 | let b = 1n; 164 | 165 | for (let i = 0; i < digits; i += 1) { 166 | const digit = BigInt(this.read8()); 167 | value += digit * b; 168 | b <<= 8n; 169 | } 170 | 171 | const v = sign === 0 ? value : -value; 172 | if (this.bigintToString) { 173 | return v.toString(); 174 | } 175 | return v; 176 | } 177 | 178 | decodeAtom(type, atomToString) { 179 | type = type ?? this.read8(); 180 | if (type === SMALL_ATOM_EXT || type === SMALL_ATOM_UTF8_EXT) { 181 | return processAtom( 182 | this.readString(this.read8()), 183 | atomToString ?? this.atomToString, 184 | ); 185 | } 186 | if (type === ATOM_EXT || type === ATOM_UTF8_EXT) { 187 | return processAtom( 188 | this.readString(this.read16()), 189 | atomToString ?? this.atomToString, 190 | ); 191 | } 192 | throw new RangeError(`unknown atom type ${type}`); 193 | } 194 | 195 | unpack() { 196 | const type = this.read8(); 197 | switch (type) { 198 | case SMALL_INTEGER_EXT: 199 | return this.readi8(); 200 | case INTEGER_EXT: 201 | return this.readi32(); 202 | case FLOAT_EXT: 203 | return Number.parseFloat(this.readString(31)); 204 | case NEW_FLOAT_EXT: 205 | return this.readDouble(); 206 | case ATOM_EXT: 207 | case ATOM_UTF8_EXT: 208 | case SMALL_ATOM_EXT: 209 | case SMALL_ATOM_UTF8_EXT: 210 | return this.decodeAtom(type); 211 | case SMALL_TUPLE_EXT: 212 | return this.decodeArray(this.read8()); 213 | case LARGE_TUPLE_EXT: 214 | return this.decodeArray(this.read32()); 215 | case NIL_EXT: 216 | return []; 217 | case STRING_EXT: { 218 | const length = this.read16(); 219 | const sub = this.buffer.subarray(this.offset, this.offset + length); 220 | this.offset += length; 221 | return [...sub]; 222 | } 223 | case LIST_EXT: { 224 | const length = this.read32(); 225 | const array = this.decodeArray(length); 226 | if (this.buffer[this.offset] === NIL_EXT) { 227 | this.read8(); 228 | return array; 229 | } 230 | const tail = this.unpack(); 231 | return new ImproperList(array, tail); 232 | } 233 | case MAP_EXT: { 234 | const length = this.read32(); 235 | 236 | if (this.mapToObject) { 237 | const map = {}; 238 | for (let i = 0; i < length; i += 1) { 239 | map[this.unpack()] = this.unpack(); 240 | } 241 | return map; 242 | } 243 | 244 | const map = new Map(); 245 | for (let i = 0; i < length; i += 1) { 246 | map.set(this.unpack(), this.unpack()); 247 | } 248 | return map; 249 | } 250 | case BINARY_EXT: { 251 | const length = this.read32(); 252 | return this.readString(length); 253 | } 254 | case SMALL_BIG_EXT: { 255 | const digits = this.read8(); 256 | return digits >= 7 ? this.decodeBigInt(digits) : this.decodeBigNumber(digits); 257 | } 258 | case LARGE_BIG_EXT: { 259 | const digits = this.read32(); 260 | return this.decodeBigInt(digits); 261 | } 262 | case NEW_PID_EXT: 263 | return new Pid( 264 | this.decodeAtom(null, false).description, 265 | this.read32(), 266 | this.read32(), 267 | this.read32(), 268 | ); 269 | case NEWER_REFERENCE_EXT: { 270 | const len = this.read16(); 271 | const node = this.decodeAtom(null, false).description; 272 | const creation = this.read32(); 273 | const id = []; 274 | for (let i = 0; i < len; i += 1) { 275 | id.push(this.read32()); 276 | } 277 | return new Reference(node, creation, id); 278 | } 279 | case NEW_FUN_EXT: { 280 | const size = this.read32(); 281 | const arity = this.read8(); 282 | const unique = [this.read32(), this.read32(), this.read32(), this.read32()]; 283 | const index = this.read32(); 284 | const numFree = this.read32(); 285 | const module = this.unpack(); 286 | const oldIndex = this.unpack(); 287 | const oldUniq = this.unpack(); 288 | const pid = this.unpack(); 289 | const freeVars = Array.from({ length: numFree }, () => this.unpack()); 290 | return { 291 | size, 292 | arity, 293 | unique, 294 | index, 295 | numFree, 296 | module, 297 | oldIndex, 298 | oldUniq, 299 | pid, 300 | freeVars, 301 | }; 302 | } 303 | default: 304 | throw new Error(`unsupported etf type ${type}`); 305 | } 306 | } 307 | }; 308 | -------------------------------------------------------------------------------- /src/encoder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Tuple, Reference, Pid, ImproperList } = require('./special'); 4 | 5 | const { 6 | FORMAT_VERSION, 7 | 8 | NEW_FLOAT_EXT, 9 | // BIT_BINARY_EXT, 10 | SMALL_INTEGER_EXT, 11 | INTEGER_EXT, 12 | // FLOAT_EXT, 13 | ATOM_EXT, 14 | ATOM_UTF8_EXT, 15 | // REFERENCE_EXT, 16 | // PORT_EXT, 17 | // PID_EXT, 18 | NEW_PID_EXT, 19 | SMALL_TUPLE_EXT, 20 | LARGE_TUPLE_EXT, 21 | NIL_EXT, 22 | // STRING_EXT, 23 | LIST_EXT, 24 | BINARY_EXT, 25 | // SMALL_BIG_EXT, 26 | LARGE_BIG_EXT, 27 | // NEW_FUN_EXT, 28 | // EXPORT_EXT, 29 | NEWER_REFERENCE_EXT, 30 | SMALL_ATOM_EXT, 31 | SMALL_ATOM_UTF8_EXT, 32 | MAP_EXT, 33 | // FUN_EXT, 34 | // COMPRESSED, 35 | } = require('./constants'); 36 | 37 | const textEncoder = new TextEncoder(); 38 | 39 | class Encoder { 40 | constructor() { 41 | this.buffer = new Uint8Array(2048); 42 | this.view = new DataView(this.buffer.buffer); 43 | this.buffer[0] = FORMAT_VERSION; 44 | this.offset = 1; 45 | } 46 | 47 | grow(length) { 48 | if (this.offset + length < this.buffer.length) { 49 | return; 50 | } 51 | const old = this.buffer; 52 | this.buffer = new Uint8Array(old.length * 2); 53 | this.buffer.set(old); 54 | this.view = new DataView(this.buffer.buffer); 55 | } 56 | 57 | write(v) { 58 | this.grow(v.length); 59 | this.buffer.set(v, this.offset); 60 | this.offset += v.length; 61 | } 62 | 63 | write8(v) { 64 | this.grow(1); 65 | this.view.setUint8(this.offset, v); 66 | this.offset += 1; 67 | } 68 | 69 | write16(v) { 70 | this.grow(2); 71 | this.view.setUint16(this.offset, v); 72 | this.offset += 2; 73 | } 74 | 75 | write32(v) { 76 | this.grow(4); 77 | this.view.setUint32(this.offset, v); 78 | this.offset += 4; 79 | } 80 | 81 | writeFloat(v) { 82 | this.grow(8); 83 | this.view.setFloat64(this.offset, v); 84 | this.offset += 8; 85 | } 86 | 87 | appendAtom(atom) { 88 | const a = textEncoder.encode(atom); 89 | const isUtf8 = /[^\u0000-\u00FF]/u.test(atom); // eslint-disable-line no-control-regex 90 | if (a.length < 256) { 91 | this.write8(isUtf8 ? SMALL_ATOM_UTF8_EXT : SMALL_ATOM_EXT); 92 | this.write8(a.length); 93 | } else { 94 | this.write8(isUtf8 ? ATOM_UTF8_EXT : ATOM_EXT); 95 | this.write16(a.length); 96 | } 97 | this.write(a); 98 | } 99 | 100 | pack(value) { 101 | if (value === null || value === undefined) { 102 | this.appendAtom('nil'); 103 | return; 104 | } 105 | 106 | if (typeof value === 'boolean') { 107 | this.appendAtom(value ? 'true' : 'false'); 108 | return; 109 | } 110 | 111 | if (typeof value === 'number') { 112 | if ((value | 0) === value) { 113 | if (value > -128 && value < 128) { 114 | this.write8(SMALL_INTEGER_EXT); 115 | this.write8(value); 116 | } else { 117 | this.write8(INTEGER_EXT); 118 | this.write32(value); 119 | } 120 | } else { 121 | this.write8(NEW_FLOAT_EXT); 122 | this.writeFloat(value); 123 | } 124 | return; 125 | } 126 | 127 | if (typeof value === 'bigint') { 128 | this.write8(LARGE_BIG_EXT); 129 | 130 | const byteCountIndex = this.offset; 131 | this.offset += 4; 132 | 133 | this.write8(value < 0n ? 1 : 0); 134 | 135 | let ull = value < 0n ? -value : value; 136 | let byteCount = 0; 137 | while (ull > 0) { 138 | byteCount += 1; 139 | this.write8(Number(ull & 0xFFn)); 140 | ull >>= 8n; 141 | } 142 | 143 | this.view.setUint32(byteCountIndex, byteCount); 144 | return; 145 | } 146 | 147 | if (typeof value === 'string') { 148 | this.write8(BINARY_EXT); 149 | const a = textEncoder.encode(value); 150 | this.write32(a.length); 151 | this.write(a); 152 | return; 153 | } 154 | 155 | if (value instanceof Tuple) { 156 | this.packTuple(value); 157 | return; 158 | } 159 | 160 | if (Array.isArray(value)) { 161 | if (value.length === 0) { 162 | this.write8(NIL_EXT); 163 | return; 164 | } 165 | 166 | this.write8(LIST_EXT); 167 | this.write32(value.length); 168 | 169 | value.forEach((v) => { 170 | this.pack(v); 171 | }); 172 | 173 | this.write8(NIL_EXT); 174 | return; 175 | } 176 | 177 | if (value instanceof ImproperList) { 178 | this.write8(LIST_EXT); 179 | this.write32(value.head.length); 180 | value.head.forEach((v) => { 181 | this.pack(v); 182 | }); 183 | this.pack(value.tail); 184 | return; 185 | } 186 | 187 | if (typeof value === 'symbol') { 188 | this.appendAtom(value.description); 189 | return; 190 | } 191 | 192 | if (value instanceof Reference) { 193 | this.write8(NEWER_REFERENCE_EXT); 194 | this.write16(value.id.length); 195 | this.appendAtom(value.node); 196 | this.write32(value.creation); 197 | value.id.forEach((v) => { 198 | this.write32(v); 199 | }); 200 | return; 201 | } 202 | 203 | if (value instanceof Pid) { 204 | this.write8(NEW_PID_EXT); 205 | this.appendAtom(value.node); 206 | this.write32(value.id); 207 | this.write32(value.serial); 208 | this.write32(value.creation); 209 | return; 210 | } 211 | 212 | if (value instanceof Map) { 213 | this.write8(MAP_EXT); 214 | this.write32(value.size); 215 | value.forEach((v, k) => { 216 | this.pack(k); 217 | this.pack(v); 218 | }); 219 | return; 220 | } 221 | 222 | if (typeof value === 'object') { 223 | this.write8(MAP_EXT); 224 | const properties = Object.keys(value); 225 | this.write32(properties.length); 226 | properties.forEach((p) => { 227 | this.pack(p); 228 | this.pack(value[p]); 229 | }); 230 | return; 231 | } 232 | 233 | throw new Error('could not pack value'); 234 | } 235 | 236 | packTuple(array) { 237 | if (!Array.isArray(array)) { 238 | throw new Error('could not pack value'); 239 | } 240 | 241 | if (array.length > 255) { 242 | this.write8(LARGE_TUPLE_EXT); 243 | this.write32(array.length); 244 | } else { 245 | this.write8(SMALL_TUPLE_EXT); 246 | this.write8(array.length); 247 | } 248 | 249 | array.forEach((v) => { 250 | this.pack(v); 251 | }); 252 | } 253 | } 254 | 255 | module.exports = Encoder; 256 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Encoder = require('./encoder'); 4 | const Decoder = require('./decoder'); 5 | const { tuple, Reference, Pid, ImproperList } = require('./special'); 6 | 7 | module.exports = { 8 | pack: (v) => { 9 | const encoder = new Encoder(); 10 | encoder.pack(v); 11 | return encoder.buffer.subarray(0, encoder.offset); 12 | }, 13 | packTuple: (v) => { 14 | const encoder = new Encoder(); 15 | encoder.packTuple(v); 16 | return encoder.buffer.subarray(0, encoder.offset); 17 | }, 18 | unpack: ( 19 | v, 20 | { 21 | bigintToString = false, 22 | atomToString = true, 23 | mapToObject = true, 24 | returnSize = false, 25 | } = {}, 26 | ) => { 27 | const decoder = new Decoder(v, { bigintToString, atomToString, mapToObject }); 28 | const value = decoder.unpack(); 29 | if (returnSize) { 30 | return { value, size: decoder.offset }; 31 | } 32 | return value; 33 | }, 34 | tuple, 35 | Reference, 36 | Pid, 37 | ImproperList, 38 | }; 39 | -------------------------------------------------------------------------------- /src/special.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class Tuple extends Array {} 4 | 5 | function tuple(...items) { 6 | return Reflect.construct(Tuple, items); 7 | } 8 | 9 | class Pid { 10 | constructor(node, id, serial, creation) { 11 | this.node = node; 12 | this.id = id; 13 | this.serial = serial; 14 | this.creation = creation; 15 | } 16 | 17 | eq(other) { 18 | return other instanceof Pid 19 | && other.node === this.node 20 | && other.id === this.id 21 | && other.serial === this.serial 22 | && other.creation === this.creation; 23 | } 24 | 25 | [Symbol.for('nodejs.util.inspect.custom')]() { 26 | return `#Pid<${this.node}.${this.id}.${this.serial}.${this.creation}>`; 27 | } 28 | } 29 | 30 | class Reference { 31 | constructor(node, creation, id) { 32 | this.node = node; 33 | this.creation = creation; 34 | this.id = id; 35 | } 36 | 37 | eq(other) { 38 | return other instanceof Reference 39 | && other.node === this.node 40 | && other.creation === this.creation 41 | && `${other.id}` === `${this.id}`; 42 | } 43 | 44 | [Symbol.for('nodejs.util.inspect.custom')]() { 45 | return `#Ref<${this.node} ${this.creation} 0x${Buffer.from(this.id).toString('hex')}>`; 46 | } 47 | } 48 | 49 | class ImproperList { 50 | constructor(head, tail) { 51 | this.head = head; 52 | this.tail = tail; 53 | } 54 | } 55 | 56 | module.exports = { 57 | Tuple, 58 | tuple, 59 | Reference, 60 | Pid, 61 | ImproperList, 62 | }; 63 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { 4 | random, 5 | undefined: fuzzyUndefined, 6 | function: fuzzyFunction, 7 | symbol, error, 8 | date, 9 | regexp, 10 | typedArray, 11 | map, set, weakMap, weakSet, 12 | arrayBuffer, json, 13 | promise, 14 | proxy, 15 | bigint, 16 | } = require('@devsnek/fuzzy'); 17 | const { deepStrictEqual } = require('assert'); 18 | const erlpack = require('erlpack'); 19 | const { 20 | FORMAT_VERSION, 21 | STRING_EXT, 22 | SMALL_BIG_EXT, 23 | SMALL_INTEGER_EXT, 24 | SMALL_TUPLE_EXT, 25 | LARGE_TUPLE_EXT, 26 | SMALL_ATOM_EXT, 27 | SMALL_ATOM_UTF8_EXT, 28 | } = require('./src/constants'); 29 | const earl = require('.'); 30 | 31 | const value = () => random({ 32 | exclude: [ 33 | fuzzyUndefined, 34 | fuzzyFunction, symbol, error, 35 | date, 36 | regexp, 37 | typedArray, 38 | map, set, weakMap, weakSet, 39 | arrayBuffer, json, 40 | promise, 41 | proxy, 42 | bigint, 43 | ], 44 | }); 45 | 46 | [ 47 | 0n, 48 | 1n, 49 | -1n, 50 | 1n << 32n, 51 | 1n << 64n, 52 | 1n << 128n, 53 | -(1n << 32n), 54 | -(1n << 64n), 55 | -(1n << 128n), 56 | -1, 57 | 0, 58 | 1, 59 | 2 ** 32, 60 | 2 ** 31, 61 | -(2 ** 32), 62 | -(2 ** 31), 63 | ].forEach((v) => { 64 | const packed = earl.pack(v); 65 | const unpacked = earl.unpack(packed); 66 | deepStrictEqual(unpacked, v); 67 | 68 | if (typeof v === 'bigint') { 69 | const unpackedS = earl.unpack(packed, { bigintToString: true }); 70 | deepStrictEqual(unpackedS, v.toString()); 71 | } 72 | }); 73 | 74 | [ 75 | [ 76 | [ 77 | STRING_EXT, 78 | 0x2, 0x0, 79 | 42, 80 | 43, 81 | ], 82 | [42, 43], 83 | ], 84 | [ 85 | [ 86 | SMALL_BIG_EXT, 87 | 1, 88 | 0, 1, 89 | ], 90 | 1, 91 | ], 92 | [ 93 | [ 94 | SMALL_BIG_EXT, 95 | 1, 96 | 1, 1, 97 | ], 98 | -1, 99 | ], 100 | [ 101 | [ 102 | SMALL_BIG_EXT, 103 | 5, 104 | 0, 1, 1, 1, 1, 1, 105 | ], 106 | 16843009, 107 | ], 108 | [ 109 | [ 110 | SMALL_BIG_EXT, 111 | 5, 112 | 1, 1, 1, 1, 1, 1, 113 | ], 114 | -16843009, 115 | ], 116 | [ 117 | [ 118 | SMALL_BIG_EXT, 119 | 10, 120 | 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 121 | ], 122 | 4740885567116192841985n, 123 | ], 124 | [ 125 | [ 126 | SMALL_TUPLE_EXT, 127 | 1, 128 | SMALL_INTEGER_EXT, 129 | 42, 130 | ], 131 | [42], 132 | ], 133 | [ 134 | [ 135 | LARGE_TUPLE_EXT, 136 | 0, 0, 0, 1, 137 | SMALL_INTEGER_EXT, 138 | 42, 139 | ], 140 | [42], 141 | ], 142 | ].forEach(([raw, v]) => { 143 | const packed = Buffer.from([FORMAT_VERSION, ...raw]); 144 | const unpacked = earl.unpack(packed); 145 | deepStrictEqual(unpacked, v); 146 | }); 147 | 148 | { 149 | const packed = earl.packTuple([1, 2, 3]); 150 | 151 | deepStrictEqual(packed, new Uint8Array([ 152 | FORMAT_VERSION, 153 | SMALL_TUPLE_EXT, 154 | 3, 155 | SMALL_INTEGER_EXT, 156 | 1, 157 | SMALL_INTEGER_EXT, 158 | 2, 159 | SMALL_INTEGER_EXT, 160 | 3, 161 | ])); 162 | 163 | deepStrictEqual(earl.unpack(packed), [1, 2, 3]); 164 | } 165 | 166 | { 167 | const packed = earl.pack(Symbol('hello')); 168 | 169 | deepStrictEqual(packed, new Uint8Array([ 170 | FORMAT_VERSION, 171 | SMALL_ATOM_EXT, 172 | 5, 173 | 104, 174 | 101, 175 | 108, 176 | 108, 177 | 111, 178 | ])); 179 | 180 | deepStrictEqual(earl.unpack(packed), 'hello'); 181 | } 182 | 183 | { 184 | const packed = earl.pack(Symbol('a🧪b')); 185 | 186 | deepStrictEqual(packed, new Uint8Array([ 187 | FORMAT_VERSION, 188 | SMALL_ATOM_UTF8_EXT, 189 | 6, 190 | 97, 191 | 240, 192 | 159, 193 | 167, 194 | 170, 195 | 98, 196 | ])); 197 | 198 | deepStrictEqual(earl.unpack(packed), 'a🧪b'); 199 | } 200 | 201 | // Fuzz testing 202 | for (let i = 0; i < 10000; i += 1) { 203 | const v = value(); 204 | { 205 | const packed = earl.pack(v); 206 | const unpacked = erlpack.unpack(packed); 207 | deepStrictEqual(unpacked, v); 208 | } 209 | { 210 | const packed = erlpack.pack(v); 211 | const unpacked = earl.unpack(packed); 212 | deepStrictEqual(unpacked, v); 213 | } 214 | } 215 | --------------------------------------------------------------------------------