├── .npmignore ├── .prettierignore ├── .gitignore ├── src ├── browser.js ├── index.js ├── reference.js ├── quadruple.js ├── void.js ├── float.js ├── double.js ├── types.js ├── errors.js ├── opaque.js ├── bool.js ├── hyper.js ├── unsigned-hyper.js ├── option.js ├── int.js ├── unsigned-int.js ├── var-opaque.js ├── array.js ├── string.js ├── var-array.js ├── struct.js ├── enum.js ├── large-int.js ├── .eslintrc.js ├── serialization │ ├── xdr-reader.js │ └── xdr-writer.js ├── bigint-encoder.js ├── union.js ├── config.js └── xdr-type.js ├── bower.json ├── Gemfile ├── .eslintrc.js ├── test ├── .eslintrc.js ├── setup.js └── unit │ ├── dynamic-buffer-resize_test.js │ ├── quadruple_test.js │ ├── struct_union_test.js │ ├── void_test.js │ ├── bool_test.js │ ├── option_test.js │ ├── opaque_test.js │ ├── float_test.js │ ├── double_test.js │ ├── unsigned-hyper_test.js │ ├── unsigned-int_test.js │ ├── hyper_test.js │ ├── var-opaque_test.js │ ├── int_test.js │ ├── var-array_test.js │ ├── array_test.js │ ├── enum_test.js │ ├── struct_test.js │ ├── string_test.js │ ├── union_test.js │ ├── define_test.js │ └── bigint-encoder_test.js ├── babel.config.json ├── prettier.config.js ├── CONTRIBUTING.md ├── examples ├── linked_list.js ├── typedef.js ├── enum.js ├── struct.js ├── union.js └── test.x ├── .travis.yml ├── .github ├── workflows │ ├── tests.yml │ ├── codeql.yml │ └── npm-publish.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── karma.conf.js ├── Gemfile.lock ├── webpack.config.js ├── package.json ├── README.md ├── CHANGELOG.md └── LICENSE.md /.npmignore: -------------------------------------------------------------------------------- 1 | tmp 2 | generated 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /package.json 2 | /node_modules 3 | /lib 4 | /dist 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | tmp/ 5 | generated/ 6 | .DS_Store 7 | .vscode 8 | -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line prefer-import/prefer-import-over-require 2 | const exports = require('./index'); 3 | module.exports = exports; 4 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-xdr", 3 | "version": "0.0.1", 4 | "private": false, 5 | "main": "dist/xdr.js", 6 | "dependencies": {}, 7 | "devDependencies": {}, 8 | "ignore": [] 9 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './config'; 3 | 4 | export { XdrReader } from './serialization/xdr-reader'; 5 | export { XdrWriter } from './serialization/xdr-writer'; 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | group :development do 4 | gem "xdrgen", git: "git@github.com:stellar/xdrgen.git" 5 | # gem "xdrgen", path: "../xdrgen" 6 | gem "pry" 7 | end 8 | 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | node: true 4 | }, 5 | extends: ['eslint:recommended', 'plugin:node/recommended'], 6 | rules: { 7 | 'node/no-unpublished-require': 0, 8 | indent: ['warn', 2] 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /test/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true 4 | }, 5 | globals: { 6 | XDR: true, 7 | chai: true, 8 | sinon: true, 9 | expect: true, 10 | stub: true, 11 | spy: true 12 | }, 13 | rules: { 14 | 'no-unused-vars': 0 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "browsers": [ 8 | "> 2%", 9 | "not ie 11", 10 | "not op_mini all" 11 | ] 12 | } 13 | } 14 | ] 15 | ] 16 | } -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | bracketSpacing: true, 4 | jsxBracketSameLine: false, 5 | printWidth: 80, 6 | proseWrap: 'always', 7 | semi: true, 8 | singleQuote: true, 9 | tabWidth: 2, 10 | trailingComma: 'none', 11 | useTabs: false 12 | }; 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Please read the [Contribution Guide](https://github.com/stellar/docs/blob/master/CONTRIBUTING.md). 4 | 5 | Then please [sign the Contributor License Agreement](https://docs.google.com/forms/d/1g7EF6PERciwn7zfmfke5Sir2n10yddGGSXyZsq98tVY/viewform?usp=send_form). 6 | 7 | -------------------------------------------------------------------------------- /src/reference.js: -------------------------------------------------------------------------------- 1 | import { XdrPrimitiveType } from './xdr-type'; 2 | import { XdrDefinitionError } from './errors'; 3 | 4 | export class Reference extends XdrPrimitiveType { 5 | /* jshint unused: false */ 6 | resolve() { 7 | throw new XdrDefinitionError( 8 | '"resolve" method should be implemented in the descendant class' 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/linked_list.js: -------------------------------------------------------------------------------- 1 | import * as XDR from '../src'; 2 | 3 | let xdr = XDR.config((xdr) => { 4 | xdr.struct('IntList', [ 5 | ['value', xdr.int()], 6 | ['rest', xdr.option(xdr.lookup('IntList'))] 7 | ]); 8 | }); 9 | 10 | let n1 = new xdr.IntList({ value: 1 }); 11 | let n2 = new xdr.IntList({ value: 3, rest: n1 }); 12 | 13 | console.log(n2.toXDR()); 14 | -------------------------------------------------------------------------------- /examples/typedef.js: -------------------------------------------------------------------------------- 1 | import * as XDR from '../src'; 2 | 3 | let xdr = XDR.config((xdr) => { 4 | xdr.struct('Signature', [ 5 | ['publicKey', xdr.opaque(32)], 6 | ['data', xdr.opaque(32)] 7 | ]); 8 | 9 | xdr.typedef('SignatureTypedef', xdr.lookup('Signature')); 10 | xdr.typedef('IntTypedef', xdr.int()); 11 | }); 12 | 13 | console.log(xdr.SignatureTypedef === xdr.Signature); 14 | console.log(xdr.IntTypedef === XDR.Int); 15 | -------------------------------------------------------------------------------- /src/quadruple.js: -------------------------------------------------------------------------------- 1 | import { XdrPrimitiveType } from './xdr-type'; 2 | import { XdrDefinitionError } from './errors'; 3 | 4 | export class Quadruple extends XdrPrimitiveType { 5 | static read() { 6 | throw new XdrDefinitionError('quadruple not supported'); 7 | } 8 | 9 | static write() { 10 | throw new XdrDefinitionError('quadruple not supported'); 11 | } 12 | 13 | static isValid() { 14 | return false; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/void.js: -------------------------------------------------------------------------------- 1 | import { XdrPrimitiveType } from './xdr-type'; 2 | import { XdrWriterError } from './errors'; 3 | 4 | export class Void extends XdrPrimitiveType { 5 | /* jshint unused: false */ 6 | 7 | static read() { 8 | return undefined; 9 | } 10 | 11 | static write(value) { 12 | if (value !== undefined) 13 | throw new XdrWriterError('trying to write value to a void slot'); 14 | } 15 | 16 | static isValid(value) { 17 | return value === undefined; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/float.js: -------------------------------------------------------------------------------- 1 | import { XdrPrimitiveType } from './xdr-type'; 2 | import { XdrWriterError } from './errors'; 3 | 4 | export class Float extends XdrPrimitiveType { 5 | /** 6 | * @inheritDoc 7 | */ 8 | static read(reader) { 9 | return reader.readFloatBE(); 10 | } 11 | 12 | /** 13 | * @inheritDoc 14 | */ 15 | static write(value, writer) { 16 | if (typeof value !== 'number') throw new XdrWriterError('not a number'); 17 | 18 | writer.writeFloatBE(value); 19 | } 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | static isValid(value) { 25 | return typeof value === 'number'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/double.js: -------------------------------------------------------------------------------- 1 | import { XdrPrimitiveType } from './xdr-type'; 2 | import { XdrWriterError } from './errors'; 3 | 4 | export class Double extends XdrPrimitiveType { 5 | /** 6 | * @inheritDoc 7 | */ 8 | static read(reader) { 9 | return reader.readDoubleBE(); 10 | } 11 | 12 | /** 13 | * @inheritDoc 14 | */ 15 | static write(value, writer) { 16 | if (typeof value !== 'number') throw new XdrWriterError('not a number'); 17 | 18 | writer.writeDoubleBE(value); 19 | } 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | static isValid(value) { 25 | return typeof value === 'number'; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | export * from './int'; 2 | export * from './hyper'; 3 | export * from './unsigned-int'; 4 | export * from './unsigned-hyper'; 5 | export * from './large-int'; 6 | 7 | export * from './float'; 8 | export * from './double'; 9 | export * from './quadruple'; 10 | 11 | export * from './bool'; 12 | 13 | export * from './string'; 14 | 15 | export * from './opaque'; 16 | export * from './var-opaque'; 17 | 18 | export * from './array'; 19 | export * from './var-array'; 20 | 21 | export * from './option'; 22 | export * from './void'; 23 | 24 | export * from './enum'; 25 | export * from './struct'; 26 | export * from './union'; 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 10 4 | - 11 5 | - 12 6 | cache: 7 | directories: 8 | - node_modules 9 | services: 10 | - xvfb 11 | script: yarn test 12 | notifications: 13 | slack: 14 | secure: Bu3OZ0Cdy8eq0IMoLJmadDba8Yyb9ajobKocfp8V7/vfOjpIVXdFrGMqfckkZ22uiWxHCsYEX1ahQ77zTjbO3tNq1CTmSgEAWaqqMVz1iIVNhSoeHRfYDa9r1sKFpJv1KEz+j/8i2phcR5MDE6cGK+byJmjfjcnkP1XoNiupuck= 15 | before_deploy: 16 | - yarn build 17 | deploy: 18 | provider: npm 19 | email: npm@stellar.org 20 | api_key: $NPM_TOKEN 21 | skip_cleanup: true 22 | on: 23 | tags: true 24 | repo: stellar/js-xdr 25 | branch: master 26 | -------------------------------------------------------------------------------- /examples/enum.js: -------------------------------------------------------------------------------- 1 | import * as XDR from '../src'; 2 | 3 | let xdr = XDR.config((xdr) => { 4 | xdr.enum('Color', { 5 | red: 0, 6 | green: 1, 7 | blue: 2 8 | }); 9 | 10 | xdr.enum('ResultType', { 11 | ok: 0, 12 | error: 1 13 | }); 14 | }); 15 | 16 | console.log(xdr); 17 | 18 | // 19 | console.log(xdr.Color.members()); // { red: 0, green: 1, blue: 2, } 20 | 21 | console.log(xdr.Color.fromName('red')); 22 | 23 | console.log(xdr.Color.fromXDR(Buffer.from([0, 0, 0, 0]))); // Color.red 24 | console.log(xdr.Color.red().toXDR()); // Buffer 25 | console.log(xdr.Color.red().toXDR('hex')); // 26 | 27 | console.log(xdr.Color.red() !== xdr.ResultType.ok()); 28 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Use Node.js 18 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 18 19 | 20 | - name: Install Dependencies 21 | run: yarn 22 | 23 | - name: Build All 24 | run: yarn build 25 | 26 | - name: Run Tests 27 | run: yarn test 28 | 29 | - name: Check Code Formatting 30 | run: yarn fmt && (git diff-index --quiet HEAD; git diff) 31 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | export class XdrWriterError extends TypeError { 2 | constructor(message) { 3 | super(`XDR Write Error: ${message}`); 4 | } 5 | } 6 | 7 | export class XdrReaderError extends TypeError { 8 | constructor(message) { 9 | super(`XDR Read Error: ${message}`); 10 | } 11 | } 12 | 13 | export class XdrDefinitionError extends TypeError { 14 | constructor(message) { 15 | super(`XDR Type Definition Error: ${message}`); 16 | } 17 | } 18 | 19 | export class XdrNotImplementedDefinitionError extends XdrDefinitionError { 20 | constructor() { 21 | super( 22 | `method not implemented, it should be overloaded in the descendant class.` 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | if (typeof global === 'undefined') { 2 | // eslint-disable-next-line no-undef 3 | window.global = window; 4 | } 5 | global['XDR'] = require('../src'); 6 | global.chai = require('chai'); 7 | global.sinon = require('sinon'); 8 | global.chai.use(require('sinon-chai')); 9 | 10 | global.expect = global.chai.expect; 11 | 12 | exports.mochaHooks = { 13 | beforeEach: function () { 14 | this.sandbox = global.sinon.createSandbox(); 15 | global.stub = this.sandbox.stub.bind(this.sandbox); 16 | global.spy = this.sandbox.spy.bind(this.sandbox); 17 | }, 18 | afterEach: function () { 19 | delete global.stub; 20 | delete global.spy; 21 | this.sandbox.restore(); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **What version are you on?** 14 | Check `yarn.lock` or `package-lock.json` to find out precisely what version of 'js-xdr' you're running. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behavior: 18 | 1. Go to '...' 19 | 2. Click on '....' 20 | 3. Scroll down to '....' 21 | 4. See error 22 | 23 | **Expected behavior** 24 | A clear and concise description of what you expected to happen. 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /test/unit/dynamic-buffer-resize_test.js: -------------------------------------------------------------------------------- 1 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 2 | 3 | describe('Dynamic writer buffer resize', function () { 4 | it('automatically resize buffer', function () { 5 | const str = new XDR.String(32768); 6 | let io = new XdrWriter(12); 7 | str.write('7 bytes', io); 8 | // expect buffer size to equal base size 9 | expect(io._buffer.length).to.eql(12); 10 | str.write('a'.repeat(32768), io); 11 | // expect buffer growth up to 5 chunks 12 | expect(io._buffer.length).to.eql(40960); 13 | // increase by 1 more 8 KB chunk 14 | str.write('a'.repeat(9000), io); 15 | expect(io._buffer.length).to.eql(49152); 16 | // check final buffer size 17 | expect(io.toArray().length).to.eql(41788); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /examples/struct.js: -------------------------------------------------------------------------------- 1 | import * as XDR from '../src'; 2 | 3 | let xdr = XDR.config((xdr) => { 4 | xdr.struct('Signature', [ 5 | ['publicKey', xdr.opaque(32)], 6 | ['data', xdr.opaque(32)] 7 | ]); 8 | 9 | xdr.struct('Envelope', [ 10 | ['body', xdr.varOpaque(1000)], 11 | ['timestamp', xdr.uint()], 12 | ['signature', xdr.lookup('Signature')] 13 | ]); 14 | }); 15 | 16 | let sig = new xdr.Signature(); 17 | sig.publicKey(Buffer.alloc(32)); 18 | sig.data(Buffer.from('00000000000000000000000000000000')); 19 | 20 | let env = new xdr.Envelope({ 21 | signature: sig, 22 | body: Buffer.from('hello'), 23 | timestamp: Math.floor(new Date() / 1000) 24 | }); 25 | 26 | let output = env.toXDR(); 27 | let parsed = xdr.Envelope.fromXDR(output); 28 | 29 | console.log(env); 30 | console.log(parsed); 31 | -------------------------------------------------------------------------------- /src/opaque.js: -------------------------------------------------------------------------------- 1 | import { XdrCompositeType } from './xdr-type'; 2 | import { XdrWriterError } from './errors'; 3 | 4 | export class Opaque extends XdrCompositeType { 5 | constructor(length) { 6 | super(); 7 | this._length = length; 8 | } 9 | 10 | /** 11 | * @inheritDoc 12 | */ 13 | read(reader) { 14 | return reader.read(this._length); 15 | } 16 | 17 | /** 18 | * @inheritDoc 19 | */ 20 | write(value, writer) { 21 | const { length } = value; 22 | if (length !== this._length) 23 | throw new XdrWriterError( 24 | `got ${value.length} bytes, expected ${this._length}` 25 | ); 26 | writer.write(value, length); 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | isValid(value) { 33 | return Buffer.isBuffer(value) && value.length === this._length; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/bool.js: -------------------------------------------------------------------------------- 1 | import { Int } from './int'; 2 | import { XdrPrimitiveType } from './xdr-type'; 3 | import { XdrReaderError } from './errors'; 4 | 5 | export class Bool extends XdrPrimitiveType { 6 | /** 7 | * @inheritDoc 8 | */ 9 | static read(reader) { 10 | const value = Int.read(reader); 11 | 12 | switch (value) { 13 | case 0: 14 | return false; 15 | case 1: 16 | return true; 17 | default: 18 | throw new XdrReaderError(`got ${value} when trying to read a bool`); 19 | } 20 | } 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | static write(value, writer) { 26 | const intVal = value ? 1 : 0; 27 | Int.write(intVal, writer); 28 | } 29 | 30 | /** 31 | * @inheritDoc 32 | */ 33 | static isValid(value) { 34 | return typeof value === 'boolean'; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/hyper.js: -------------------------------------------------------------------------------- 1 | import { LargeInt } from './large-int'; 2 | 3 | export class Hyper extends LargeInt { 4 | /** 5 | * @param {Array} parts - Slices to encode 6 | */ 7 | constructor(...args) { 8 | super(args); 9 | } 10 | 11 | get low() { 12 | return Number(this._value & 0xffffffffn) << 0; 13 | } 14 | 15 | get high() { 16 | return Number(this._value >> 32n) >> 0; 17 | } 18 | 19 | get size() { 20 | return 64; 21 | } 22 | 23 | get unsigned() { 24 | return false; 25 | } 26 | 27 | /** 28 | * Create Hyper instance from two [high][low] i32 values 29 | * @param {Number} low - Low part of i64 number 30 | * @param {Number} high - High part of i64 number 31 | * @return {LargeInt} 32 | */ 33 | static fromBits(low, high) { 34 | return new this(low, high); 35 | } 36 | } 37 | 38 | Hyper.defineIntBoundaries(); 39 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | frameworks: ['mocha', 'webpack', 'sinon-chai'], 6 | browsers: ['FirefoxHeadless', 'ChromeHeadless'], 7 | browserNoActivityTimeout: 20000, 8 | 9 | files: ['dist/xdr.js', 'test/unit/**/*.js'], 10 | 11 | preprocessors: { 12 | 'test/unit/**/*.js': ['webpack'] 13 | }, 14 | 15 | webpack: { 16 | mode: 'development', 17 | module: { 18 | rules: [ 19 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' } 20 | ] 21 | }, 22 | plugins: [ 23 | new webpack.ProvidePlugin({ 24 | Buffer: ['buffer', 'Buffer'] 25 | }) 26 | ] 27 | }, 28 | 29 | webpackMiddleware: { 30 | noInfo: true 31 | }, 32 | 33 | singleRun: true, 34 | 35 | reporters: ['dots'] 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /examples/union.js: -------------------------------------------------------------------------------- 1 | import * as XDR from '../src'; 2 | 3 | let xdr = XDR.config((xdr) => { 4 | xdr.union('Result', { 5 | switchOn: xdr.lookup('ResultType'), 6 | switches: [ 7 | ['ok', xdr.void()], 8 | ['error', 'message'] 9 | ], 10 | // defaultArm: xdr.void(), 11 | arms: { 12 | message: xdr.string(100) 13 | } 14 | }); 15 | 16 | xdr.enum('ResultType', { 17 | ok: 0, 18 | error: 1, 19 | nonsense: 2 20 | }); 21 | }); 22 | 23 | let r = xdr.Result.ok(); 24 | r.set('error', 'this is an error'); 25 | r.message(); // => "this is an error" 26 | r.get('message'); // => "this is an error" 27 | 28 | r.set(xdr.ResultType.ok()); 29 | r.get(); // => undefined 30 | 31 | // r.set("nonsense"); 32 | r.get(); // => undefined 33 | 34 | let output = r.toXDR(); 35 | let parsed = xdr.Result.fromXDR(output); 36 | 37 | console.log(r); 38 | console.log(r.arm()); 39 | console.log(parsed); 40 | -------------------------------------------------------------------------------- /src/unsigned-hyper.js: -------------------------------------------------------------------------------- 1 | import { LargeInt } from './large-int'; 2 | 3 | export class UnsignedHyper extends LargeInt { 4 | /** 5 | * @param {Array} parts - Slices to encode 6 | */ 7 | constructor(...args) { 8 | super(args); 9 | } 10 | 11 | get low() { 12 | return Number(this._value & 0xffffffffn) << 0; 13 | } 14 | 15 | get high() { 16 | return Number(this._value >> 32n) >> 0; 17 | } 18 | 19 | get size() { 20 | return 64; 21 | } 22 | 23 | get unsigned() { 24 | return true; 25 | } 26 | 27 | /** 28 | * Create UnsignedHyper instance from two [high][low] i32 values 29 | * @param {Number} low - Low part of u64 number 30 | * @param {Number} high - High part of u64 number 31 | * @return {UnsignedHyper} 32 | */ 33 | static fromBits(low, high) { 34 | return new this(low, high); 35 | } 36 | } 37 | 38 | UnsignedHyper.defineIntBoundaries(); 39 | -------------------------------------------------------------------------------- /src/option.js: -------------------------------------------------------------------------------- 1 | import { Bool } from './bool'; 2 | import { XdrPrimitiveType } from './xdr-type'; 3 | 4 | export class Option extends XdrPrimitiveType { 5 | constructor(childType) { 6 | super(); 7 | this._childType = childType; 8 | } 9 | 10 | /** 11 | * @inheritDoc 12 | */ 13 | read(reader) { 14 | if (Bool.read(reader)) { 15 | return this._childType.read(reader); 16 | } 17 | 18 | return undefined; 19 | } 20 | 21 | /** 22 | * @inheritDoc 23 | */ 24 | write(value, writer) { 25 | const isPresent = value !== null && value !== undefined; 26 | 27 | Bool.write(isPresent, writer); 28 | 29 | if (isPresent) { 30 | this._childType.write(value, writer); 31 | } 32 | } 33 | 34 | /** 35 | * @inheritDoc 36 | */ 37 | isValid(value) { 38 | if (value === null || value === undefined) { 39 | return true; 40 | } 41 | return this._childType.isValid(value); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/int.js: -------------------------------------------------------------------------------- 1 | import { XdrPrimitiveType } from './xdr-type'; 2 | import { XdrWriterError } from './errors'; 3 | 4 | const MAX_VALUE = 2147483647; 5 | const MIN_VALUE = -2147483648; 6 | 7 | export class Int extends XdrPrimitiveType { 8 | /** 9 | * @inheritDoc 10 | */ 11 | static read(reader) { 12 | return reader.readInt32BE(); 13 | } 14 | 15 | /** 16 | * @inheritDoc 17 | */ 18 | static write(value, writer) { 19 | if (typeof value !== 'number') throw new XdrWriterError('not a number'); 20 | 21 | if ((value | 0) !== value) throw new XdrWriterError('invalid i32 value'); 22 | 23 | writer.writeInt32BE(value); 24 | } 25 | 26 | /** 27 | * @inheritDoc 28 | */ 29 | static isValid(value) { 30 | if (typeof value !== 'number' || (value | 0) !== value) { 31 | return false; 32 | } 33 | 34 | return value >= MIN_VALUE && value <= MAX_VALUE; 35 | } 36 | } 37 | 38 | Int.MAX_VALUE = MAX_VALUE; 39 | Int.MIN_VALUE = -MIN_VALUE; 40 | -------------------------------------------------------------------------------- /src/unsigned-int.js: -------------------------------------------------------------------------------- 1 | import { XdrPrimitiveType } from './xdr-type'; 2 | import { XdrWriterError } from './errors'; 3 | 4 | const MAX_VALUE = 4294967295; 5 | const MIN_VALUE = 0; 6 | 7 | export class UnsignedInt extends XdrPrimitiveType { 8 | /** 9 | * @inheritDoc 10 | */ 11 | static read(reader) { 12 | return reader.readUInt32BE(); 13 | } 14 | 15 | /** 16 | * @inheritDoc 17 | */ 18 | static write(value, writer) { 19 | if ( 20 | typeof value !== 'number' || 21 | !(value >= MIN_VALUE && value <= MAX_VALUE) || 22 | value % 1 !== 0 23 | ) 24 | throw new XdrWriterError('invalid u32 value'); 25 | 26 | writer.writeUInt32BE(value); 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | static isValid(value) { 33 | if (typeof value !== 'number' || value % 1 !== 0) { 34 | return false; 35 | } 36 | 37 | return value >= MIN_VALUE && value <= MAX_VALUE; 38 | } 39 | } 40 | 41 | UnsignedInt.MAX_VALUE = MAX_VALUE; 42 | UnsignedInt.MIN_VALUE = MIN_VALUE; 43 | -------------------------------------------------------------------------------- /test/unit/quadruple_test.js: -------------------------------------------------------------------------------- 1 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 2 | import { XdrReader } from '../../src/serialization/xdr-reader'; 3 | 4 | const Quadruple = XDR.Quadruple; 5 | 6 | describe('Quadruple.read', function () { 7 | it('is not supported', function () { 8 | expect(() => read([0x00, 0x00, 0x00, 0x00])).to.throw( 9 | /Type Definition Error/i 10 | ); 11 | }); 12 | 13 | function read(bytes) { 14 | let io = new XdrReader(bytes); 15 | return Quadruple.read(io); 16 | } 17 | }); 18 | 19 | describe('Quadruple.write', function () { 20 | it('is not supported', function () { 21 | expect(() => write(0.0)).to.throw(/Type Definition Error/i); 22 | }); 23 | 24 | function write(value) { 25 | let io = new XdrWriter(8); 26 | Quadruple.write(value, io); 27 | return io.toArray(); 28 | } 29 | }); 30 | 31 | describe('Quadruple.isValid', function () { 32 | it('returns false', function () { 33 | expect(Quadruple.isValid(1.0)).to.be.false; 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GIT 2 | remote: git@github.com:stellar/xdrgen.git 3 | revision: 80e38ef2a96489f6b501d4db3a350406e5aa3bab 4 | specs: 5 | xdrgen (0.1.1) 6 | activesupport (~> 6) 7 | memoist (~> 0.11.0) 8 | slop (~> 3.4) 9 | treetop (~> 1.5.3) 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | activesupport (6.1.7.6) 15 | concurrent-ruby (~> 1.0, >= 1.0.2) 16 | i18n (>= 1.6, < 2) 17 | minitest (>= 5.1) 18 | tzinfo (~> 2.0) 19 | zeitwerk (~> 2.3) 20 | coderay (1.1.3) 21 | concurrent-ruby (1.2.2) 22 | i18n (1.14.1) 23 | concurrent-ruby (~> 1.0) 24 | memoist (0.11.0) 25 | method_source (1.0.0) 26 | minitest (5.19.0) 27 | polyglot (0.3.5) 28 | pry (0.14.2) 29 | coderay (~> 1.1) 30 | method_source (~> 1.0) 31 | slop (3.6.0) 32 | treetop (1.5.3) 33 | polyglot (~> 0.3) 34 | tzinfo (2.0.6) 35 | concurrent-ruby (~> 1.0) 36 | zeitwerk (2.6.11) 37 | 38 | PLATFORMS 39 | ruby 40 | 41 | DEPENDENCIES 42 | pry 43 | xdrgen! 44 | 45 | BUNDLED WITH 46 | 2.4.6 47 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: '35 22 * * 5' 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze (${{ matrix.language }}) 14 | runs-on: ubuntu-latest 15 | timeout-minutes: 360 16 | permissions: 17 | # required for all workflows 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | include: 24 | - language: javascript-typescript 25 | build-mode: none 26 | - language: ruby 27 | build-mode: none 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v4 32 | 33 | # Initializes the CodeQL tools for scanning. 34 | - name: Initialize CodeQL 35 | uses: github/codeql-action/init@v3 36 | with: 37 | languages: ${{ matrix.language }} 38 | build-mode: ${{ matrix.build-mode }} 39 | 40 | - name: Perform CodeQL Analysis 41 | uses: github/codeql-action/analyze@v3 42 | with: 43 | category: "/language:${{matrix.language}}" 44 | -------------------------------------------------------------------------------- /src/var-opaque.js: -------------------------------------------------------------------------------- 1 | import { UnsignedInt } from './unsigned-int'; 2 | import { XdrCompositeType } from './xdr-type'; 3 | import { XdrReaderError, XdrWriterError } from './errors'; 4 | 5 | export class VarOpaque extends XdrCompositeType { 6 | constructor(maxLength = UnsignedInt.MAX_VALUE) { 7 | super(); 8 | this._maxLength = maxLength; 9 | } 10 | 11 | /** 12 | * @inheritDoc 13 | */ 14 | read(reader) { 15 | const size = UnsignedInt.read(reader); 16 | if (size > this._maxLength) 17 | throw new XdrReaderError( 18 | `saw ${size} length VarOpaque, max allowed is ${this._maxLength}` 19 | ); 20 | return reader.read(size); 21 | } 22 | 23 | /** 24 | * @inheritDoc 25 | */ 26 | write(value, writer) { 27 | const { length } = value; 28 | if (value.length > this._maxLength) 29 | throw new XdrWriterError( 30 | `got ${value.length} bytes, max allowed is ${this._maxLength}` 31 | ); 32 | // write size info 33 | UnsignedInt.write(length, writer); 34 | writer.write(value, length); 35 | } 36 | 37 | /** 38 | * @inheritDoc 39 | */ 40 | isValid(value) { 41 | return Buffer.isBuffer(value) && value.length <= this._maxLength; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: npm publish 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | 13 | - name: Install Node 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: '18.x' 17 | registry-url: 'https://registry.npmjs.org' 18 | always-auth: true 19 | 20 | - name: Install Dependencies 21 | run: yarn 22 | 23 | - name: Build & Test 24 | run: yarn build && yarn test 25 | 26 | - name: Publish npm packages 27 | run: | 28 | yarn publish --access public 29 | sed -i -e 's#"@stellar/js-xdr"#"js-xdr"#' package.json 30 | yarn publish 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | 34 | - name: Publish npm package under old scope 35 | run: | 36 | V=$(cat package.json | jq '.version' | sed -e 's/\"//g') 37 | echo "Deprecating js-xdr@$V" 38 | npm deprecate js-xdr@"<= $V" "⚠️ This package has moved to @stellar/js-xdr! 🚚" 39 | env: 40 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 41 | -------------------------------------------------------------------------------- /test/unit/struct_union_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | 3 | /* jshint -W030 */ 4 | 5 | let emptyContext = { definitions: {}, results: {} }; 6 | 7 | let Ext = XDR.Union.create(emptyContext, 'Ext', { 8 | switchOn: XDR.Int, 9 | switches: [ 10 | [0, XDR.Void], 11 | [1, XDR.Int] 12 | ] 13 | }); 14 | 15 | let StructUnion = XDR.Struct.create(emptyContext, 'StructUnion', [ 16 | ['id', XDR.Int], 17 | ['ext', Ext] 18 | ]); 19 | 20 | describe('StructUnion.read', function () { 21 | it('decodes correctly', function () { 22 | let empty = read([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]); 23 | expect(empty).to.be.instanceof(StructUnion); 24 | expect(empty.id()).to.eql(1); 25 | expect(empty.ext().switch()).to.eql(0); 26 | expect(empty.ext().arm()).to.eql(XDR.Void); 27 | 28 | let filled = read([ 29 | 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02 30 | ]); 31 | 32 | expect(filled).to.be.instanceof(StructUnion); 33 | expect(filled.id()).to.eql(2); 34 | expect(filled.ext().switch()).to.eql(1); 35 | expect(filled.ext().arm()).to.eql(XDR.Int); 36 | expect(filled.ext().value()).to.eql(2); 37 | }); 38 | 39 | function read(bytes) { 40 | let io = new XdrReader(bytes); 41 | return StructUnion.read(io); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /src/array.js: -------------------------------------------------------------------------------- 1 | import { XdrCompositeType } from './xdr-type'; 2 | import { XdrWriterError } from './errors'; 3 | 4 | export class Array extends XdrCompositeType { 5 | constructor(childType, length) { 6 | super(); 7 | this._childType = childType; 8 | this._length = length; 9 | } 10 | 11 | /** 12 | * @inheritDoc 13 | */ 14 | read(reader) { 15 | // allocate array of specified length 16 | const result = new global.Array(this._length); 17 | // read values 18 | for (let i = 0; i < this._length; i++) { 19 | result[i] = this._childType.read(reader); 20 | } 21 | return result; 22 | } 23 | 24 | /** 25 | * @inheritDoc 26 | */ 27 | write(value, writer) { 28 | if (!global.Array.isArray(value)) 29 | throw new XdrWriterError(`value is not array`); 30 | 31 | if (value.length !== this._length) 32 | throw new XdrWriterError( 33 | `got array of size ${value.length}, expected ${this._length}` 34 | ); 35 | 36 | for (const child of value) { 37 | this._childType.write(child, writer); 38 | } 39 | } 40 | 41 | /** 42 | * @inheritDoc 43 | */ 44 | isValid(value) { 45 | if (!(value instanceof global.Array) || value.length !== this._length) { 46 | return false; 47 | } 48 | 49 | for (const child of value) { 50 | if (!this._childType.isValid(child)) return false; 51 | } 52 | return true; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/unit/void_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 3 | 4 | let subject = XDR.Void; 5 | 6 | describe('Void#read', function () { 7 | it('decodes correctly', function () { 8 | expect(read([0x00, 0x00, 0x00, 0x00])).to.be.undefined; 9 | expect(read([0x00, 0x00, 0x00, 0x01])).to.be.undefined; 10 | expect(read([0x00, 0x00, 0x00, 0x02])).to.be.undefined; 11 | }); 12 | 13 | function read(bytes) { 14 | let io = new XdrReader(bytes); 15 | return subject.read(io); 16 | } 17 | }); 18 | 19 | describe('Void#write', function () { 20 | it('encodes correctly', function () { 21 | expect(write(undefined)).to.eql([]); 22 | }); 23 | 24 | function write(value) { 25 | let io = new XdrWriter(8); 26 | subject.write(value, io); 27 | return io.toArray(); 28 | } 29 | }); 30 | 31 | describe('Void#isValid', function () { 32 | it('returns true undefined', function () { 33 | expect(subject.isValid(undefined)).to.be.true; 34 | }); 35 | 36 | it('returns false for anything defined', function () { 37 | expect(subject.isValid(null)).to.be.false; 38 | expect(subject.isValid(false)).to.be.false; 39 | expect(subject.isValid(1)).to.be.false; 40 | expect(subject.isValid('aaa')).to.be.false; 41 | expect(subject.isValid({})).to.be.false; 42 | expect(subject.isValid([undefined])).to.be.false; 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | 5 | const browserBuild = !process.argv.includes('--mode=development'); 6 | 7 | module.exports = function () { 8 | const mode = browserBuild ? 'production' : 'development'; 9 | const config = { 10 | mode, 11 | devtool: 'source-map', 12 | entry: { 13 | xdr: [path.join(__dirname, '/src/browser.js')] 14 | }, 15 | output: { 16 | path: path.join(__dirname, browserBuild ? './dist' : './lib'), 17 | filename: '[name].js', 18 | library: { 19 | name: 'XDR', 20 | type: 'umd' 21 | }, 22 | globalObject: 'this' 23 | }, 24 | module: { 25 | rules: [ 26 | { 27 | test: /\.js$/, 28 | loader: 'babel-loader', 29 | exclude: /node_modules/ 30 | } 31 | ] 32 | }, 33 | plugins: [ 34 | new webpack.DefinePlugin({ 35 | 'process.env.NODE_ENV': JSON.stringify(mode) 36 | }) 37 | ] 38 | }; 39 | if (browserBuild) { 40 | config.optimization = { 41 | minimize: true, 42 | minimizer: [ 43 | new TerserPlugin({ 44 | parallel: true 45 | }) 46 | ] 47 | }; 48 | config.plugins.push( 49 | new webpack.ProvidePlugin({ 50 | Buffer: ['buffer', 'Buffer'] 51 | }) 52 | ); 53 | } else { 54 | config.target = 'node'; 55 | } 56 | return config; 57 | }; 58 | -------------------------------------------------------------------------------- /test/unit/bool_test.js: -------------------------------------------------------------------------------- 1 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 2 | import { XdrReader } from '../../src/serialization/xdr-reader'; 3 | let Bool = XDR.Bool; 4 | 5 | describe('Bool.read', function () { 6 | it('decodes correctly', function () { 7 | expect(read([0, 0, 0, 0])).to.eql(false); 8 | expect(read([0, 0, 0, 1])).to.eql(true); 9 | 10 | expect(() => read([0, 0, 0, 2])).to.throw(/read error/i); 11 | expect(() => read([255, 255, 255, 255])).to.throw(/read error/i); 12 | }); 13 | 14 | function read(bytes) { 15 | let io = new XdrReader(bytes); 16 | return Bool.read(io); 17 | } 18 | }); 19 | 20 | describe('Bool.write', function () { 21 | it('encodes correctly', function () { 22 | expect(write(false)).to.eql([0, 0, 0, 0]); 23 | expect(write(true)).to.eql([0, 0, 0, 1]); 24 | }); 25 | 26 | function write(value) { 27 | let io = new XdrWriter(8); 28 | Bool.write(value, io); 29 | return io.toArray(); 30 | } 31 | }); 32 | 33 | describe('Bool.isValid', function () { 34 | it('returns true for booleans', function () { 35 | expect(Bool.isValid(true)).to.be.true; 36 | expect(Bool.isValid(false)).to.be.true; 37 | }); 38 | 39 | it('returns false for non booleans', function () { 40 | expect(Bool.isValid(0)).to.be.false; 41 | expect(Bool.isValid('0')).to.be.false; 42 | expect(Bool.isValid([true])).to.be.false; 43 | expect(Bool.isValid(null)).to.be.false; 44 | expect(Bool.isValid({})).to.be.false; 45 | expect(Bool.isValid(undefined)).to.be.false; 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /src/string.js: -------------------------------------------------------------------------------- 1 | import { UnsignedInt } from './unsigned-int'; 2 | import { XdrCompositeType } from './xdr-type'; 3 | import { XdrReaderError, XdrWriterError } from './errors'; 4 | 5 | export class String extends XdrCompositeType { 6 | constructor(maxLength = UnsignedInt.MAX_VALUE) { 7 | super(); 8 | this._maxLength = maxLength; 9 | } 10 | 11 | /** 12 | * @inheritDoc 13 | */ 14 | read(reader) { 15 | const size = UnsignedInt.read(reader); 16 | if (size > this._maxLength) 17 | throw new XdrReaderError( 18 | `saw ${size} length String, max allowed is ${this._maxLength}` 19 | ); 20 | 21 | return reader.read(size); 22 | } 23 | 24 | readString(reader) { 25 | return this.read(reader).toString('utf8'); 26 | } 27 | 28 | /** 29 | * @inheritDoc 30 | */ 31 | write(value, writer) { 32 | // calculate string byte size before writing 33 | const size = 34 | typeof value === 'string' 35 | ? Buffer.byteLength(value, 'utf8') 36 | : value.length; 37 | if (size > this._maxLength) 38 | throw new XdrWriterError( 39 | `got ${value.length} bytes, max allowed is ${this._maxLength}` 40 | ); 41 | // write size info 42 | UnsignedInt.write(size, writer); 43 | writer.write(value, size); 44 | } 45 | 46 | /** 47 | * @inheritDoc 48 | */ 49 | isValid(value) { 50 | if (typeof value === 'string') { 51 | return Buffer.byteLength(value, 'utf8') <= this._maxLength; 52 | } 53 | if (value instanceof Array || Buffer.isBuffer(value)) { 54 | return value.length <= this._maxLength; 55 | } 56 | return false; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/var-array.js: -------------------------------------------------------------------------------- 1 | import { UnsignedInt } from './unsigned-int'; 2 | import { XdrCompositeType } from './xdr-type'; 3 | import { XdrReaderError, XdrWriterError } from './errors'; 4 | 5 | export class VarArray extends XdrCompositeType { 6 | constructor(childType, maxLength = UnsignedInt.MAX_VALUE) { 7 | super(); 8 | this._childType = childType; 9 | this._maxLength = maxLength; 10 | } 11 | 12 | /** 13 | * @inheritDoc 14 | */ 15 | read(reader) { 16 | const length = UnsignedInt.read(reader); 17 | if (length > this._maxLength) 18 | throw new XdrReaderError( 19 | `saw ${length} length VarArray, max allowed is ${this._maxLength}` 20 | ); 21 | 22 | const result = new Array(length); 23 | for (let i = 0; i < length; i++) { 24 | result[i] = this._childType.read(reader); 25 | } 26 | return result; 27 | } 28 | 29 | /** 30 | * @inheritDoc 31 | */ 32 | write(value, writer) { 33 | if (!(value instanceof Array)) 34 | throw new XdrWriterError(`value is not array`); 35 | 36 | if (value.length > this._maxLength) 37 | throw new XdrWriterError( 38 | `got array of size ${value.length}, max allowed is ${this._maxLength}` 39 | ); 40 | 41 | UnsignedInt.write(value.length, writer); 42 | for (const child of value) { 43 | this._childType.write(child, writer); 44 | } 45 | } 46 | 47 | /** 48 | * @inheritDoc 49 | */ 50 | isValid(value) { 51 | if (!(value instanceof Array) || value.length > this._maxLength) { 52 | return false; 53 | } 54 | for (const child of value) { 55 | if (!this._childType.isValid(child)) return false; 56 | } 57 | return true; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/unit/option_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 3 | 4 | const subject = new XDR.Option(XDR.Int); 5 | 6 | describe('Option#read', function () { 7 | it('decodes correctly', function () { 8 | expect(read([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00])).to.eql(0); 9 | expect(read([0x00, 0x00, 0x00, 0x00])).to.be.undefined; 10 | }); 11 | 12 | function read(bytes) { 13 | let io = new XdrReader(bytes); 14 | return subject.read(io); 15 | } 16 | }); 17 | 18 | describe('Option#write', function () { 19 | it('encodes correctly', function () { 20 | expect(write(3)).to.eql([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x03]); 21 | expect(write(null)).to.eql([0x00, 0x00, 0x00, 0x00]); 22 | expect(write(undefined)).to.eql([0x00, 0x00, 0x00, 0x00]); 23 | }); 24 | 25 | function write(value) { 26 | let io = new XdrWriter(8); 27 | subject.write(value, io); 28 | return io.toArray(); 29 | } 30 | }); 31 | 32 | describe('Option#isValid', function () { 33 | it('returns true for values of the correct child type', function () { 34 | expect(subject.isValid(0)).to.be.true; 35 | expect(subject.isValid(-1)).to.be.true; 36 | expect(subject.isValid(1)).to.be.true; 37 | }); 38 | 39 | it('returns true for null and undefined', function () { 40 | expect(subject.isValid(null)).to.be.true; 41 | expect(subject.isValid(undefined)).to.be.true; 42 | }); 43 | 44 | it('returns false for values of the wrong type', function () { 45 | expect(subject.isValid(false)).to.be.false; 46 | expect(subject.isValid('hello')).to.be.false; 47 | expect(subject.isValid({})).to.be.false; 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /test/unit/opaque_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 3 | let Opaque = XDR.Opaque; 4 | 5 | let subject = new Opaque(3); 6 | 7 | describe('Opaque#read', function () { 8 | it('decodes correctly', function () { 9 | expect(read([0, 0, 0, 0])).to.eql(Buffer.from([0, 0, 0])); 10 | expect(read([0, 0, 1, 0])).to.eql(Buffer.from([0, 0, 1])); 11 | }); 12 | 13 | it('throws a read error if the padding bytes are not zero', function () { 14 | expect(() => read([0, 0, 1, 1])).to.throw(/read error/i); 15 | }); 16 | 17 | function read(bytes) { 18 | let io = new XdrReader(bytes); 19 | const res = subject.read(io); 20 | expect(io._index).to.eql(4, 'padding not processed by the reader'); 21 | return res; 22 | } 23 | }); 24 | 25 | describe('Opaque#write', function () { 26 | it('encodes correctly', function () { 27 | expect(write(Buffer.from([0, 0, 0]))).to.eql([0, 0, 0, 0]); 28 | expect(write(Buffer.from([0, 0, 1]))).to.eql([0, 0, 1, 0]); 29 | }); 30 | 31 | function write(value) { 32 | let io = new XdrWriter(8); 33 | subject.write(value, io); 34 | return io.toArray(); 35 | } 36 | }); 37 | 38 | describe('Opaque#isValid', function () { 39 | it('returns true for buffers of the correct length', function () { 40 | expect(subject.isValid(Buffer.alloc(3))).to.be.true; 41 | }); 42 | 43 | it('returns false for buffers of the wrong size', function () { 44 | expect(subject.isValid(Buffer.alloc(2))).to.be.false; 45 | expect(subject.isValid(Buffer.alloc(4))).to.be.false; 46 | }); 47 | 48 | it('returns false for non buffers', function () { 49 | expect(subject.isValid(true)).to.be.false; 50 | expect(subject.isValid(null)).to.be.false; 51 | expect(subject.isValid(3)).to.be.false; 52 | expect(subject.isValid([0])).to.be.false; 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /test/unit/float_test.js: -------------------------------------------------------------------------------- 1 | let Float = XDR.Float; 2 | import { XdrReader } from '../../src/serialization/xdr-reader'; 3 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 4 | 5 | describe('Float.read', function () { 6 | it('decodes correctly', function () { 7 | expect(read([0x00, 0x00, 0x00, 0x00])).to.eql(0.0); 8 | expect(read([0x80, 0x00, 0x00, 0x00])).to.eql(-0.0); 9 | expect(read([0x3f, 0x80, 0x00, 0x00])).to.eql(1.0); 10 | expect(read([0xbf, 0x80, 0x00, 0x00])).to.eql(-1.0); 11 | expect(read([0x7f, 0xc0, 0x00, 0x00])).to.eql(NaN); 12 | expect(read([0x7f, 0xf8, 0x00, 0x00])).to.eql(NaN); 13 | }); 14 | 15 | function read(bytes) { 16 | let io = new XdrReader(bytes); 17 | return Float.read(io); 18 | } 19 | }); 20 | 21 | describe('Float.write', function () { 22 | it('encodes correctly', function () { 23 | expect(write(0.0)).to.eql([0x00, 0x00, 0x00, 0x00]); 24 | expect(write(-0.0)).to.eql([0x80, 0x00, 0x00, 0x00]); 25 | expect(write(1.0)).to.eql([0x3f, 0x80, 0x00, 0x00]); 26 | expect(write(-1.0)).to.eql([0xbf, 0x80, 0x00, 0x00]); 27 | }); 28 | 29 | function write(value) { 30 | let io = new XdrWriter(8); 31 | Float.write(value, io); 32 | return io.toArray(); 33 | } 34 | }); 35 | 36 | describe('Float.isValid', function () { 37 | it('returns true for numbers', function () { 38 | expect(Float.isValid(0)).to.be.true; 39 | expect(Float.isValid(-1)).to.be.true; 40 | expect(Float.isValid(1.0)).to.be.true; 41 | expect(Float.isValid(100000.0)).to.be.true; 42 | expect(Float.isValid(NaN)).to.be.true; 43 | expect(Float.isValid(Infinity)).to.be.true; 44 | expect(Float.isValid(-Infinity)).to.be.true; 45 | }); 46 | 47 | it('returns false for non numbers', function () { 48 | expect(Float.isValid(true)).to.be.false; 49 | expect(Float.isValid(false)).to.be.false; 50 | expect(Float.isValid(null)).to.be.false; 51 | expect(Float.isValid('0')).to.be.false; 52 | expect(Float.isValid([])).to.be.false; 53 | expect(Float.isValid([0])).to.be.false; 54 | expect(Float.isValid('hello')).to.be.false; 55 | expect(Float.isValid({ why: 'hello' })).to.be.false; 56 | expect(Float.isValid(['how', 'do', 'you', 'do'])).to.be.false; 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /src/struct.js: -------------------------------------------------------------------------------- 1 | import { Reference } from './reference'; 2 | import { XdrCompositeType, isSerializableIsh } from './xdr-type'; 3 | import { XdrWriterError } from './errors'; 4 | 5 | export class Struct extends XdrCompositeType { 6 | constructor(attributes) { 7 | super(); 8 | this._attributes = attributes || {}; 9 | } 10 | 11 | /** 12 | * @inheritDoc 13 | */ 14 | static read(reader) { 15 | const attributes = {}; 16 | for (const [fieldName, type] of this._fields) { 17 | attributes[fieldName] = type.read(reader); 18 | } 19 | return new this(attributes); 20 | } 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | static write(value, writer) { 26 | if (!this.isValid(value)) { 27 | throw new XdrWriterError( 28 | `${value} has struct name ${value?.constructor?.structName}, not ${ 29 | this.structName 30 | }: ${JSON.stringify(value)}` 31 | ); 32 | } 33 | 34 | for (const [fieldName, type] of this._fields) { 35 | const attribute = value._attributes[fieldName]; 36 | type.write(attribute, writer); 37 | } 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | static isValid(value) { 44 | return ( 45 | value?.constructor?.structName === this.structName || 46 | isSerializableIsh(value, this) 47 | ); 48 | } 49 | 50 | static create(context, name, fields) { 51 | const ChildStruct = class extends Struct {}; 52 | 53 | ChildStruct.structName = name; 54 | 55 | context.results[name] = ChildStruct; 56 | 57 | const mappedFields = new Array(fields.length); 58 | for (let i = 0; i < fields.length; i++) { 59 | const fieldDescriptor = fields[i]; 60 | const fieldName = fieldDescriptor[0]; 61 | let field = fieldDescriptor[1]; 62 | if (field instanceof Reference) { 63 | field = field.resolve(context); 64 | } 65 | mappedFields[i] = [fieldName, field]; 66 | // create accessors 67 | ChildStruct.prototype[fieldName] = createAccessorMethod(fieldName); 68 | } 69 | 70 | ChildStruct._fields = mappedFields; 71 | 72 | return ChildStruct; 73 | } 74 | } 75 | 76 | function createAccessorMethod(name) { 77 | return function readOrWriteAttribute(value) { 78 | if (value !== undefined) { 79 | this._attributes[name] = value; 80 | } 81 | return this._attributes[name]; 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/enum.js: -------------------------------------------------------------------------------- 1 | import { Int } from './int'; 2 | import { XdrPrimitiveType, isSerializableIsh } from './xdr-type'; 3 | import { XdrReaderError, XdrWriterError } from './errors'; 4 | 5 | export class Enum extends XdrPrimitiveType { 6 | constructor(name, value) { 7 | super(); 8 | this.name = name; 9 | this.value = value; 10 | } 11 | 12 | /** 13 | * @inheritDoc 14 | */ 15 | static read(reader) { 16 | const intVal = Int.read(reader); 17 | const res = this._byValue[intVal]; 18 | if (res === undefined) 19 | throw new XdrReaderError( 20 | `unknown ${this.enumName} member for value ${intVal}` 21 | ); 22 | return res; 23 | } 24 | 25 | /** 26 | * @inheritDoc 27 | */ 28 | static write(value, writer) { 29 | if (!this.isValid(value)) { 30 | throw new XdrWriterError( 31 | `${value} has enum name ${value?.enumName}, not ${ 32 | this.enumName 33 | }: ${JSON.stringify(value)}` 34 | ); 35 | } 36 | 37 | Int.write(value.value, writer); 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | static isValid(value) { 44 | return ( 45 | value?.constructor?.enumName === this.enumName || 46 | isSerializableIsh(value, this) 47 | ); 48 | } 49 | 50 | static members() { 51 | return this._members; 52 | } 53 | 54 | static values() { 55 | return Object.values(this._members); 56 | } 57 | 58 | static fromName(name) { 59 | const result = this._members[name]; 60 | 61 | if (!result) 62 | throw new TypeError(`${name} is not a member of ${this.enumName}`); 63 | 64 | return result; 65 | } 66 | 67 | static fromValue(value) { 68 | const result = this._byValue[value]; 69 | if (result === undefined) 70 | throw new TypeError( 71 | `${value} is not a value of any member of ${this.enumName}` 72 | ); 73 | return result; 74 | } 75 | 76 | static create(context, name, members) { 77 | const ChildEnum = class extends Enum {}; 78 | 79 | ChildEnum.enumName = name; 80 | context.results[name] = ChildEnum; 81 | 82 | ChildEnum._members = {}; 83 | ChildEnum._byValue = {}; 84 | 85 | for (const [key, value] of Object.entries(members)) { 86 | const inst = new ChildEnum(key, value); 87 | ChildEnum._members[key] = inst; 88 | ChildEnum._byValue[value] = inst; 89 | ChildEnum[key] = () => inst; 90 | } 91 | 92 | return ChildEnum; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test/unit/double_test.js: -------------------------------------------------------------------------------- 1 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 2 | import { XdrReader } from '../../src/serialization/xdr-reader'; 3 | let Double = XDR.Double; 4 | 5 | describe('Double.read', function () { 6 | it('decodes correctly', function () { 7 | expect(read([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])).to.eql(0.0); 8 | expect(read([0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])).to.eql(-0.0); 9 | expect(read([0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])).to.eql(1.0); 10 | expect(read([0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])).to.eql(-1.0); 11 | expect(read([0x7f, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])).to.eql(NaN); 12 | expect(read([0x7f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01])).to.eql(NaN); 13 | }); 14 | 15 | function read(bytes) { 16 | let io = new XdrReader(bytes); 17 | return Double.read(io); 18 | } 19 | }); 20 | 21 | describe('Double.write', function () { 22 | it('encodes correctly', function () { 23 | expect(write(0.0)).to.eql([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); 24 | expect(write(-0.0)).to.eql([ 25 | 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 26 | ]); 27 | expect(write(1.0)).to.eql([0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); 28 | expect(write(-1.0)).to.eql([ 29 | 0xbf, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 30 | ]); 31 | }); 32 | 33 | function write(value) { 34 | let io = new XdrWriter(8); 35 | Double.write(value, io); 36 | return io.toArray(); 37 | } 38 | }); 39 | 40 | describe('Double.isValid', function () { 41 | it('returns true for numbers', function () { 42 | expect(Double.isValid(0)).to.be.true; 43 | expect(Double.isValid(-1)).to.be.true; 44 | expect(Double.isValid(1.0)).to.be.true; 45 | expect(Double.isValid(100000.0)).to.be.true; 46 | expect(Double.isValid(NaN)).to.be.true; 47 | expect(Double.isValid(Infinity)).to.be.true; 48 | expect(Double.isValid(-Infinity)).to.be.true; 49 | }); 50 | 51 | it('returns false for non numbers', function () { 52 | expect(Double.isValid(true)).to.be.false; 53 | expect(Double.isValid(false)).to.be.false; 54 | expect(Double.isValid(null)).to.be.false; 55 | expect(Double.isValid('0')).to.be.false; 56 | expect(Double.isValid([])).to.be.false; 57 | expect(Double.isValid([0])).to.be.false; 58 | expect(Double.isValid('hello')).to.be.false; 59 | expect(Double.isValid({ why: 'hello' })).to.be.false; 60 | expect(Double.isValid(['how', 'do', 'you', 'do'])).to.be.false; 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stellar/js-xdr", 3 | "version": "3.1.2", 4 | "description": "Read/write XDR encoded data structures (RFC 4506)", 5 | "main": "lib/xdr.js", 6 | "browser": "dist/xdr.js", 7 | "module": "src/index.js", 8 | "scripts": { 9 | "build": "yarn run build:browser && yarn run build:node", 10 | "build:browser": "webpack --mode=production --config ./webpack.config.js", 11 | "build:node": "webpack --mode=development --config ./webpack.config.js", 12 | "test:node": "yarn run build:node && mocha", 13 | "test:browser": "yarn run build:browser && karma start", 14 | "test": "yarn run test:node && yarn run test:browser", 15 | "test-generate": "bundle exec xdrgen -o generated -n test -l javascript examples/test.x", 16 | "fmt": "prettier --write '**/*.js'", 17 | "prepare": "yarn build", 18 | "clean": "rm -rf lib/ dist/" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/stellar/js-xdr.git" 23 | }, 24 | "keywords": [], 25 | "author": "Stellar Development Foundation ", 26 | "license": "Apache-2.0", 27 | "bugs": { 28 | "url": "https://github.com/stellar/js-xdr/issues" 29 | }, 30 | "homepage": "https://github.com/stellar/js-xdr", 31 | "husky": { 32 | "hooks": { 33 | "pre-commit": "lint-staged" 34 | } 35 | }, 36 | "lint-staged": { 37 | "**/*.{js,json}": [ 38 | "yarn fmt", 39 | "git add" 40 | ] 41 | }, 42 | "mocha": { 43 | "require": [ 44 | "@babel/register", 45 | "./test/setup.js" 46 | ], 47 | "recursive": true, 48 | "ui": "bdd" 49 | }, 50 | "devDependencies": { 51 | "@babel/core": "^7.24.9", 52 | "@babel/eslint-parser": "^7.24.8", 53 | "@babel/preset-env": "^7.24.8", 54 | "@babel/register": "^7.24.6", 55 | "babel-loader": "^9.1.3", 56 | "buffer": "^6.0.3", 57 | "chai": "^4.3.10", 58 | "eslint": "^8.57.0", 59 | "eslint-config-airbnb-base": "^15.0.0", 60 | "eslint-config-prettier": "^8.8.0", 61 | "eslint-plugin-import": "^2.29.1", 62 | "eslint-plugin-node": "^11.1.0", 63 | "eslint-plugin-prefer-import": "^0.0.1", 64 | "eslint-plugin-prettier": "^4.2.1", 65 | "husky": "^8.0.3", 66 | "karma": "^6.4.3", 67 | "karma-chrome-launcher": "^3.2.0", 68 | "karma-firefox-launcher": "^2.1.3", 69 | "karma-mocha": "^2.0.1", 70 | "karma-sinon-chai": "^2.0.2", 71 | "karma-webpack": "^5.0.1", 72 | "lint-staged": "13.2.2", 73 | "mocha": "^10.6.0", 74 | "prettier": "^2.8.7", 75 | "sinon": "^15.2.0", 76 | "sinon-chai": "^3.7.0", 77 | "terser-webpack-plugin": "^5.3.10", 78 | "webpack": "^5.93.0", 79 | "webpack-cli": "^5.0.2" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/unit/unsigned-hyper_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 3 | 4 | const UnsignedHyper = XDR.UnsignedHyper; 5 | 6 | describe('UnsignedHyper.read', function () { 7 | it('decodes correctly', function () { 8 | expect(read([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])).to.eql( 9 | UnsignedHyper.fromString('0') 10 | ); 11 | expect(read([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01])).to.eql( 12 | UnsignedHyper.fromString('1') 13 | ); 14 | expect(read([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])).to.eql( 15 | new UnsignedHyper(UnsignedHyper.MAX_VALUE) 16 | ); 17 | }); 18 | 19 | function read(bytes) { 20 | let io = new XdrReader(bytes); 21 | return UnsignedHyper.read(io); 22 | } 23 | }); 24 | 25 | describe('UnsignedHyper.write', function () { 26 | it('encodes correctly', function () { 27 | expect(write(UnsignedHyper.fromString('0'))).to.eql([ 28 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 29 | ]); 30 | expect(write(UnsignedHyper.fromString('1'))).to.eql([ 31 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 32 | ]); 33 | expect(write(UnsignedHyper.MAX_VALUE)).to.eql([ 34 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff 35 | ]); 36 | }); 37 | 38 | function write(value) { 39 | let io = new XdrWriter(8); 40 | UnsignedHyper.write(value, io); 41 | return io.toArray(); 42 | } 43 | }); 44 | 45 | describe('UnsignedHyper.isValid', function () { 46 | it('returns true for UnsignedHyper instances', function () { 47 | expect(UnsignedHyper.isValid(UnsignedHyper.fromString('1'))).to.be.true; 48 | expect(UnsignedHyper.isValid(UnsignedHyper.MIN_VALUE)).to.be.true; 49 | expect(UnsignedHyper.isValid(UnsignedHyper.MAX_VALUE)).to.be.true; 50 | }); 51 | 52 | it('returns false for non UnsignedHypers', function () { 53 | expect(UnsignedHyper.isValid(null)).to.be.false; 54 | expect(UnsignedHyper.isValid(undefined)).to.be.false; 55 | expect(UnsignedHyper.isValid([])).to.be.false; 56 | expect(UnsignedHyper.isValid({})).to.be.false; 57 | expect(UnsignedHyper.isValid(1)).to.be.false; 58 | expect(UnsignedHyper.isValid(true)).to.be.false; 59 | }); 60 | }); 61 | 62 | describe('UnsignedHyper.fromString', function () { 63 | it('works for positive numbers', function () { 64 | expect(UnsignedHyper.fromString('1059').toString()).to.eql('1059'); 65 | }); 66 | 67 | it('fails for negative numbers', function () { 68 | expect(() => UnsignedHyper.fromString('-1059')).to.throw(/positive/); 69 | }); 70 | 71 | it('fails when providing a string with a decimal place', function () { 72 | expect(() => UnsignedHyper.fromString('105946095601.5')).to.throw(/bigint/); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/unit/unsigned-int_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 3 | let UnsignedInt = XDR.UnsignedInt; 4 | 5 | describe('UnsignedInt.read', function () { 6 | it('decodes correctly', function () { 7 | expect(read([0x00, 0x00, 0x00, 0x00])).to.eql(0); 8 | expect(read([0x00, 0x00, 0x00, 0x01])).to.eql(1); 9 | expect(read([0xff, 0xff, 0xff, 0xff])).to.eql(Math.pow(2, 32) - 1); 10 | }); 11 | 12 | function read(bytes) { 13 | let io = new XdrReader(bytes); 14 | return UnsignedInt.read(io); 15 | } 16 | }); 17 | 18 | describe('UnsignedInt.write', function () { 19 | it('encodes correctly', function () { 20 | expect(write(0)).to.eql([0x00, 0x00, 0x00, 0x00]); 21 | expect(write(1)).to.eql([0x00, 0x00, 0x00, 0x01]); 22 | expect(write(Math.pow(2, 32) - 1)).to.eql([0xff, 0xff, 0xff, 0xff]); 23 | }); 24 | 25 | it('throws a write error if the value is not an integral number', function () { 26 | expect(() => write(true)).to.throw(/write error/i); 27 | expect(() => write(undefined)).to.throw(/write error/i); 28 | expect(() => write([])).to.throw(/write error/i); 29 | expect(() => write({})).to.throw(/write error/i); 30 | expect(() => write(1.1)).to.throw(/write error/i); 31 | }); 32 | 33 | function write(value) { 34 | let io = new XdrWriter(8); 35 | UnsignedInt.write(value, io); 36 | return io.toArray(); 37 | } 38 | }); 39 | 40 | describe('UnsignedInt.isValid', function () { 41 | it('returns true for number in a 32-bit range', function () { 42 | expect(UnsignedInt.isValid(0)).to.be.true; 43 | expect(UnsignedInt.isValid(1)).to.be.true; 44 | expect(UnsignedInt.isValid(1.0)).to.be.true; 45 | expect(UnsignedInt.isValid(Math.pow(2, 32) - 1)).to.be.true; 46 | }); 47 | 48 | it('returns false for numbers outside a 32-bit range', function () { 49 | expect(UnsignedInt.isValid(Math.pow(2, 32))).to.be.false; 50 | expect(UnsignedInt.isValid(-1)).to.be.false; 51 | }); 52 | 53 | it('returns false for non numbers', function () { 54 | expect(UnsignedInt.isValid(true)).to.be.false; 55 | expect(UnsignedInt.isValid(false)).to.be.false; 56 | expect(UnsignedInt.isValid(null)).to.be.false; 57 | expect(UnsignedInt.isValid('0')).to.be.false; 58 | expect(UnsignedInt.isValid([])).to.be.false; 59 | expect(UnsignedInt.isValid([0])).to.be.false; 60 | expect(UnsignedInt.isValid('hello')).to.be.false; 61 | expect(UnsignedInt.isValid({ why: 'hello' })).to.be.false; 62 | expect(UnsignedInt.isValid(['how', 'do', 'you', 'do'])).to.be.false; 63 | expect(UnsignedInt.isValid(NaN)).to.be.false; 64 | }); 65 | 66 | it('returns false for non-integral values', function () { 67 | expect(UnsignedInt.isValid(1.1)).to.be.false; 68 | expect(UnsignedInt.isValid(0.1)).to.be.false; 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/unit/hyper_test.js: -------------------------------------------------------------------------------- 1 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 2 | import { XdrReader } from '../../src/serialization/xdr-reader'; 3 | let Hyper = XDR.Hyper; 4 | 5 | describe('Hyper.read', function () { 6 | it('decodes correctly', function () { 7 | expect(read([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])).to.eql( 8 | Hyper.fromString('0') 9 | ); 10 | expect(read([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01])).to.eql( 11 | Hyper.fromString('1') 12 | ); 13 | expect(read([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])).to.eql( 14 | Hyper.fromString('-1') 15 | ); 16 | expect(read([0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])).to.eql( 17 | new Hyper(Hyper.MAX_VALUE) 18 | ); 19 | expect(read([0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])).to.eql( 20 | new Hyper(Hyper.MIN_VALUE) 21 | ); 22 | }); 23 | 24 | function read(bytes) { 25 | let io = new XdrReader(bytes); 26 | return Hyper.read(io); 27 | } 28 | }); 29 | 30 | describe('Hyper.write', function () { 31 | it('encodes correctly', function () { 32 | expect(write(Hyper.fromString('0'))).to.eql([ 33 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 34 | ]); 35 | expect(write(Hyper.fromString('1'))).to.eql([ 36 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 37 | ]); 38 | expect(write(Hyper.fromString('-1'))).to.eql([ 39 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff 40 | ]); 41 | expect(write(Hyper.MAX_VALUE)).to.eql([ 42 | 0x7f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff 43 | ]); 44 | expect(write(Hyper.MIN_VALUE)).to.eql([ 45 | 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 46 | ]); 47 | }); 48 | 49 | function write(value) { 50 | let io = new XdrWriter(8); 51 | Hyper.write(value, io); 52 | return io.toArray(); 53 | } 54 | }); 55 | 56 | describe('Hyper.isValid', function () { 57 | it('returns true for Hyper instances', function () { 58 | expect(Hyper.isValid(Hyper.MIN_VALUE)).to.be.true; 59 | expect(Hyper.isValid(Hyper.MAX_VALUE)).to.be.true; 60 | expect(Hyper.isValid(Hyper.fromString('0'))).to.be.true; 61 | expect(Hyper.isValid(Hyper.fromString('-1'))).to.be.true; 62 | }); 63 | 64 | it('returns false for non Hypers', function () { 65 | expect(Hyper.isValid(null)).to.be.false; 66 | expect(Hyper.isValid(undefined)).to.be.false; 67 | expect(Hyper.isValid([])).to.be.false; 68 | expect(Hyper.isValid({})).to.be.false; 69 | expect(Hyper.isValid(1)).to.be.false; 70 | expect(Hyper.isValid(true)).to.be.false; 71 | }); 72 | }); 73 | 74 | describe('Hyper.fromString', function () { 75 | it('works for positive numbers', function () { 76 | expect(Hyper.fromString('1059').toString()).to.eql('1059'); 77 | }); 78 | 79 | it('works for negative numbers', function () { 80 | expect(Hyper.fromString('-1059').toString()).to.eql('-1059'); 81 | }); 82 | 83 | it('fails when providing a string with a decimal place', function () { 84 | expect(() => Hyper.fromString('105946095601.5')).to.throw(/bigint/); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/unit/var-opaque_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 3 | 4 | const VarOpaque = XDR.VarOpaque; 5 | 6 | let subject = new VarOpaque(2); 7 | 8 | describe('VarOpaque#read', function () { 9 | it('decodes correctly', function () { 10 | expect(read([0, 0, 0, 0])).to.eql(Buffer.from([])); 11 | expect(read([0, 0, 0, 1, 0, 0, 0, 0])).to.eql(Buffer.from([0])); 12 | expect(read([0, 0, 0, 1, 1, 0, 0, 0])).to.eql(Buffer.from([1])); 13 | expect(read([0, 0, 0, 2, 0, 1, 0, 0])).to.eql(Buffer.from([0, 1])); 14 | }); 15 | 16 | it('throws a read error when the encoded length is greater than the allowed max', function () { 17 | expect(() => read([0, 0, 0, 3, 0, 0, 0, 0])).to.throw(/read error/i); 18 | }); 19 | 20 | it('throws a read error if the padding bytes are not zero', function () { 21 | expect(() => 22 | read([0x00, 0x00, 0x00, 0x01, 0x41, 0x01, 0x00, 0x00]) 23 | ).to.throw(/read error/i); 24 | expect(() => 25 | read([0x00, 0x00, 0x00, 0x01, 0x41, 0x00, 0x01, 0x00]) 26 | ).to.throw(/read error/i); 27 | expect(() => 28 | read([0x00, 0x00, 0x00, 0x01, 0x41, 0x00, 0x00, 0x01]) 29 | ).to.throw(/read error/i); 30 | }); 31 | 32 | function read(bytes) { 33 | let io = new XdrReader(bytes); 34 | const res = subject.read(io); 35 | expect(io._index).to.eql( 36 | !res.length ? 4 : 8, 37 | 'padding not processed by the reader' 38 | ); 39 | return res; 40 | } 41 | }); 42 | 43 | describe('VarOpaque#write', function () { 44 | it('encodes correctly', function () { 45 | expect(write(Buffer.from([]))).to.eql([0, 0, 0, 0]); 46 | expect(write(Buffer.from([0]))).to.eql([0, 0, 0, 1, 0, 0, 0, 0]); 47 | expect(write(Buffer.from([1]))).to.eql([0, 0, 0, 1, 1, 0, 0, 0]); 48 | expect(write(Buffer.from([0, 1]))).to.eql([0, 0, 0, 2, 0, 1, 0, 0]); 49 | }); 50 | 51 | function write(value) { 52 | let io = new XdrWriter(8); 53 | subject.write(value, io); 54 | return io.toArray(); 55 | } 56 | }); 57 | 58 | describe('VarOpaque#isValid', function () { 59 | it('returns true for buffers of the correct length', function () { 60 | expect(subject.isValid(Buffer.alloc(0))).to.be.true; 61 | expect(subject.isValid(Buffer.alloc(1))).to.be.true; 62 | expect(subject.isValid(Buffer.alloc(2))).to.be.true; 63 | }); 64 | 65 | it('returns false for buffers of the wrong size', function () { 66 | expect(subject.isValid(Buffer.alloc(3))).to.be.false; 67 | expect(subject.isValid(Buffer.alloc(3000))).to.be.false; 68 | }); 69 | 70 | it('returns false for non buffers', function () { 71 | expect(subject.isValid(true)).to.be.false; 72 | expect(subject.isValid(null)).to.be.false; 73 | expect(subject.isValid(3)).to.be.false; 74 | expect(subject.isValid([0])).to.be.false; 75 | }); 76 | }); 77 | 78 | describe('VarOpaque#constructor', function () { 79 | let subject = new XDR.VarOpaque(); 80 | 81 | it('defaults to max length of a uint max value', function () { 82 | expect(subject._maxLength).to.eql(Math.pow(2, 32) - 1); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /test/unit/int_test.js: -------------------------------------------------------------------------------- 1 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 2 | import { XdrReader } from '../../src/serialization/xdr-reader'; 3 | let Int = XDR.Int; 4 | 5 | describe('Int.read', function () { 6 | it('decodes correctly', function () { 7 | expect(read([0x00, 0x00, 0x00, 0x00])).to.eql(0); 8 | expect(read([0x00, 0x00, 0x00, 0x01])).to.eql(1); 9 | expect(read([0xff, 0xff, 0xff, 0xff])).to.eql(-1); 10 | expect(read([0x7f, 0xff, 0xff, 0xff])).to.eql(Math.pow(2, 31) - 1); 11 | expect(read([0x80, 0x00, 0x00, 0x00])).to.eql(-Math.pow(2, 31)); 12 | }); 13 | 14 | function read(bytes) { 15 | let io = new XdrReader(bytes); 16 | return Int.read(io); 17 | } 18 | }); 19 | 20 | describe('Int.write', function () { 21 | it('encodes correctly', function () { 22 | expect(write(0)).to.eql([0x00, 0x00, 0x00, 0x00]); 23 | expect(write(1)).to.eql([0x00, 0x00, 0x00, 0x01]); 24 | expect(write(-1)).to.eql([0xff, 0xff, 0xff, 0xff]); 25 | expect(write(Math.pow(2, 31) - 1)).to.eql([0x7f, 0xff, 0xff, 0xff]); 26 | expect(write(-Math.pow(2, 31))).to.eql([0x80, 0x00, 0x00, 0x00]); 27 | }); 28 | 29 | it('throws a write error if the value is not an integral number', function () { 30 | expect(() => write(true)).to.throw(/write error/i); 31 | expect(() => write(undefined)).to.throw(/write error/i); 32 | expect(() => write([])).to.throw(/write error/i); 33 | expect(() => write({})).to.throw(/write error/i); 34 | expect(() => write(1.1)).to.throw(/write error/i); 35 | }); 36 | 37 | function write(value) { 38 | let io = new XdrWriter(8); 39 | Int.write(value, io); 40 | return io.toArray(); 41 | } 42 | }); 43 | 44 | describe('Int.isValid', function () { 45 | it('returns true for number in a 32-bit range', function () { 46 | expect(Int.isValid(0)).to.be.true; 47 | expect(Int.isValid(-1)).to.be.true; 48 | expect(Int.isValid(1.0)).to.be.true; 49 | expect(Int.isValid(Math.pow(2, 31) - 1)).to.be.true; 50 | expect(Int.isValid(-Math.pow(2, 31))).to.be.true; 51 | }); 52 | 53 | it('returns false for numbers outside a 32-bit range', function () { 54 | expect(Int.isValid(Math.pow(2, 31))).to.be.false; 55 | expect(Int.isValid(-(Math.pow(2, 31) + 1))).to.be.false; 56 | expect(Int.isValid(1000000000000)).to.be.false; 57 | }); 58 | 59 | it('returns false for non numbers', function () { 60 | expect(Int.isValid(true)).to.be.false; 61 | expect(Int.isValid(false)).to.be.false; 62 | expect(Int.isValid(null)).to.be.false; 63 | expect(Int.isValid('0')).to.be.false; 64 | expect(Int.isValid([])).to.be.false; 65 | expect(Int.isValid([0])).to.be.false; 66 | expect(Int.isValid('hello')).to.be.false; 67 | expect(Int.isValid({ why: 'hello' })).to.be.false; 68 | expect(Int.isValid(['how', 'do', 'you', 'do'])).to.be.false; 69 | expect(Int.isValid(NaN)).to.be.false; 70 | }); 71 | 72 | it('returns false for non-integral values', function () { 73 | expect(Int.isValid(1.1)).to.be.false; 74 | expect(Int.isValid(0.1)).to.be.false; 75 | expect(Int.isValid(-0.1)).to.be.false; 76 | expect(Int.isValid(-1.1)).to.be.false; 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/unit/var-array_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 3 | 4 | const subject = new XDR.VarArray(XDR.Int, 2); 5 | 6 | describe('VarArray#read', function () { 7 | it('decodes correctly', function () { 8 | expect(read([0x00, 0x00, 0x00, 0x00])).to.eql([]); 9 | 10 | expect(read([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00])).to.eql([0]); 11 | expect(read([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01])).to.eql([1]); 12 | 13 | expect( 14 | read([ 15 | 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 16 | ]) 17 | ).to.eql([0, 1]); 18 | expect( 19 | read([ 20 | 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01 21 | ]) 22 | ).to.eql([1, 1]); 23 | }); 24 | 25 | it('throws read error when the encoded array is too large', function () { 26 | expect(() => read([0x00, 0x00, 0x00, 0x03])).to.throw(/read error/i); 27 | }); 28 | 29 | function read(bytes) { 30 | let io = new XdrReader(bytes); 31 | return subject.read(io); 32 | } 33 | }); 34 | 35 | describe('VarArray#write', function () { 36 | it('encodes correctly', function () { 37 | expect(write([])).to.eql([0x00, 0x00, 0x00, 0x00]); 38 | expect(write([0])).to.eql([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00]); 39 | expect(write([0, 1])).to.eql([ 40 | 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 41 | ]); 42 | }); 43 | 44 | it('throws a write error if the value is too large', function () { 45 | expect(() => write([1, 2, 3])).to.throw(/write error/i); 46 | }); 47 | 48 | it('throws a write error if a child element is of the wrong type', function () { 49 | expect(() => write([1, null])).to.throw(/write error/i); 50 | expect(() => write([1, undefined])).to.throw(/write error/i); 51 | expect(() => write([1, 'hi'])).to.throw(/write error/i); 52 | }); 53 | 54 | function write(value) { 55 | let io = new XdrWriter(256); 56 | subject.write(value, io); 57 | return io.toArray(); 58 | } 59 | }); 60 | 61 | describe('VarArray#isValid', function () { 62 | it('returns true for an array of the correct sizes with the correct types', function () { 63 | expect(subject.isValid([])).to.be.true; 64 | expect(subject.isValid([1])).to.be.true; 65 | expect(subject.isValid([1, 2])).to.be.true; 66 | }); 67 | 68 | it('returns false for arrays of the wrong size', function () { 69 | expect(subject.isValid([1, 2, 3])).to.be.false; 70 | }); 71 | 72 | it('returns false if a child element is invalid for the child type', function () { 73 | expect(subject.isValid([1, null])).to.be.false; 74 | expect(subject.isValid([1, undefined])).to.be.false; 75 | expect(subject.isValid([1, 'hello'])).to.be.false; 76 | expect(subject.isValid([1, []])).to.be.false; 77 | expect(subject.isValid([1, {}])).to.be.false; 78 | }); 79 | }); 80 | 81 | describe('VarArray#constructor', function () { 82 | let subject = new XDR.VarArray(XDR.Int); 83 | 84 | it('defaults to max length of a uint max value', function () { 85 | expect(subject._maxLength).to.eql(Math.pow(2, 32) - 1); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /test/unit/array_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 3 | 4 | let zero = new XDR.Array(XDR.Int, 0); 5 | let one = new XDR.Array(XDR.Int, 1); 6 | let many = new XDR.Array(XDR.Int, 2); 7 | 8 | describe('Array#read', function () { 9 | it('decodes correctly', function () { 10 | expect(read(zero, [])).to.eql([]); 11 | expect(read(zero, [0x00, 0x00, 0x00, 0x00])).to.eql([]); 12 | 13 | expect(read(one, [0x00, 0x00, 0x00, 0x00])).to.eql([0]); 14 | expect(read(one, [0x00, 0x00, 0x00, 0x01])).to.eql([1]); 15 | 16 | expect(read(many, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01])).to.eql( 17 | [0, 1] 18 | ); 19 | expect(read(many, [0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01])).to.eql( 20 | [1, 1] 21 | ); 22 | }); 23 | 24 | it("throws XdrReaderError when the byte stream isn't large enough", function () { 25 | expect(() => read(many, [0x00, 0x00, 0x00, 0x00])).to.throw( 26 | /read outside the boundary/i 27 | ); 28 | }); 29 | 30 | function read(arr, bytes) { 31 | let reader = new XdrReader(bytes); 32 | return arr.read(reader); 33 | } 34 | }); 35 | 36 | describe('Array#write', function () { 37 | let subject = many; 38 | 39 | it('encodes correctly', function () { 40 | expect(write([1, 2])).to.eql([ 41 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02 42 | ]); 43 | expect(write([3, 4])).to.eql([ 44 | 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x04 45 | ]); 46 | }); 47 | 48 | it('throws a write error if the value is not the correct length', function () { 49 | expect(() => write(null)).to.throw(/write error/i); 50 | expect(() => write(undefined)).to.throw(/write error/i); 51 | expect(() => write([])).to.throw(/write error/i); 52 | expect(() => write([1])).to.throw(/write error/i); 53 | expect(() => write([1, 2, 3])).to.throw(/write error/i); 54 | }); 55 | 56 | it('throws a write error if a child element is of the wrong type', function () { 57 | expect(() => write([1, null])).to.throw(/write error/i); 58 | expect(() => write([1, undefined])).to.throw(/write error/i); 59 | expect(() => write([1, 'hi'])).to.throw(/write error/i); 60 | }); 61 | 62 | function write(value) { 63 | let writer = new XdrWriter(8); 64 | subject.write(value, writer); 65 | return writer.toArray(); 66 | } 67 | }); 68 | 69 | describe('Array#isValid', function () { 70 | let subject = many; 71 | 72 | it('returns true for an array of the correct size with the correct types', function () { 73 | expect(subject.isValid([1, 2])).to.be.true; 74 | }); 75 | 76 | it('returns false for arrays of the wrong size', function () { 77 | expect(subject.isValid([])).to.be.false; 78 | expect(subject.isValid([1])).to.be.false; 79 | expect(subject.isValid([1, 2, 3])).to.be.false; 80 | }); 81 | 82 | it('returns false if a child element is invalid for the child type', function () { 83 | expect(subject.isValid([1, null])).to.be.false; 84 | expect(subject.isValid([1, undefined])).to.be.false; 85 | expect(subject.isValid([1, 'hello'])).to.be.false; 86 | expect(subject.isValid([1, []])).to.be.false; 87 | expect(subject.isValid([1, {}])).to.be.false; 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/large-int.js: -------------------------------------------------------------------------------- 1 | import { XdrPrimitiveType } from './xdr-type'; 2 | import { 3 | calculateBigIntBoundaries, 4 | encodeBigIntFromBits, 5 | sliceBigInt 6 | } from './bigint-encoder'; 7 | import { XdrNotImplementedDefinitionError, XdrWriterError } from './errors'; 8 | 9 | export class LargeInt extends XdrPrimitiveType { 10 | /** 11 | * @param {Array} parts - Slices to encode 12 | */ 13 | constructor(args) { 14 | super(); 15 | this._value = encodeBigIntFromBits(args, this.size, this.unsigned); 16 | } 17 | 18 | /** 19 | * Signed/unsigned representation 20 | * @type {Boolean} 21 | * @abstract 22 | */ 23 | get unsigned() { 24 | throw new XdrNotImplementedDefinitionError(); 25 | } 26 | 27 | /** 28 | * Size of the integer in bits 29 | * @type {Number} 30 | * @abstract 31 | */ 32 | get size() { 33 | throw new XdrNotImplementedDefinitionError(); 34 | } 35 | 36 | /** 37 | * Slice integer to parts with smaller bit size 38 | * @param {32|64|128} sliceSize - Size of each part in bits 39 | * @return {BigInt[]} 40 | */ 41 | slice(sliceSize) { 42 | return sliceBigInt(this._value, this.size, sliceSize); 43 | } 44 | 45 | toString() { 46 | return this._value.toString(); 47 | } 48 | 49 | toJSON() { 50 | return { _value: this._value.toString() }; 51 | } 52 | 53 | toBigInt() { 54 | return BigInt(this._value); 55 | } 56 | 57 | /** 58 | * @inheritDoc 59 | */ 60 | static read(reader) { 61 | const { size } = this.prototype; 62 | if (size === 64) return new this(reader.readBigUInt64BE()); 63 | return new this( 64 | ...Array.from({ length: size / 64 }, () => 65 | reader.readBigUInt64BE() 66 | ).reverse() 67 | ); 68 | } 69 | 70 | /** 71 | * @inheritDoc 72 | */ 73 | static write(value, writer) { 74 | if (value instanceof this) { 75 | value = value._value; 76 | } else if ( 77 | typeof value !== 'bigint' || 78 | value > this.MAX_VALUE || 79 | value < this.MIN_VALUE 80 | ) 81 | throw new XdrWriterError(`${value} is not a ${this.name}`); 82 | 83 | const { unsigned, size } = this.prototype; 84 | if (size === 64) { 85 | if (unsigned) { 86 | writer.writeBigUInt64BE(value); 87 | } else { 88 | writer.writeBigInt64BE(value); 89 | } 90 | } else { 91 | for (const part of sliceBigInt(value, size, 64).reverse()) { 92 | if (unsigned) { 93 | writer.writeBigUInt64BE(part); 94 | } else { 95 | writer.writeBigInt64BE(part); 96 | } 97 | } 98 | } 99 | } 100 | 101 | /** 102 | * @inheritDoc 103 | */ 104 | static isValid(value) { 105 | return typeof value === 'bigint' || value instanceof this; 106 | } 107 | 108 | /** 109 | * Create instance from string 110 | * @param {String} string - Numeric representation 111 | * @return {LargeInt} 112 | */ 113 | static fromString(string) { 114 | return new this(string); 115 | } 116 | 117 | static MAX_VALUE = 0n; 118 | 119 | static MIN_VALUE = 0n; 120 | 121 | /** 122 | * @internal 123 | * @return {void} 124 | */ 125 | static defineIntBoundaries() { 126 | const [min, max] = calculateBigIntBoundaries( 127 | this.prototype.size, 128 | this.prototype.unsigned 129 | ); 130 | this.MIN_VALUE = min; 131 | this.MAX_VALUE = max; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | es2017: true, 5 | es2020: true, 6 | es2022: true 7 | }, 8 | parserOptions: { ecmaVersion: 13 }, 9 | extends: ['airbnb-base', 'prettier'], 10 | plugins: ['prettier', 'prefer-import'], 11 | rules: { 12 | // OFF 13 | 'import/prefer-default-export': 0, 14 | 'node/no-unsupported-features/es-syntax': 0, 15 | 'node/no-unsupported-features/es-builtins': 0, 16 | camelcase: 0, 17 | 'class-methods-use-this': 0, 18 | 'linebreak-style': 0, 19 | 'new-cap': 0, 20 | 'no-param-reassign': 0, 21 | 'no-underscore-dangle': 0, 22 | 'no-use-before-define': 0, 23 | 'prefer-destructuring': 0, 24 | 'lines-between-class-members': 0, 25 | 'no-plusplus': 0, // allow ++ for iterators 26 | 'no-bitwise': 0, // allow high-performant bitwise operations 27 | 28 | // WARN 29 | 'prefer-import/prefer-import-over-require': [1], 30 | 'no-console': ['warn', { allow: ['assert'] }], 31 | 'no-debugger': 1, 32 | 'no-unused-vars': 1, 33 | 'arrow-body-style': 1, 34 | 'valid-jsdoc': [ 35 | 1, 36 | { 37 | requireReturnDescription: false 38 | } 39 | ], 40 | 'prefer-const': 1, 41 | 'object-shorthand': 1, 42 | 'require-await': 1, 43 | 'max-classes-per-file': ['warn', 3], // do not block imports from other classes 44 | 45 | // ERROR 46 | 'no-unused-expressions': [2, { allowTaggedTemplates: true }], 47 | 48 | // we're redefining this without the Math.pow restriction 49 | // (since we don't want to manually add support for it) 50 | // copied from https://github.com/airbnb/javascript/blob/070e6200bb6c70fa31470ed7a6294f2497468b44/packages/eslint-config-airbnb-base/rules/best-practices.js#L200 51 | 'no-restricted-properties': [ 52 | 'error', 53 | { 54 | object: 'arguments', 55 | property: 'callee', 56 | message: 'arguments.callee is deprecated' 57 | }, 58 | { 59 | object: 'global', 60 | property: 'isFinite', 61 | message: 'Please use Number.isFinite instead' 62 | }, 63 | { 64 | object: 'self', 65 | property: 'isFinite', 66 | message: 'Please use Number.isFinite instead' 67 | }, 68 | { 69 | object: 'window', 70 | property: 'isFinite', 71 | message: 'Please use Number.isFinite instead' 72 | }, 73 | { 74 | object: 'global', 75 | property: 'isNaN', 76 | message: 'Please use Number.isNaN instead' 77 | }, 78 | { 79 | object: 'self', 80 | property: 'isNaN', 81 | message: 'Please use Number.isNaN instead' 82 | }, 83 | { 84 | object: 'window', 85 | property: 'isNaN', 86 | message: 'Please use Number.isNaN instead' 87 | }, 88 | { 89 | property: '__defineGetter__', 90 | message: 'Please use Object.defineProperty instead.' 91 | }, 92 | { 93 | property: '__defineSetter__', 94 | message: 'Please use Object.defineProperty instead.' 95 | } 96 | ], 97 | 'no-restricted-syntax': [ 98 | // override basic rule to allow ForOfStatement 99 | 'error', 100 | { 101 | selector: 'ForInStatement', 102 | message: 103 | 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.' 104 | }, 105 | { 106 | selector: 'LabeledStatement', 107 | message: 108 | 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.' 109 | }, 110 | { 111 | selector: 'WithStatement', 112 | message: 113 | '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.' 114 | } 115 | ] 116 | } 117 | }; 118 | -------------------------------------------------------------------------------- /src/serialization/xdr-reader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | */ 4 | import { XdrReaderError } from '../errors'; 5 | 6 | export class XdrReader { 7 | /** 8 | * @constructor 9 | * @param {Buffer} source - Buffer containing serialized data 10 | */ 11 | constructor(source) { 12 | if (!Buffer.isBuffer(source)) { 13 | if ( 14 | source instanceof Array || 15 | Array.isArray(source) || 16 | ArrayBuffer.isView(source) 17 | ) { 18 | source = Buffer.from(source); 19 | } else { 20 | throw new XdrReaderError(`source invalid: ${source}`); 21 | } 22 | } 23 | 24 | this._buffer = source; 25 | this._length = source.length; 26 | this._index = 0; 27 | } 28 | 29 | /** 30 | * @type {Buffer} 31 | * @private 32 | * @readonly 33 | */ 34 | _buffer; 35 | /** 36 | * @type {Number} 37 | * @private 38 | * @readonly 39 | */ 40 | _length; 41 | /** 42 | * @type {Number} 43 | * @private 44 | * @readonly 45 | */ 46 | _index; 47 | 48 | /** 49 | * Check if the reader reached the end of the input buffer 50 | * @return {Boolean} 51 | */ 52 | get eof() { 53 | return this._index === this._length; 54 | } 55 | 56 | /** 57 | * Advance reader position, check padding and overflow 58 | * @param {Number} size - Bytes to read 59 | * @return {Number} Position to read from 60 | * @private 61 | */ 62 | advance(size) { 63 | const from = this._index; 64 | // advance cursor position 65 | this._index += size; 66 | // check buffer boundaries 67 | if (this._length < this._index) 68 | throw new XdrReaderError( 69 | 'attempt to read outside the boundary of the buffer' 70 | ); 71 | // check that padding is correct for Opaque and String 72 | const padding = 4 - (size % 4 || 4); 73 | if (padding > 0) { 74 | for (let i = 0; i < padding; i++) 75 | if (this._buffer[this._index + i] !== 0) 76 | // all bytes in the padding should be zeros 77 | throw new XdrReaderError('invalid padding'); 78 | this._index += padding; 79 | } 80 | return from; 81 | } 82 | 83 | /** 84 | * Reset reader position 85 | * @return {void} 86 | */ 87 | rewind() { 88 | this._index = 0; 89 | } 90 | 91 | /** 92 | * Read byte array from the buffer 93 | * @param {Number} size - Bytes to read 94 | * @return {Buffer} - Sliced portion of the underlying buffer 95 | */ 96 | read(size) { 97 | const from = this.advance(size); 98 | return this._buffer.subarray(from, from + size); 99 | } 100 | 101 | /** 102 | * Read i32 from buffer 103 | * @return {Number} 104 | */ 105 | readInt32BE() { 106 | return this._buffer.readInt32BE(this.advance(4)); 107 | } 108 | 109 | /** 110 | * Read u32 from buffer 111 | * @return {Number} 112 | */ 113 | readUInt32BE() { 114 | return this._buffer.readUInt32BE(this.advance(4)); 115 | } 116 | 117 | /** 118 | * Read i64 from buffer 119 | * @return {BigInt} 120 | */ 121 | readBigInt64BE() { 122 | return this._buffer.readBigInt64BE(this.advance(8)); 123 | } 124 | 125 | /** 126 | * Read u64 from buffer 127 | * @return {BigInt} 128 | */ 129 | readBigUInt64BE() { 130 | return this._buffer.readBigUInt64BE(this.advance(8)); 131 | } 132 | 133 | /** 134 | * Read float from buffer 135 | * @return {Number} 136 | */ 137 | readFloatBE() { 138 | return this._buffer.readFloatBE(this.advance(4)); 139 | } 140 | 141 | /** 142 | * Read double from buffer 143 | * @return {Number} 144 | */ 145 | readDoubleBE() { 146 | return this._buffer.readDoubleBE(this.advance(8)); 147 | } 148 | 149 | /** 150 | * Ensure that input buffer has been consumed in full, otherwise it's a type mismatch 151 | * @return {void} 152 | * @throws {XdrReaderError} 153 | */ 154 | ensureInputConsumed() { 155 | if (this._index !== this._length) 156 | throw new XdrReaderError( 157 | `invalid XDR contract typecast - source buffer not entirely consumed` 158 | ); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /examples/test.x: -------------------------------------------------------------------------------- 1 | const HASH_SIZE = 32; 2 | 3 | typedef opaque Signature[32]; 4 | 5 | enum ResultType 6 | { 7 | OK = 0, 8 | ERROR = 1 9 | }; 10 | 11 | union Result switch(ResultType type) 12 | { 13 | case OK: 14 | void; 15 | case ERROR: 16 | int code; 17 | }; 18 | 19 | typedef unsigned int ColorCode; 20 | 21 | struct Color 22 | { 23 | ColorCode red; 24 | ColorCode green; 25 | ColorCode blue; 26 | }; 27 | 28 | 29 | struct Exhaustive 30 | { 31 | 32 | // Bools 33 | bool aBool; 34 | bool* anOptionalBool; 35 | bool aBoolArray[5]; 36 | bool aBoolVarArray<5>; 37 | bool anUnboundedBoolVarArray<>; 38 | 39 | // Ints 40 | int anInt; 41 | int* anOptionalInt; 42 | int anIntArray[5]; 43 | int anIntVarArray<5>; 44 | int anUnboundedIntVarArray<>; 45 | 46 | // Uints 47 | unsigned int anUnsignedInt; 48 | unsigned int* anOptionalUnsignedInt; 49 | unsigned int anUnsignedIntArray[5]; 50 | unsigned int anUnsignedIntVarArray<5>; 51 | unsigned int anUnboundedUnsignedIntVarArray<>; 52 | 53 | // Hypers 54 | hyper aHyper; 55 | hyper* anOptionalHyper; 56 | hyper aHyperArray[5]; 57 | hyper aHyperVarArray<5>; 58 | hyper anUnboundedHyperVarArray<>; 59 | 60 | // Uhypers 61 | unsigned hyper anUnsignedHyper; 62 | unsigned hyper* anOptionalUnsignedHyper; 63 | unsigned hyper anUnsignedHyperArray[5]; 64 | unsigned hyper anUnsignedHyperVarArray<5>; 65 | unsigned hyper anUnboundedUnsignedHyperVarArray<>; 66 | 67 | // Floats 68 | float aFloat; 69 | float* anOptionalFloat; 70 | float aFloatArray[5]; 71 | float aFloatVarArray<5>; 72 | float anUnboundedFloatVarArray<>; 73 | 74 | 75 | // Doubles 76 | double aDouble; 77 | double* anOptionalDouble; 78 | double aDoubleArray[5]; 79 | double aDoubleVarArray<5>; 80 | double anUnboundedDoubleVarArray<>; 81 | 82 | 83 | // Opaque 84 | opaque anOpaque[10]; 85 | 86 | // VarOpaque 87 | opaque aVarOpaque<10>; 88 | opaque anUnboundedVarOpaque<>; 89 | 90 | // String 91 | string aString<19>; 92 | string anUnboundedString<>; 93 | 94 | 95 | // Typedef 96 | Signature aSignature; 97 | Signature* anOptionalSignature; 98 | Signature aSignatureArray[5]; 99 | Signature aSignatureVarArray<5>; 100 | Signature anUnboundedSignatureVarArray<>; 101 | 102 | // Enum 103 | ResultType aResultType; 104 | ResultType* anOptionalResultType; 105 | ResultType aResultTypeArray[5]; 106 | ResultType aResultTypeVarArray<5>; 107 | ResultType anUnboundedResultTypeVarArray<>; 108 | 109 | 110 | // Struct 111 | Color aColor; 112 | Color* anOptionalColor; 113 | Color aColorArray[5]; 114 | Color aColorVarArray<5>; 115 | Color anUnboundedColorVarArray<>; 116 | 117 | // Union 118 | Result aResult; 119 | Result* anOptionalResult; 120 | Result aResultArray[5]; 121 | Result aResultVarArray<5>; 122 | Result anUnboundedResultVarArray<>; 123 | 124 | //Nested enum 125 | enum { OK = 0 } aNestedEnum; 126 | enum { OK = 0 } *anOptionalNestedEnum; 127 | enum { OK = 0 } aNestedEnumArray[3]; 128 | enum { OK = 0 } aNestedEnumVarArray<3>; 129 | enum { OK = 0 } anUnboundedNestedEnumVarArray<>; 130 | 131 | //Nested Struct 132 | struct { int value; } aNestedStruct; 133 | struct { int value; } *anOptionalNestedStruct; 134 | struct { int value; } aNestedStructArray[3]; 135 | struct { int value; } aNestedStructVarArray<3>; 136 | struct { int value; } anUnboundedNestedStructVarArray<>; 137 | 138 | }; 139 | 140 | enum ExhaustiveUnionType { 141 | VOID_ARM = 0, 142 | 143 | PRIMITIVE_SIMPLE_ARM = 1, 144 | PRIMITIVE_OPTIONAL_ARM = 2, 145 | PRIMITIVE_ARRAY_ARM = 2, 146 | PRIMITIVE_VARARRAY_ARM = 3, 147 | 148 | CUSTOM_SIMPLE_ARM = 4, 149 | CUSTOM_OPTIONAL_ARM = 5, 150 | CUSTOM_ARRAY_ARM = 6, 151 | CUSTOM_VARARRAY_ARM = 7, 152 | 153 | FOR_DEFAULT = -1 154 | }; 155 | 156 | 157 | union ExhaustiveUnion switch(ExhaustiveUnionType type) 158 | { 159 | case VOID_ARM: void; 160 | case PRIMITIVE_SIMPLE_ARM: int aPrimitiveSimpleArm; 161 | case PRIMITIVE_OPTIONAL_ARM: int* aPrimitiveOptionalArm; 162 | 163 | default: int aPrimitiveDefault; 164 | }; -------------------------------------------------------------------------------- /test/unit/enum_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 3 | import { Enum } from '../../src/enum'; 4 | 5 | /* jshint -W030 */ 6 | 7 | let emptyContext = { definitions: {}, results: {} }; 8 | let Color = XDR.Enum.create(emptyContext, 'Color', { 9 | red: 0, 10 | green: 1, 11 | evenMoreGreen: 3 12 | }); 13 | 14 | describe('Enum.fromName', function () { 15 | it('returns the member with the provided name', function () { 16 | expect(Color.fromName('red')).to.eql(Color.red()); 17 | expect(Color.fromName('green')).to.eql(Color.green()); 18 | expect(Color.fromName('evenMoreGreen')).to.eql(Color.evenMoreGreen()); 19 | }); 20 | 21 | it('throws an error if the name is not correct', function () { 22 | expect(() => Color.fromName('obviouslyNotAColor')).to.throw( 23 | /not a member/i 24 | ); 25 | }); 26 | }); 27 | 28 | describe('Enum.fromValue', function () { 29 | it('returns the member with the provided value', function () { 30 | expect(Color.fromValue(0)).to.eql(Color.red()); 31 | expect(Color.fromValue(1)).to.eql(Color.green()); 32 | expect(Color.fromValue(3)).to.eql(Color.evenMoreGreen()); 33 | }); 34 | 35 | it('throws an error if the value is not correct', function () { 36 | expect(() => Color.fromValue(999)).to.throw(/not a value/i); 37 | }); 38 | }); 39 | 40 | describe('Enum.read', function () { 41 | it('decodes correctly', function () { 42 | expect(read([0x00, 0x00, 0x00, 0x00])).to.eql(Color.red()); 43 | expect(read([0x00, 0x00, 0x00, 0x01])).to.eql(Color.green()); 44 | expect(read([0x00, 0x00, 0x00, 0x03])).to.eql(Color.evenMoreGreen()); 45 | }); 46 | 47 | it("throws read error when encoded value isn't defined on the enum", function () { 48 | expect(() => read([0x00, 0x00, 0x00, 0x02])).to.throw(/read error/i); 49 | }); 50 | 51 | function read(bytes) { 52 | let io = new XdrReader(bytes); 53 | return Color.read(io); 54 | } 55 | }); 56 | 57 | describe('Enum.write', function () { 58 | it('encodes correctly', function () { 59 | expect(write(Color.red())).to.eql([0x00, 0x00, 0x00, 0x00]); 60 | expect(write(Color.green())).to.eql([0x00, 0x00, 0x00, 0x01]); 61 | expect(write(Color.evenMoreGreen())).to.eql([0x00, 0x00, 0x00, 0x03]); 62 | 63 | expect(Color.red().toXDR('hex')).to.eql('00000000'); 64 | expect(Color.green().toXDR('hex')).to.eql('00000001'); 65 | expect(Color.evenMoreGreen().toXDR('hex')).to.eql('00000003'); 66 | }); 67 | 68 | it('throws a write error if the value is not the correct type', function () { 69 | expect(() => write(null)).to.throw(/write error/i); 70 | expect(() => write(undefined)).to.throw(/write error/i); 71 | expect(() => write([])).to.throw(/write error/i); 72 | expect(() => write({})).to.throw(/write error/i); 73 | expect(() => write(1)).to.throw(/write error/i); 74 | expect(() => write(true)).to.throw(/write error/i); 75 | }); 76 | 77 | function write(value) { 78 | let io = new XdrWriter(8); 79 | Color.write(value, io); 80 | return io.toArray(); 81 | } 82 | }); 83 | 84 | describe('Enum.isValid', function () { 85 | it('returns true for members of the enum', function () { 86 | expect(Color.isValid(Color.red())).to.be.true; 87 | expect(Color.isValid(Color.green())).to.be.true; 88 | expect(Color.isValid(Color.evenMoreGreen())).to.be.true; 89 | }); 90 | 91 | it('works for "enum-like" objects', function () { 92 | class FakeEnum extends Enum {} 93 | FakeEnum.enumName = 'Color'; 94 | 95 | let r = new FakeEnum(); 96 | expect(Color.isValid(r)).to.be.true; 97 | 98 | FakeEnum.enumName = 'NotColor'; 99 | r = new FakeEnum(); 100 | expect(Color.isValid(r)).to.be.false; 101 | 102 | // make sure you can't fool it 103 | FakeEnum.enumName = undefined; 104 | FakeEnum.unionName = 'Color'; 105 | r = new FakeEnum(); 106 | expect(Color.isValid(r)).to.be.false; 107 | }); 108 | 109 | it('returns false for arrays of the wrong size', function () { 110 | expect(Color.isValid(null)).to.be.false; 111 | expect(Color.isValid(undefined)).to.be.false; 112 | expect(Color.isValid([])).to.be.false; 113 | expect(Color.isValid({})).to.be.false; 114 | expect(Color.isValid(1)).to.be.false; 115 | expect(Color.isValid(true)).to.be.false; 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /src/bigint-encoder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Encode a native `bigint` value from a list of arbitrary integer-like values. 3 | * 4 | * @param {Array} parts - Slices to encode in big-endian 5 | * format (i.e. earlier elements are higher bits) 6 | * @param {64|128|256} size - Number of bits in the target integer type 7 | * @param {boolean} unsigned - Whether it's an unsigned integer 8 | * 9 | * @returns {bigint} 10 | */ 11 | export function encodeBigIntFromBits(parts, size, unsigned) { 12 | if (!(parts instanceof Array)) { 13 | // allow a single parameter instead of an array 14 | parts = [parts]; 15 | } else if (parts.length && parts[0] instanceof Array) { 16 | // unpack nested array param 17 | parts = parts[0]; 18 | } 19 | 20 | const total = parts.length; 21 | const sliceSize = size / total; 22 | switch (sliceSize) { 23 | case 32: 24 | case 64: 25 | case 128: 26 | case 256: 27 | break; 28 | 29 | default: 30 | throw new RangeError( 31 | `expected slices to fit in 32/64/128/256 bits, got ${parts}` 32 | ); 33 | } 34 | 35 | // normalize all inputs to bigint 36 | try { 37 | for (let i = 0; i < parts.length; i++) { 38 | if (typeof parts[i] !== 'bigint') { 39 | parts[i] = BigInt(parts[i].valueOf()); 40 | } 41 | } 42 | } catch (e) { 43 | throw new TypeError(`expected bigint-like values, got: ${parts} (${e})`); 44 | } 45 | 46 | // check for sign mismatches for single inputs (this is a special case to 47 | // handle one parameter passed to e.g. UnsignedHyper et al.) 48 | // see https://github.com/stellar/js-xdr/pull/100#discussion_r1228770845 49 | if (unsigned && parts.length === 1 && parts[0] < 0n) { 50 | throw new RangeError(`expected a positive value, got: ${parts}`); 51 | } 52 | 53 | // encode in big-endian fashion, shifting each slice by the slice size 54 | let result = BigInt.asUintN(sliceSize, parts[0]); // safe: len >= 1 55 | for (let i = 1; i < parts.length; i++) { 56 | result |= BigInt.asUintN(sliceSize, parts[i]) << BigInt(i * sliceSize); 57 | } 58 | 59 | // interpret value as signed if necessary and clamp it 60 | if (!unsigned) { 61 | result = BigInt.asIntN(size, result); 62 | } 63 | 64 | // check boundaries 65 | const [min, max] = calculateBigIntBoundaries(size, unsigned); 66 | if (result >= min && result <= max) { 67 | return result; 68 | } 69 | 70 | // failed to encode 71 | throw new TypeError( 72 | `bigint values [${parts}] for ${formatIntName( 73 | size, 74 | unsigned 75 | )} out of range [${min}, ${max}]: ${result}` 76 | ); 77 | } 78 | 79 | /** 80 | * Transforms a single bigint value that's supposed to represent a `size`-bit 81 | * integer into a list of `sliceSize`d chunks. 82 | * 83 | * @param {bigint} value - Single bigint value to decompose 84 | * @param {64|128|256} iSize - Number of bits represented by `value` 85 | * @param {32|64|128} sliceSize - Number of chunks to decompose into 86 | * @return {bigint[]} 87 | */ 88 | export function sliceBigInt(value, iSize, sliceSize) { 89 | if (typeof value !== 'bigint') { 90 | throw new TypeError(`Expected bigint 'value', got ${typeof value}`); 91 | } 92 | 93 | const total = iSize / sliceSize; 94 | if (total === 1) { 95 | return [value]; 96 | } 97 | 98 | if ( 99 | sliceSize < 32 || 100 | sliceSize > 128 || 101 | (total !== 2 && total !== 4 && total !== 8) 102 | ) { 103 | throw new TypeError( 104 | `invalid bigint (${value}) and slice size (${iSize} -> ${sliceSize}) combination` 105 | ); 106 | } 107 | 108 | const shift = BigInt(sliceSize); 109 | 110 | // iterate shift and mask application 111 | const result = new Array(total); 112 | for (let i = 0; i < total; i++) { 113 | // we force a signed interpretation to preserve sign in each slice value, 114 | // but downstream can convert to unsigned if it's appropriate 115 | result[i] = BigInt.asIntN(sliceSize, value); // clamps to size 116 | 117 | // move on to the next chunk 118 | value >>= shift; 119 | } 120 | 121 | return result; 122 | } 123 | 124 | export function formatIntName(precision, unsigned) { 125 | return `${unsigned ? 'u' : 'i'}${precision}`; 126 | } 127 | 128 | /** 129 | * Get min|max boundaries for an integer with a specified bits size 130 | * @param {64|128|256} size - Number of bits in the source integer type 131 | * @param {Boolean} unsigned - Whether it's an unsigned integer 132 | * @return {BigInt[]} 133 | */ 134 | export function calculateBigIntBoundaries(size, unsigned) { 135 | if (unsigned) { 136 | return [0n, (1n << BigInt(size)) - 1n]; 137 | } 138 | 139 | const boundary = 1n << BigInt(size - 1); 140 | return [0n - boundary, boundary - 1n]; 141 | } 142 | -------------------------------------------------------------------------------- /test/unit/struct_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 3 | import { Struct } from '../../src/struct'; 4 | 5 | /* jshint -W030 */ 6 | 7 | let emptyContext = { definitions: {}, results: {} }; 8 | let MyRange = XDR.Struct.create(emptyContext, 'MyRange', [ 9 | ['begin', XDR.Int], 10 | ['end', XDR.Int], 11 | ['inclusive', XDR.Bool] 12 | ]); 13 | 14 | describe('Struct.read', function () { 15 | it('decodes correctly', function () { 16 | let empty = read([ 17 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 18 | ]); 19 | expect(empty).to.be.instanceof(MyRange); 20 | expect(empty.begin()).to.eql(0); 21 | expect(empty.end()).to.eql(0); 22 | expect(empty.inclusive()).to.eql(false); 23 | 24 | let filled = read([ 25 | 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x01 26 | ]); 27 | expect(filled).to.be.instanceof(MyRange); 28 | expect(filled.begin()).to.eql(5); 29 | expect(filled.end()).to.eql(255); 30 | expect(filled.inclusive()).to.eql(true); 31 | }); 32 | 33 | function read(bytes) { 34 | let io = new XdrReader(bytes); 35 | return MyRange.read(io); 36 | } 37 | }); 38 | 39 | describe('Struct.write', function () { 40 | it('encodes correctly', function () { 41 | let empty = new MyRange({ 42 | begin: 0, 43 | end: 0, 44 | inclusive: false 45 | }); 46 | 47 | expect(write(empty)).to.eql([ 48 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 49 | ]); 50 | 51 | let filled = new MyRange({ 52 | begin: 5, 53 | end: 255, 54 | inclusive: true 55 | }); 56 | 57 | expect(write(filled)).to.eql([ 58 | 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x01 59 | ]); 60 | }); 61 | 62 | it('throws a write error if the value is not the correct type', function () { 63 | expect(() => write(null)).to.throw(/write error/i); 64 | expect(() => write(undefined)).to.throw(/write error/i); 65 | expect(() => write([])).to.throw(/write error/i); 66 | expect(() => write({})).to.throw(/write error/i); 67 | expect(() => write(1)).to.throw(/write error/i); 68 | expect(() => write(true)).to.throw(/write error/i); 69 | }); 70 | 71 | it('throws a write error if the struct is not valid', function () { 72 | expect(() => write(new MyRange({}))).to.throw(/write error/i); 73 | }); 74 | 75 | function write(value) { 76 | let io = new XdrWriter(256); 77 | MyRange.write(value, io); 78 | return io.toArray(); 79 | } 80 | }); 81 | 82 | describe('Struct.isValid', function () { 83 | it('returns true for instances of the struct', function () { 84 | expect(MyRange.isValid(new MyRange({}))).to.be.true; 85 | }); 86 | 87 | it('works for "struct-like" objects', function () { 88 | class FakeStruct extends Struct {} 89 | 90 | FakeStruct.structName = 'MyRange'; 91 | let r = new FakeStruct(); 92 | expect(MyRange.isValid(r)).to.be.true; 93 | 94 | FakeStruct.structName = 'NotMyRange'; 95 | r = new FakeStruct(); 96 | expect(MyRange.isValid(r)).to.be.false; 97 | }); 98 | 99 | it('returns false for anything else', function () { 100 | expect(MyRange.isValid(null)).to.be.false; 101 | expect(MyRange.isValid(undefined)).to.be.false; 102 | expect(MyRange.isValid([])).to.be.false; 103 | expect(MyRange.isValid({})).to.be.false; 104 | expect(MyRange.isValid(1)).to.be.false; 105 | expect(MyRange.isValid(true)).to.be.false; 106 | }); 107 | }); 108 | 109 | describe('Struct.validateXDR', function () { 110 | it('returns true for valid XDRs', function () { 111 | let subject = new MyRange({ begin: 5, end: 255, inclusive: true }); 112 | expect(MyRange.validateXDR(subject.toXDR())).to.be.true; 113 | expect(MyRange.validateXDR(subject.toXDR('hex'), 'hex')).to.be.true; 114 | expect(MyRange.validateXDR(subject.toXDR('base64'), 'base64')).to.be.true; 115 | }); 116 | 117 | it('returns false for invalid XDRs', function () { 118 | expect(MyRange.validateXDR(Buffer.alloc(1))).to.be.false; 119 | expect(MyRange.validateXDR('00', 'hex')).to.be.false; 120 | expect(MyRange.validateXDR('AA==', 'base64')).to.be.false; 121 | }); 122 | }); 123 | 124 | describe('Struct: attributes', function () { 125 | it('properly retrieves attributes', function () { 126 | let subject = new MyRange({ begin: 5, end: 255, inclusive: true }); 127 | expect(subject.begin()).to.eql(5); 128 | }); 129 | 130 | it('properly sets attributes', function () { 131 | let subject = new MyRange({ begin: 5, end: 255, inclusive: true }); 132 | expect(subject.begin(10)).to.eql(10); 133 | expect(subject.begin()).to.eql(10); 134 | }); 135 | }); 136 | -------------------------------------------------------------------------------- /src/union.js: -------------------------------------------------------------------------------- 1 | import { Void } from './void'; 2 | import { Reference } from './reference'; 3 | import { XdrCompositeType, isSerializableIsh } from './xdr-type'; 4 | import { XdrWriterError } from './errors'; 5 | 6 | export class Union extends XdrCompositeType { 7 | constructor(aSwitch, value) { 8 | super(); 9 | this.set(aSwitch, value); 10 | } 11 | 12 | set(aSwitch, value) { 13 | if (typeof aSwitch === 'string') { 14 | aSwitch = this.constructor._switchOn.fromName(aSwitch); 15 | } 16 | 17 | this._switch = aSwitch; 18 | const arm = this.constructor.armForSwitch(this._switch); 19 | this._arm = arm; 20 | this._armType = arm === Void ? Void : this.constructor._arms[arm]; 21 | this._value = value; 22 | } 23 | 24 | get(armName = this._arm) { 25 | if (this._arm !== Void && this._arm !== armName) 26 | throw new TypeError(`${armName} not set`); 27 | return this._value; 28 | } 29 | 30 | switch() { 31 | return this._switch; 32 | } 33 | 34 | arm() { 35 | return this._arm; 36 | } 37 | 38 | armType() { 39 | return this._armType; 40 | } 41 | 42 | value() { 43 | return this._value; 44 | } 45 | 46 | static armForSwitch(aSwitch) { 47 | const member = this._switches.get(aSwitch); 48 | if (member !== undefined) { 49 | return member; 50 | } 51 | if (this._defaultArm) { 52 | return this._defaultArm; 53 | } 54 | throw new TypeError(`Bad union switch: ${aSwitch}`); 55 | } 56 | 57 | static armTypeForArm(arm) { 58 | if (arm === Void) { 59 | return Void; 60 | } 61 | return this._arms[arm]; 62 | } 63 | 64 | /** 65 | * @inheritDoc 66 | */ 67 | static read(reader) { 68 | const aSwitch = this._switchOn.read(reader); 69 | const arm = this.armForSwitch(aSwitch); 70 | const armType = arm === Void ? Void : this._arms[arm]; 71 | let value; 72 | if (armType !== undefined) { 73 | value = armType.read(reader); 74 | } else { 75 | value = arm.read(reader); 76 | } 77 | return new this(aSwitch, value); 78 | } 79 | 80 | /** 81 | * @inheritDoc 82 | */ 83 | static write(value, writer) { 84 | if (!this.isValid(value)) { 85 | throw new XdrWriterError( 86 | `${value} has union name ${value?.unionName}, not ${ 87 | this.unionName 88 | }: ${JSON.stringify(value)}` 89 | ); 90 | } 91 | 92 | this._switchOn.write(value.switch(), writer); 93 | value.armType().write(value.value(), writer); 94 | } 95 | 96 | /** 97 | * @inheritDoc 98 | */ 99 | static isValid(value) { 100 | return ( 101 | value?.constructor?.unionName === this.unionName || 102 | isSerializableIsh(value, this) 103 | ); 104 | } 105 | 106 | static create(context, name, config) { 107 | const ChildUnion = class extends Union {}; 108 | 109 | ChildUnion.unionName = name; 110 | context.results[name] = ChildUnion; 111 | 112 | if (config.switchOn instanceof Reference) { 113 | ChildUnion._switchOn = config.switchOn.resolve(context); 114 | } else { 115 | ChildUnion._switchOn = config.switchOn; 116 | } 117 | 118 | ChildUnion._switches = new Map(); 119 | ChildUnion._arms = {}; 120 | 121 | // resolve default arm 122 | let defaultArm = config.defaultArm; 123 | if (defaultArm instanceof Reference) { 124 | defaultArm = defaultArm.resolve(context); 125 | } 126 | 127 | ChildUnion._defaultArm = defaultArm; 128 | 129 | for (const [aSwitch, armName] of config.switches) { 130 | const key = 131 | typeof aSwitch === 'string' 132 | ? ChildUnion._switchOn.fromName(aSwitch) 133 | : aSwitch; 134 | 135 | ChildUnion._switches.set(key, armName); 136 | } 137 | 138 | // add enum-based helpers 139 | // NOTE: we don't have good notation for "is a subclass of XDR.Enum", 140 | // and so we use the following check (does _switchOn have a `values` 141 | // attribute) to approximate the intent. 142 | if (ChildUnion._switchOn.values !== undefined) { 143 | for (const aSwitch of ChildUnion._switchOn.values()) { 144 | // Add enum-based constructors 145 | ChildUnion[aSwitch.name] = function ctr(value) { 146 | return new ChildUnion(aSwitch, value); 147 | }; 148 | 149 | // Add enum-based "set" helpers 150 | ChildUnion.prototype[aSwitch.name] = function set(value) { 151 | return this.set(aSwitch, value); 152 | }; 153 | } 154 | } 155 | 156 | if (config.arms) { 157 | for (const [armsName, value] of Object.entries(config.arms)) { 158 | ChildUnion._arms[armsName] = 159 | value instanceof Reference ? value.resolve(context) : value; 160 | // Add arm accessor helpers 161 | if (value !== Void) { 162 | ChildUnion.prototype[armsName] = function get() { 163 | return this.get(armsName); 164 | }; 165 | } 166 | } 167 | } 168 | 169 | return ChildUnion; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XDR, for Javascript 2 | 3 | Read/write XDR encoded data structures (RFC 4506) 4 | 5 | [![Build Status](https://travis-ci.com/stellar/js-xdr.svg?branch=master)](https://travis-ci.com/stellar/js-xdr) 6 | [![Code Climate](https://codeclimate.com/github/stellar/js-xdr/badges/gpa.svg)](https://codeclimate.com/github/stellar/js-xdr) 7 | [![Dependency Status](https://david-dm.org/stellar/js-xdr.svg)](https://david-dm.org/stellar/js-xdr) 8 | [![devDependency Status](https://david-dm.org/stellar/js-xdr/dev-status.svg)](https://david-dm.org/stellar/js-xdr#info=devDependencies) 9 | 10 | XDR is an open data format, specified in 11 | [RFC 4506](http://tools.ietf.org/html/rfc4506.html). This library provides a way 12 | to read and write XDR data from javascript. It can read/write all of the 13 | primitive XDR types and also provides facilities to define readers for the 14 | compound XDR types (enums, structs and unions) 15 | 16 | ## Installation 17 | 18 | via npm: 19 | 20 | ```shell 21 | npm install --save @stellar/js-xdr 22 | ``` 23 | 24 | ## Usage 25 | 26 | You can find some [examples here](examples/). 27 | 28 | First, let's import the library: 29 | 30 | ```javascript 31 | var xdr = require('@stellar/js-xdr'); 32 | // or 33 | import xdr from '@stellar/js-xdr'; 34 | ``` 35 | 36 | Now, let's look at how to decode some primitive types: 37 | 38 | ```javascript 39 | // booleans 40 | xdr.Bool.fromXDR([0, 0, 0, 0]); // returns false 41 | xdr.Bool.fromXDR([0, 0, 0, 1]); // returns true 42 | 43 | // the inverse of `fromXDR` is `toXDR`, which returns a Buffer 44 | xdr.Bool.toXDR(true); // returns Buffer.from([0,0,0,1]) 45 | 46 | // XDR ints and unsigned ints can be safely represented as 47 | // a javascript number 48 | 49 | xdr.Int.fromXDR([0xff, 0xff, 0xff, 0xff]); // returns -1 50 | xdr.UnsignedInt.fromXDR([0xff, 0xff, 0xff, 0xff]); // returns 4294967295 51 | 52 | // XDR Hypers, however, cannot be safely represented in the 53-bits 53 | // of precision we get with a JavaScript `Number`, so we allow creation from big-endian arrays of numbers, strings, or bigints. 54 | var result = xdr.Hyper.fromXDR([0, 0, 0, 0, 0, 0, 0, 0]); // returns an instance of xdr.Hyper 55 | result = new xdr.Hyper(0); // equivalent 56 | 57 | // convert the hyper to a string 58 | result.toString(); // return '0' 59 | 60 | // math! 61 | var ten = result.toBigInt() + 10; 62 | var minusone = result.toBigInt() - 1; 63 | 64 | // construct a number from a string 65 | var big = xdr.Hyper.fromString('1099511627776'); 66 | 67 | // encode the hyper back into xdr 68 | big.toXDR(); // 69 | ``` 70 | 71 | ## Caveats 72 | 73 | There are a couple of caveats to be aware of with this library: 74 | 75 | 1. We do not support quadruple precision floating point values. Attempting to 76 | read or write these values will throw errors. 77 | 2. NaN is not handled perfectly for floats and doubles. There are several forms 78 | of NaN as defined by IEEE754 and the browser polyfill for node's Buffer 79 | class seems to handle them poorly. 80 | 81 | ## Code generation 82 | 83 | `js-xdr` by itself does not have any ability to parse XDR IDL files and produce 84 | a parser for your custom data types. Instead, that is the responsibility of 85 | [`xdrgen`](http://github.com/stellar/xdrgen). xdrgen will take your .x files 86 | and produce a javascript file that target this library to allow for your own 87 | custom types. 88 | 89 | See [`stellar-base`](http://github.com/stellar/js-stellar-base) for an example 90 | (check out the src/generated directory) 91 | 92 | ## Contributing 93 | 94 | Please [see CONTRIBUTING.md for details](CONTRIBUTING.md). 95 | 96 | ### To develop and test js-xdr itself 97 | 98 | 1. Clone the repo 99 | 100 | ```shell 101 | git clone https://github.com/stellar/js-xdr.git 102 | ``` 103 | 104 | 2. Install dependencies inside js-xdr folder 105 | 106 | ```shell 107 | cd js-xdr 108 | npm i 109 | ``` 110 | 111 | 3. Install Node 14 112 | 113 | Because we support the oldest maintenance version of Node, please install and 114 | develop on Node 14 so you don't get surprised when your code works locally but 115 | breaks in CI. 116 | 117 | Here's out to install `nvm` if you haven't: https://github.com/creationix/nvm 118 | 119 | ```shell 120 | nvm install 121 | 122 | # if you've never installed 14.x before you'll want to re-install yarn 123 | npm install -g yarn 124 | ``` 125 | 126 | If you work on several projects that use different Node versions, you might it 127 | helpful to install this automatic version manager: 128 | https://github.com/wbyoung/avn 129 | 130 | 4. Observe the project's code style 131 | 132 | While you're making changes, make sure to run the linter periodically to catch any linting errors (in addition to making sure your text editor supports ESLint) 133 | 134 | ```shell 135 | yarn fmt 136 | ```` 137 | 138 | If you're working on a file not in `src`, limit your code to Node 14! See what's 139 | supported here: https://node.green/ (The reason is that our npm library must 140 | support earlier versions of Node, so the tests need to run on those versions.) 141 | -------------------------------------------------------------------------------- /src/serialization/xdr-writer.js: -------------------------------------------------------------------------------- 1 | const BUFFER_CHUNK = 8192; // 8 KB chunk size increment 2 | 3 | /** 4 | * @internal 5 | */ 6 | export class XdrWriter { 7 | /** 8 | * @param {Buffer|Number} [buffer] - Optional destination buffer 9 | */ 10 | constructor(buffer) { 11 | if (typeof buffer === 'number') { 12 | buffer = Buffer.allocUnsafe(buffer); 13 | } else if (!(buffer instanceof Buffer)) { 14 | buffer = Buffer.allocUnsafe(BUFFER_CHUNK); 15 | } 16 | this._buffer = buffer; 17 | this._length = buffer.length; 18 | } 19 | 20 | /** 21 | * @type {Buffer} 22 | * @private 23 | * @readonly 24 | */ 25 | _buffer; 26 | /** 27 | * @type {Number} 28 | * @private 29 | * @readonly 30 | */ 31 | _length; 32 | /** 33 | * @type {Number} 34 | * @private 35 | * @readonly 36 | */ 37 | _index = 0; 38 | 39 | /** 40 | * Advance writer position, write padding if needed, auto-resize the buffer 41 | * @param {Number} size - Bytes to write 42 | * @return {Number} Position to read from 43 | * @private 44 | */ 45 | alloc(size) { 46 | const from = this._index; 47 | // advance cursor position 48 | this._index += size; 49 | // ensure sufficient buffer size 50 | if (this._length < this._index) { 51 | this.resize(this._index); 52 | } 53 | return from; 54 | } 55 | 56 | /** 57 | * Increase size of the underlying buffer 58 | * @param {Number} minRequiredSize - Minimum required buffer size 59 | * @return {void} 60 | * @private 61 | */ 62 | resize(minRequiredSize) { 63 | // calculate new length, align new buffer length by chunk size 64 | const newLength = Math.ceil(minRequiredSize / BUFFER_CHUNK) * BUFFER_CHUNK; 65 | // create new buffer and copy previous data 66 | const newBuffer = Buffer.allocUnsafe(newLength); 67 | this._buffer.copy(newBuffer, 0, 0, this._length); 68 | // update references 69 | this._buffer = newBuffer; 70 | this._length = newLength; 71 | } 72 | 73 | /** 74 | * Return XDR-serialized value 75 | * @return {Buffer} 76 | */ 77 | finalize() { 78 | // clip underlying buffer to the actually written value 79 | return this._buffer.subarray(0, this._index); 80 | } 81 | 82 | /** 83 | * Return XDR-serialized value as byte array 84 | * @return {Number[]} 85 | */ 86 | toArray() { 87 | return [...this.finalize()]; 88 | } 89 | 90 | /** 91 | * Write byte array from the buffer 92 | * @param {Buffer|String} value - Bytes/string to write 93 | * @param {Number} size - Size in bytes 94 | * @return {XdrReader} - XdrReader wrapper on top of a subarray 95 | */ 96 | write(value, size) { 97 | if (typeof value === 'string') { 98 | // serialize string directly to the output buffer 99 | const offset = this.alloc(size); 100 | this._buffer.write(value, offset, 'utf8'); 101 | } else { 102 | // copy data to the output buffer 103 | if (!(value instanceof Buffer)) { 104 | value = Buffer.from(value); 105 | } 106 | const offset = this.alloc(size); 107 | value.copy(this._buffer, offset, 0, size); 108 | } 109 | 110 | // add padding for 4-byte XDR alignment 111 | const padding = 4 - (size % 4 || 4); 112 | if (padding > 0) { 113 | const offset = this.alloc(padding); 114 | this._buffer.fill(0, offset, this._index); 115 | } 116 | } 117 | 118 | /** 119 | * Write i32 from buffer 120 | * @param {Number} value - Value to serialize 121 | * @return {void} 122 | */ 123 | writeInt32BE(value) { 124 | const offset = this.alloc(4); 125 | this._buffer.writeInt32BE(value, offset); 126 | } 127 | 128 | /** 129 | * Write u32 from buffer 130 | * @param {Number} value - Value to serialize 131 | * @return {void} 132 | */ 133 | writeUInt32BE(value) { 134 | const offset = this.alloc(4); 135 | this._buffer.writeUInt32BE(value, offset); 136 | } 137 | 138 | /** 139 | * Write i64 from buffer 140 | * @param {BigInt} value - Value to serialize 141 | * @return {void} 142 | */ 143 | writeBigInt64BE(value) { 144 | const offset = this.alloc(8); 145 | this._buffer.writeBigInt64BE(value, offset); 146 | } 147 | 148 | /** 149 | * Write u64 from buffer 150 | * @param {BigInt} value - Value to serialize 151 | * @return {void} 152 | */ 153 | writeBigUInt64BE(value) { 154 | const offset = this.alloc(8); 155 | this._buffer.writeBigUInt64BE(value, offset); 156 | } 157 | 158 | /** 159 | * Write float from buffer 160 | * @param {Number} value - Value to serialize 161 | * @return {void} 162 | */ 163 | writeFloatBE(value) { 164 | const offset = this.alloc(4); 165 | this._buffer.writeFloatBE(value, offset); 166 | } 167 | 168 | /** 169 | * Write double from buffer 170 | * @param {Number} value - Value to serialize 171 | * @return {void} 172 | */ 173 | writeDoubleBE(value) { 174 | const offset = this.alloc(8); 175 | this._buffer.writeDoubleBE(value, offset); 176 | } 177 | 178 | static bufferChunkSize = BUFFER_CHUNK; 179 | } 180 | -------------------------------------------------------------------------------- /test/unit/string_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 3 | 4 | let subject = new XDR.String(4); 5 | 6 | describe('String#read', function () { 7 | it('decodes correctly', function () { 8 | expect(read([0x00, 0x00, 0x00, 0x00]).toString('utf8')).to.eql(''); 9 | expect( 10 | read([0x00, 0x00, 0x00, 0x01, 0x41, 0x00, 0x00, 0x00]).toString('utf8') 11 | ).to.eql('A'); 12 | expect( 13 | read([0x00, 0x00, 0x00, 0x03, 0xe4, 0xb8, 0x89, 0x00]).toString('utf8') 14 | ).to.eql('三'); 15 | expect( 16 | read([0x00, 0x00, 0x00, 0x02, 0x41, 0x41, 0x00, 0x00]).toString('utf8') 17 | ).to.eql('AA'); 18 | }); 19 | 20 | it('decodes correctly to string', function () { 21 | expect(readString([0x00, 0x00, 0x00, 0x00])).to.eql(''); 22 | expect(readString([0x00, 0x00, 0x00, 0x01, 0x41, 0x00, 0x00, 0x00])).to.eql( 23 | 'A' 24 | ); 25 | expect(readString([0x00, 0x00, 0x00, 0x03, 0xe4, 0xb8, 0x89, 0x00])).to.eql( 26 | '三' 27 | ); 28 | expect(readString([0x00, 0x00, 0x00, 0x02, 0x41, 0x41, 0x00, 0x00])).to.eql( 29 | 'AA' 30 | ); 31 | }); 32 | 33 | it('decodes non-utf-8 correctly', function () { 34 | let val = read([0x00, 0x00, 0x00, 0x01, 0xd1, 0x00, 0x00, 0x00]); 35 | expect(val[0]).to.eql(0xd1); 36 | }); 37 | 38 | it('throws a read error when the encoded length is greater than the allowed max', function () { 39 | expect(() => 40 | read([0x00, 0x00, 0x00, 0x05, 0x41, 0x41, 0x41, 0x41, 0x41]) 41 | ).to.throw(/read error/i); 42 | }); 43 | 44 | it('throws a read error if the padding bytes are not zero', function () { 45 | expect(() => 46 | read([0x00, 0x00, 0x00, 0x01, 0x41, 0x01, 0x00, 0x00]) 47 | ).to.throw(/read error/i); 48 | expect(() => 49 | read([0x00, 0x00, 0x00, 0x01, 0x41, 0x00, 0x01, 0x00]) 50 | ).to.throw(/read error/i); 51 | expect(() => 52 | read([0x00, 0x00, 0x00, 0x01, 0x41, 0x00, 0x00, 0x01]) 53 | ).to.throw(/read error/i); 54 | }); 55 | 56 | function read(bytes) { 57 | let io = new XdrReader(bytes); 58 | return subject.read(io); 59 | } 60 | 61 | function readString(bytes) { 62 | const io = new XdrReader(bytes); 63 | const res = subject.readString(io); 64 | expect(io._index).to.eql( 65 | !res ? 4 : 8, 66 | 'padding not processed by the reader' 67 | ); 68 | return res; 69 | } 70 | }); 71 | 72 | describe('String#write', function () { 73 | it('encodes string correctly', function () { 74 | expect(write('')).to.eql([0x00, 0x00, 0x00, 0x00]); 75 | expect(write('三')).to.eql([ 76 | 0x00, 0x00, 0x00, 0x03, 0xe4, 0xb8, 0x89, 0x00 77 | ]); 78 | expect(write('A')).to.eql([0x00, 0x00, 0x00, 0x01, 0x41, 0x00, 0x00, 0x00]); 79 | expect(write('AA')).to.eql([ 80 | 0x00, 0x00, 0x00, 0x02, 0x41, 0x41, 0x00, 0x00 81 | ]); 82 | }); 83 | 84 | it('encodes non-utf-8 correctly', function () { 85 | expect(write([0xd1])).to.eql([ 86 | 0x00, 0x00, 0x00, 0x01, 0xd1, 0x00, 0x00, 0x00 87 | ]); 88 | }); 89 | 90 | it('encodes non-utf-8 correctly (buffer)', function () { 91 | expect(write(Buffer.from([0xd1]))).to.eql([ 92 | 0x00, 0x00, 0x00, 0x01, 0xd1, 0x00, 0x00, 0x00 93 | ]); 94 | }); 95 | 96 | it('checks actual utf-8 strings length on write', function () { 97 | expect(() => write('€€€€')).to.throw(/max allowed/i); 98 | }); 99 | 100 | function write(value) { 101 | let io = new XdrWriter(8); 102 | subject.write(value, io); 103 | return io.toArray(); 104 | } 105 | }); 106 | 107 | describe('String#isValid', function () { 108 | it('returns true for strings of the correct length', function () { 109 | expect(subject.isValid('')).to.be.true; 110 | expect(subject.isValid('a')).to.be.true; 111 | expect(subject.isValid('aa')).to.be.true; 112 | }); 113 | 114 | it('returns true for arrays of the correct length', function () { 115 | expect(subject.isValid([0x01])).to.be.true; 116 | }); 117 | 118 | it('returns true for buffers of the correct length', function () { 119 | expect(subject.isValid(Buffer.from([0x01]))).to.be.true; 120 | }); 121 | 122 | it('returns false for strings that are too large', function () { 123 | expect(subject.isValid('aaaaa')).to.be.false; 124 | }); 125 | 126 | it('returns false for arrays that are too large', function () { 127 | expect(subject.isValid([0x01, 0x01, 0x01, 0x01, 0x01])).to.be.false; 128 | }); 129 | 130 | it('returns false for buffers that are too large', function () { 131 | expect(subject.isValid(Buffer.from([0x01, 0x01, 0x01, 0x01, 0x01]))).to.be 132 | .false; 133 | }); 134 | 135 | it('returns false for non string/array/buffer', function () { 136 | expect(subject.isValid(true)).to.be.false; 137 | expect(subject.isValid(null)).to.be.false; 138 | expect(subject.isValid(3)).to.be.false; 139 | }); 140 | }); 141 | 142 | describe('String#constructor', function () { 143 | let subject = new XDR.String(); 144 | 145 | it('defaults to max length of a uint max value', function () { 146 | expect(subject._maxLength).to.eql(Math.pow(2, 32) - 1); 147 | }); 148 | }); 149 | -------------------------------------------------------------------------------- /test/unit/union_test.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from '../../src/serialization/xdr-reader'; 2 | import { XdrWriter } from '../../src/serialization/xdr-writer'; 3 | import { XdrPrimitiveType } from '../../src/xdr-type'; 4 | 5 | /* jshint -W030 */ 6 | 7 | let emptyContext = { definitions: {}, results: {} }; 8 | let ResultType = XDR.Enum.create(emptyContext, 'ResultType', { 9 | ok: 0, 10 | error: 1, 11 | nonsense: 2 12 | }); 13 | 14 | let Result = XDR.Union.create(emptyContext, 'Result', { 15 | switchOn: ResultType, 16 | switches: [ 17 | ['ok', XDR.Void], 18 | ['error', 'code'] 19 | ], 20 | defaultArm: XDR.Void, 21 | arms: { 22 | code: XDR.Int 23 | } 24 | }); 25 | 26 | let Ext = XDR.Union.create(emptyContext, 'Ext', { 27 | switchOn: XDR.Int, 28 | switches: [[0, XDR.Void]] 29 | }); 30 | 31 | describe('Union.armForSwitch', function () { 32 | it('returns the defined arm for the provided switch', function () { 33 | expect(Result.armForSwitch(ResultType.ok())).to.eql(XDR.Void); 34 | expect(Result.armForSwitch(ResultType.error())).to.eql('code'); 35 | }); 36 | 37 | it('returns the default arm if no specific arm is defined', function () { 38 | expect(Result.armForSwitch(ResultType.nonsense())).to.eql(XDR.Void); 39 | }); 40 | 41 | it('works for XDR.Int discriminated unions', function () { 42 | expect(Ext.armForSwitch(0)).to.eql(XDR.Void); 43 | }); 44 | }); 45 | 46 | describe('Union: constructor', function () { 47 | it('works for XDR.Int discriminated unions', function () { 48 | expect(() => new Ext(0)).to.not.throw(); 49 | }); 50 | 51 | it('works for Enum discriminated unions', function () { 52 | expect(() => new Result('ok')).to.not.throw(); 53 | expect(() => new Result(ResultType.ok())).to.not.throw(); 54 | }); 55 | }); 56 | 57 | describe('Union: set', function () { 58 | it('works for XDR.Int discriminated unions', function () { 59 | let u = new Ext(0); 60 | u.set(0); 61 | }); 62 | 63 | it('works for Enum discriminated unions', function () { 64 | let u = Result.ok(); 65 | 66 | expect(() => u.set('ok')).to.not.throw(); 67 | expect(() => u.set('notok')).to.throw(/not a member/); 68 | expect(() => u.set(ResultType.ok())).to.not.throw(); 69 | }); 70 | }); 71 | 72 | describe('Union.read', function () { 73 | it('decodes correctly', function () { 74 | let ok = read([0x00, 0x00, 0x00, 0x00]); 75 | 76 | expect(ok).to.be.instanceof(Result); 77 | expect(ok.switch()).to.eql(ResultType.ok()); 78 | expect(ok.arm()).to.eql(XDR.Void); 79 | expect(ok.armType()).to.eql(XDR.Void); 80 | expect(ok.value()).to.be.undefined; 81 | 82 | let error = read([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05]); 83 | 84 | expect(error).to.be.instanceof(Result); 85 | expect(error.switch()).to.eql(ResultType.error()); 86 | expect(error.arm()).to.eql('code'); 87 | expect(error.armType()).to.eql(XDR.Int); 88 | expect(error.value()).to.eql(5); 89 | expect(error.code()).to.eql(5); 90 | }); 91 | 92 | function read(bytes) { 93 | let io = new XdrReader(bytes); 94 | return Result.read(io); 95 | } 96 | }); 97 | 98 | describe('Union.write', function () { 99 | it('encodes correctly', function () { 100 | let ok = Result.ok(); 101 | 102 | expect(write(ok)).to.eql([0x00, 0x00, 0x00, 0x00]); 103 | 104 | let error = Result.error(5); 105 | 106 | expect(write(error)).to.eql([ 107 | 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x05 108 | ]); 109 | }); 110 | 111 | it('throws a write error if the value is not the correct type', function () { 112 | expect(() => write(null)).to.throw(/write error/i); 113 | expect(() => write(undefined)).to.throw(/write error/i); 114 | expect(() => write([])).to.throw(/write error/i); 115 | expect(() => write({})).to.throw(/write error/i); 116 | expect(() => write(1)).to.throw(/write error/i); 117 | expect(() => write(true)).to.throw(/write error/i); 118 | }); 119 | 120 | function write(value) { 121 | let io = new XdrWriter(256); 122 | Result.write(value, io); 123 | return io.toArray(); 124 | } 125 | }); 126 | 127 | describe('Union.isValid', function () { 128 | it('returns true for instances of the union', function () { 129 | expect(Result.isValid(Result.ok())).to.be.true; 130 | expect(Result.isValid(Result.error(1))).to.be.true; 131 | expect(Result.isValid(Result.nonsense())).to.be.true; 132 | }); 133 | 134 | it('works for "union-like" objects', function () { 135 | class FakeUnion extends XdrPrimitiveType {} 136 | 137 | FakeUnion.unionName = 'Result'; 138 | let r = new FakeUnion(); 139 | expect(Result.isValid(r)).to.be.true; 140 | 141 | FakeUnion.unionName = 'NotResult'; 142 | r = new FakeUnion(); 143 | expect(Result.isValid(r)).to.be.false; 144 | 145 | // make sure you can't fool it 146 | FakeUnion.unionName = undefined; 147 | FakeUnion.structName = 'Result'; 148 | r = new FakeUnion(); 149 | expect(Result.isValid(r)).to.be.false; 150 | }); 151 | 152 | it('returns false for anything else', function () { 153 | expect(Result.isValid(null)).to.be.false; 154 | expect(Result.isValid(undefined)).to.be.false; 155 | expect(Result.isValid([])).to.be.false; 156 | expect(Result.isValid({})).to.be.false; 157 | expect(Result.isValid(1)).to.be.false; 158 | expect(Result.isValid(true)).to.be.false; 159 | expect(Result.isValid('ok')).to.be.false; 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /test/unit/define_test.js: -------------------------------------------------------------------------------- 1 | import * as XDR from '../../src'; 2 | 3 | describe('XDR.config', function () { 4 | beforeEach(function () { 5 | this.types = XDR.config(); // get the xdr object root 6 | for (const toDelete of Object.keys(this.types)) { 7 | delete this.types[toDelete]; 8 | } 9 | }); 10 | 11 | it('can define objects that have no dependency', function () { 12 | XDR.config((xdr) => { 13 | xdr.enum('Color', { 14 | red: 0, 15 | green: 1, 16 | blue: 2 17 | }); 18 | 19 | xdr.enum('ResultType', { 20 | ok: 0, 21 | error: 1 22 | }); 23 | }, this.types); 24 | 25 | expect(this.types.Color).to.exist; 26 | expect(this.types.ResultType).to.exist; 27 | }); 28 | 29 | it('can define objects with the same name from different contexts', function () { 30 | XDR.config((xdr) => { 31 | xdr.enum('Color', { 32 | red: 0, 33 | green: 1, 34 | blue: 2 35 | }); 36 | }); 37 | 38 | XDR.config((xdr) => { 39 | xdr.enum('Color', { 40 | red: 0, 41 | green: 1, 42 | blue: 2 43 | }); 44 | }); 45 | }); 46 | 47 | it('can define objects that have simple dependencies', function () { 48 | XDR.config((xdr) => { 49 | xdr.union('Result', { 50 | switchOn: xdr.lookup('ResultType'), 51 | switches: [ 52 | ['ok', XDR.Void], 53 | ['error', 'message'] 54 | ], 55 | defaultArm: XDR.Void, 56 | arms: { 57 | message: new XDR.String(100) 58 | } 59 | }); 60 | 61 | xdr.enum('ResultType', { 62 | ok: 0, 63 | error: 1 64 | }); 65 | }, this.types); 66 | 67 | expect(this.types.Result).to.exist; 68 | expect(this.types.ResultType).to.exist; 69 | 70 | let result = this.types.Result.ok(); 71 | expect(result.switch()).to.eql(this.types.ResultType.ok()); 72 | 73 | result = this.types.Result.error('It broke!'); 74 | expect(result.switch()).to.eql(this.types.ResultType.error()); 75 | expect(result.message()).to.eql('It broke!'); 76 | }); 77 | 78 | it('can define structs', function () { 79 | XDR.config((xdr) => { 80 | xdr.struct('Color', [ 81 | ['red', xdr.int()], 82 | ['green', xdr.int()], 83 | ['blue', xdr.int()] 84 | ]); 85 | }, this.types); 86 | 87 | expect(this.types.Color).to.exist; 88 | 89 | let result = new this.types.Color({ 90 | red: 0, 91 | green: 1, 92 | blue: 2 93 | }); 94 | expect(result.red()).to.eql(0); 95 | expect(result.green()).to.eql(1); 96 | expect(result.blue()).to.eql(2); 97 | }); 98 | 99 | it('can define typedefs', function () { 100 | let xdr = XDR.config((xdr) => { 101 | xdr.typedef('Uint256', xdr.opaque(32)); 102 | }); 103 | expect(xdr.Uint256).to.be.instanceof(XDR.Opaque); 104 | }); 105 | 106 | it('can define consts', function () { 107 | let xdr = XDR.config((xdr) => { 108 | xdr.typedef('MAX_SIZE', 300); 109 | }); 110 | expect(xdr.MAX_SIZE).to.eql(300); 111 | }); 112 | 113 | it('can define arrays', function () { 114 | let xdr = XDR.config((xdr) => { 115 | xdr.typedef('ArrayOfInts', xdr.array(xdr.int(), 3)); 116 | xdr.struct('MyStruct', [['red', xdr.int()]]); 117 | xdr.typedef('ArrayOfEmpty', xdr.array(xdr.lookup('MyStruct'), 5)); 118 | }); 119 | 120 | expect(xdr.ArrayOfInts).to.be.instanceof(XDR.Array); 121 | expect(xdr.ArrayOfInts._childType).to.eql(XDR.Int); 122 | expect(xdr.ArrayOfInts._length).to.eql(3); 123 | 124 | expect(xdr.ArrayOfEmpty).to.be.instanceof(XDR.Array); 125 | expect(xdr.ArrayOfEmpty._childType).to.eql(xdr.MyStruct); 126 | expect(xdr.ArrayOfEmpty._length).to.eql(5); 127 | }); 128 | 129 | it('can define vararrays', function () { 130 | let xdr = XDR.config((xdr) => { 131 | xdr.typedef('ArrayOfInts', xdr.varArray(xdr.int(), 3)); 132 | }); 133 | 134 | expect(xdr.ArrayOfInts).to.be.instanceof(XDR.VarArray); 135 | expect(xdr.ArrayOfInts._childType).to.eql(XDR.Int); 136 | expect(xdr.ArrayOfInts._maxLength).to.eql(3); 137 | }); 138 | 139 | it('can define options', function () { 140 | let xdr = XDR.config((xdr) => { 141 | xdr.typedef('OptionalInt', xdr.option(xdr.int())); 142 | }); 143 | 144 | expect(xdr.OptionalInt).to.be.instanceof(XDR.Option); 145 | expect(xdr.OptionalInt._childType).to.eql(XDR.Int); 146 | }); 147 | 148 | it('can use sizes defined as an xdr const', function () { 149 | let xdr = XDR.config((xdr) => { 150 | xdr.const('SIZE', 5); 151 | xdr.typedef('MyArray', xdr.array(xdr.int(), xdr.lookup('SIZE'))); 152 | xdr.typedef('MyVarArray', xdr.varArray(xdr.int(), xdr.lookup('SIZE'))); 153 | xdr.typedef('MyString', xdr.string(xdr.lookup('SIZE'))); 154 | xdr.typedef('MyOpaque', xdr.opaque(xdr.lookup('SIZE'))); 155 | xdr.typedef('MyVarOpaque', xdr.varOpaque(xdr.lookup('SIZE'))); 156 | }); 157 | 158 | expect(xdr.MyArray).to.be.instanceof(XDR.Array); 159 | expect(xdr.MyArray._childType).to.eql(XDR.Int); 160 | expect(xdr.MyArray._length).to.eql(5); 161 | 162 | expect(xdr.MyVarArray).to.be.instanceof(XDR.VarArray); 163 | expect(xdr.MyVarArray._childType).to.eql(XDR.Int); 164 | expect(xdr.MyVarArray._maxLength).to.eql(5); 165 | 166 | expect(xdr.MyString).to.be.instanceof(XDR.String); 167 | expect(xdr.MyString._maxLength).to.eql(5); 168 | 169 | expect(xdr.MyOpaque).to.be.instanceof(XDR.Opaque); 170 | expect(xdr.MyOpaque._length).to.eql(5); 171 | 172 | expect(xdr.MyVarOpaque).to.be.instanceof(XDR.VarOpaque); 173 | expect(xdr.MyVarOpaque._maxLength).to.eql(5); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line max-classes-per-file 2 | import * as XDRTypes from './types'; 3 | import { Reference } from './reference'; 4 | import { XdrDefinitionError } from './errors'; 5 | 6 | export * from './reference'; 7 | 8 | class SimpleReference extends Reference { 9 | constructor(name) { 10 | super(); 11 | this.name = name; 12 | } 13 | 14 | resolve(context) { 15 | const defn = context.definitions[this.name]; 16 | return defn.resolve(context); 17 | } 18 | } 19 | 20 | class ArrayReference extends Reference { 21 | constructor(childReference, length, variable = false) { 22 | super(); 23 | this.childReference = childReference; 24 | this.length = length; 25 | this.variable = variable; 26 | } 27 | 28 | resolve(context) { 29 | let resolvedChild = this.childReference; 30 | let length = this.length; 31 | 32 | if (resolvedChild instanceof Reference) { 33 | resolvedChild = resolvedChild.resolve(context); 34 | } 35 | 36 | if (length instanceof Reference) { 37 | length = length.resolve(context); 38 | } 39 | 40 | if (this.variable) { 41 | return new XDRTypes.VarArray(resolvedChild, length); 42 | } 43 | return new XDRTypes.Array(resolvedChild, length); 44 | } 45 | } 46 | 47 | class OptionReference extends Reference { 48 | constructor(childReference) { 49 | super(); 50 | this.childReference = childReference; 51 | this.name = childReference.name; 52 | } 53 | 54 | resolve(context) { 55 | let resolvedChild = this.childReference; 56 | 57 | if (resolvedChild instanceof Reference) { 58 | resolvedChild = resolvedChild.resolve(context); 59 | } 60 | 61 | return new XDRTypes.Option(resolvedChild); 62 | } 63 | } 64 | 65 | class SizedReference extends Reference { 66 | constructor(sizedType, length) { 67 | super(); 68 | this.sizedType = sizedType; 69 | this.length = length; 70 | } 71 | 72 | resolve(context) { 73 | let length = this.length; 74 | 75 | if (length instanceof Reference) { 76 | length = length.resolve(context); 77 | } 78 | 79 | return new this.sizedType(length); 80 | } 81 | } 82 | 83 | class Definition { 84 | constructor(constructor, name, cfg) { 85 | this.constructor = constructor; 86 | this.name = name; 87 | this.config = cfg; 88 | } 89 | 90 | // resolve calls the constructor of this definition with the provided context 91 | // and this definitions config values. The definitions constructor should 92 | // populate the final type on `context.results`, and may refer to other 93 | // definitions through `context.definitions` 94 | resolve(context) { 95 | if (this.name in context.results) { 96 | return context.results[this.name]; 97 | } 98 | 99 | return this.constructor(context, this.name, this.config); 100 | } 101 | } 102 | 103 | // let the reference resolution system do its thing 104 | // the "constructor" for a typedef just returns the resolved value 105 | function createTypedef(context, typeName, value) { 106 | if (value instanceof Reference) { 107 | value = value.resolve(context); 108 | } 109 | context.results[typeName] = value; 110 | return value; 111 | } 112 | 113 | function createConst(context, name, value) { 114 | context.results[name] = value; 115 | return value; 116 | } 117 | 118 | class TypeBuilder { 119 | constructor(destination) { 120 | this._destination = destination; 121 | this._definitions = {}; 122 | } 123 | 124 | enum(name, members) { 125 | const result = new Definition(XDRTypes.Enum.create, name, members); 126 | this.define(name, result); 127 | } 128 | 129 | struct(name, members) { 130 | const result = new Definition(XDRTypes.Struct.create, name, members); 131 | this.define(name, result); 132 | } 133 | 134 | union(name, cfg) { 135 | const result = new Definition(XDRTypes.Union.create, name, cfg); 136 | this.define(name, result); 137 | } 138 | 139 | typedef(name, cfg) { 140 | const result = new Definition(createTypedef, name, cfg); 141 | this.define(name, result); 142 | } 143 | 144 | const(name, cfg) { 145 | const result = new Definition(createConst, name, cfg); 146 | this.define(name, result); 147 | } 148 | 149 | void() { 150 | return XDRTypes.Void; 151 | } 152 | 153 | bool() { 154 | return XDRTypes.Bool; 155 | } 156 | 157 | int() { 158 | return XDRTypes.Int; 159 | } 160 | 161 | hyper() { 162 | return XDRTypes.Hyper; 163 | } 164 | 165 | uint() { 166 | return XDRTypes.UnsignedInt; 167 | } 168 | 169 | uhyper() { 170 | return XDRTypes.UnsignedHyper; 171 | } 172 | 173 | float() { 174 | return XDRTypes.Float; 175 | } 176 | 177 | double() { 178 | return XDRTypes.Double; 179 | } 180 | 181 | quadruple() { 182 | return XDRTypes.Quadruple; 183 | } 184 | 185 | string(length) { 186 | return new SizedReference(XDRTypes.String, length); 187 | } 188 | 189 | opaque(length) { 190 | return new SizedReference(XDRTypes.Opaque, length); 191 | } 192 | 193 | varOpaque(length) { 194 | return new SizedReference(XDRTypes.VarOpaque, length); 195 | } 196 | 197 | array(childType, length) { 198 | return new ArrayReference(childType, length); 199 | } 200 | 201 | varArray(childType, maxLength) { 202 | return new ArrayReference(childType, maxLength, true); 203 | } 204 | 205 | option(childType) { 206 | return new OptionReference(childType); 207 | } 208 | 209 | define(name, definition) { 210 | if (this._destination[name] === undefined) { 211 | this._definitions[name] = definition; 212 | } else { 213 | throw new XdrDefinitionError(`${name} is already defined`); 214 | } 215 | } 216 | 217 | lookup(name) { 218 | return new SimpleReference(name); 219 | } 220 | 221 | resolve() { 222 | for (const defn of Object.values(this._definitions)) { 223 | defn.resolve({ 224 | definitions: this._definitions, 225 | results: this._destination 226 | }); 227 | } 228 | } 229 | } 230 | 231 | export function config(fn, types = {}) { 232 | if (fn) { 233 | const builder = new TypeBuilder(types); 234 | fn(builder); 235 | builder.resolve(); 236 | } 237 | 238 | return types; 239 | } 240 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. This 4 | project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## Unreleased 7 | 8 | ### Fixed 9 | * Removed [the custom `Buffer.subarray` polyfill](https://github.com/stellar/js-xdr/pull/118) introduced in v3.1.1 to address the issue of it not being usable in the React Native Hermes engine. We recommend using [`@exodus/patch-broken-hermes-typed-arrays`](https://github.com/ExodusMovement/patch-broken-hermes-typed-arrays) as an alternative. If needed, please review and consider manually adding it to your project ([#128](https://github.com/stellar/js-xdr/pull/128)). 10 | 11 | ## [v3.1.2](https://github.com/stellar/js-xdr/compare/v3.1.1...v3.1.2) 12 | 13 | ### Fixed 14 | * Increase robustness of compatibility across multiple `js-xdr` instances in an environment ([#122](https://github.com/stellar/js-xdr/pull/122)). 15 | 16 | 17 | ## [v3.1.1](https://github.com/stellar/js-xdr/compare/v3.1.0...v3.1.1) 18 | 19 | ### Fixed 20 | * Add compatibility with pre-ES2016 environments (like some React Native JS compilers) by adding a custom `Buffer.subarray` polyfill ([#118](https://github.com/stellar/js-xdr/pull/118)). 21 | 22 | 23 | ## [v3.1.0](https://github.com/stellar/js-xdr/compare/v3.0.1...v3.1.0) 24 | 25 | ### Added 26 | * The raw, underlying `XdrReader` and `XdrWriter` types are now exposed by the library for reading without consuming the entire stream ([#116](https://github.com/stellar/js-xdr/pull/116)). 27 | 28 | ### Fixed 29 | * Added additional type checks for passing a bytearray-like object to `XdrReader`s and improves the error with details ([#116](https://github.com/stellar/js-xdr/pull/116)). 30 | 31 | 32 | ## [v3.0.1](https://github.com/stellar/js-xdr/compare/v3.0.0...v3.0.1) 33 | 34 | ### Fixes 35 | - This package is now being published to `@stellar/js-xdr` on NPM. 36 | - The versions at `js-xdr` are now considered **deprecated** ([#111](https://github.com/stellar/js-xdr/pull/111)). 37 | - Misc. dependencies have been upgraded ([#104](https://github.com/stellar/js-xdr/pull/104), [#106](https://github.com/stellar/js-xdr/pull/106), [#107](https://github.com/stellar/js-xdr/pull/107), [#108](https://github.com/stellar/js-xdr/pull/108), [#105](https://github.com/stellar/js-xdr/pull/105)). 38 | 39 | 40 | ## [v3.0.0](https://github.com/stellar/js-xdr/compare/v2.0.0...v3.0.0) 41 | 42 | ### Breaking Change 43 | - Add support for easily encoding integers larger than 32 bits ([#100](https://github.com/stellar/js-xdr/pull/100)). This (partially) breaks the API for creating `Hyper` and `UnsignedHyper` instances. Previously, you would pass `low` and `high` parts to represent the lower and upper 32 bits. Now, you can pass the entire 64-bit value directly as a `bigint` or `string` instance, or as a list of "chunks" like before, e.g.: 44 | 45 | ```diff 46 | -new Hyper({ low: 1, high: 1 }); // representing (1 << 32) + 1 = 4294967297n 47 | +new Hyper(4294967297n); 48 | +new Hyper("4294967297"); 49 | +new Hyper(1, 1); 50 | ``` 51 | 52 | 53 | ## [v2.0.0](https://github.com/stellar/js-xdr/compare/v1.3.0...v2.0.0) 54 | 55 | - Refactor XDR serialization/deserialization logic ([#91](https://github.com/stellar/js-xdr/pull/91)). 56 | - Replace `long` dependency with native `BigInt` arithmetics. 57 | - Replace `lodash` dependency with built-in Array and Object methods, iterators. 58 | - Add `buffer` dependency for WebPack browser polyfill. 59 | - Update devDependencies to more recent versions, modernize bundler pipeline. 60 | - Automatically grow underlying buffer on writes (#84 fixed). 61 | - Always check that the entire read buffer is consumed (#32 fixed). 62 | - Check actual byte size of the string on write (#33 fixed). 63 | - Fix babel-polyfill build warnings (#34 fixed). 64 | - Upgrade dependencies to their latest versions ([#92](https://github.com/stellar/js-xdr/pull/92)). 65 | 66 | ## [v1.3.0](https://github.com/stellar/js-xdr/compare/v1.2.0...v1.3.0) 67 | 68 | - Inline and modernize the `cursor` dependency ([#](https://github.com/stellar/js-xdr/pull/63)). 69 | 70 | ## [v1.2.0](https://github.com/stellar/js-xdr/compare/v1.1.4...v1.2.0) 71 | 72 | - Add method `validateXDR(input, format = 'raw')` which validates if a given XDR is valid or not. ([#56](https://github.com/stellar/js-xdr/pull/56)). 73 | 74 | ## [v1.1.4](https://github.com/stellar/js-xdr/compare/v1.1.3...v1.1.4) 75 | 76 | - Remove `core-js` dependency ([#45](https://github.com/stellar/js-xdr/pull/45)). 77 | 78 | ## [v1.1.3](https://github.com/stellar/js-xdr/compare/v1.1.2...v1.1.3) 79 | 80 | - Split out reference class to it's own file to avoid circular import ([#39](https://github.com/stellar/js-xdr/pull/39)). 81 | 82 | ## [v1.1.2](https://github.com/stellar/js-xdr/compare/v1.1.1...v1.1.2) 83 | 84 | - Travis: Deploy to NPM with an env variable instead of an encrypted key 85 | - Instruct Travis to cache node_modules 86 | 87 | ## [v1.1.1](https://github.com/stellar/js-xdr/compare/v1.1.0...v1.1.1) 88 | 89 | - Updated some out-of-date dependencies 90 | 91 | ## [v1.1.0](https://github.com/stellar/js-xdr/compare/v1.0.3...v1.1.0) 92 | 93 | ### Changed 94 | 95 | - Added ESLint and Prettier to enforce code style 96 | - Upgraded dependencies, including Babel to 6 97 | - Bump local node version to 6.14.0 98 | 99 | ## [v1.0.3](https://github.com/stellar/js-xdr/compare/v1.0.2...v1.0.3) 100 | 101 | ### Changed 102 | 103 | - Updated dependencies 104 | - Improved lodash imports (the browser build should be smaller) 105 | 106 | ## [v1.0.2](https://github.com/stellar/js-xdr/compare/v1.0.1...v1.0.2) 107 | 108 | ### Changed 109 | 110 | - bugfix: removed `runtime` flag from babel to make this package working in 111 | React/Webpack environments 112 | 113 | ## [v1.0.1](https://github.com/stellar/js-xdr/compare/v1.0.0...v1.0.1) 114 | 115 | ### Changed 116 | 117 | - bugfix: padding bytes are now ensured to be zero when reading 118 | 119 | ## [v1.0.0](https://github.com/stellar/js-xdr/compare/v0.0.12...v1.0.0) 120 | 121 | ### Changed 122 | 123 | - Strings are now encoded/decoded as utf-8 124 | 125 | ## [v0.0.12](https://github.com/stellar/js-xdr/compare/v0.0.11...v0.0.12) 126 | 127 | ### Changed 128 | 129 | - bugfix: Hyper.fromString() no longer silently accepts strings with decimal 130 | points 131 | - bugfix: UnsignedHyper.fromString() no longer silently accepts strings with 132 | decimal points 133 | -------------------------------------------------------------------------------- /src/xdr-type.js: -------------------------------------------------------------------------------- 1 | import { XdrReader } from './serialization/xdr-reader'; 2 | import { XdrWriter } from './serialization/xdr-writer'; 3 | import { XdrNotImplementedDefinitionError } from './errors'; 4 | 5 | class XdrType { 6 | /** 7 | * Encode value to XDR format 8 | * @param {XdrEncodingFormat} [format] - Encoding format (one of "raw", "hex", "base64") 9 | * @return {String|Buffer} 10 | */ 11 | toXDR(format = 'raw') { 12 | if (!this.write) return this.constructor.toXDR(this, format); 13 | 14 | const writer = new XdrWriter(); 15 | this.write(this, writer); 16 | return encodeResult(writer.finalize(), format); 17 | } 18 | 19 | /** 20 | * Decode XDR-encoded value 21 | * @param {Buffer|String} input - XDR-encoded input data 22 | * @param {XdrEncodingFormat} [format] - Encoding format (one of "raw", "hex", "base64") 23 | * @return {this} 24 | */ 25 | fromXDR(input, format = 'raw') { 26 | if (!this.read) return this.constructor.fromXDR(input, format); 27 | 28 | const reader = new XdrReader(decodeInput(input, format)); 29 | const result = this.read(reader); 30 | reader.ensureInputConsumed(); 31 | return result; 32 | } 33 | 34 | /** 35 | * Check whether input contains a valid XDR-encoded value 36 | * @param {Buffer|String} input - XDR-encoded input data 37 | * @param {XdrEncodingFormat} [format] - Encoding format (one of "raw", "hex", "base64") 38 | * @return {Boolean} 39 | */ 40 | validateXDR(input, format = 'raw') { 41 | try { 42 | this.fromXDR(input, format); 43 | return true; 44 | } catch (e) { 45 | return false; 46 | } 47 | } 48 | 49 | /** 50 | * Encode value to XDR format 51 | * @param {this} value - Value to serialize 52 | * @param {XdrEncodingFormat} [format] - Encoding format (one of "raw", "hex", "base64") 53 | * @return {Buffer} 54 | */ 55 | static toXDR(value, format = 'raw') { 56 | const writer = new XdrWriter(); 57 | this.write(value, writer); 58 | return encodeResult(writer.finalize(), format); 59 | } 60 | 61 | /** 62 | * Decode XDR-encoded value 63 | * @param {Buffer|String} input - XDR-encoded input data 64 | * @param {XdrEncodingFormat} [format] - Encoding format (one of "raw", "hex", "base64") 65 | * @return {this} 66 | */ 67 | static fromXDR(input, format = 'raw') { 68 | const reader = new XdrReader(decodeInput(input, format)); 69 | const result = this.read(reader); 70 | reader.ensureInputConsumed(); 71 | return result; 72 | } 73 | 74 | /** 75 | * Check whether input contains a valid XDR-encoded value 76 | * @param {Buffer|String} input - XDR-encoded input data 77 | * @param {XdrEncodingFormat} [format] - Encoding format (one of "raw", "hex", "base64") 78 | * @return {Boolean} 79 | */ 80 | static validateXDR(input, format = 'raw') { 81 | try { 82 | this.fromXDR(input, format); 83 | return true; 84 | } catch (e) { 85 | return false; 86 | } 87 | } 88 | } 89 | 90 | export class XdrPrimitiveType extends XdrType { 91 | /** 92 | * Read value from the XDR-serialized input 93 | * @param {XdrReader} reader - XdrReader instance 94 | * @return {this} 95 | * @abstract 96 | */ 97 | // eslint-disable-next-line no-unused-vars 98 | static read(reader) { 99 | throw new XdrNotImplementedDefinitionError(); 100 | } 101 | 102 | /** 103 | * Write XDR value to the buffer 104 | * @param {this} value - Value to write 105 | * @param {XdrWriter} writer - XdrWriter instance 106 | * @return {void} 107 | * @abstract 108 | */ 109 | // eslint-disable-next-line no-unused-vars 110 | static write(value, writer) { 111 | throw new XdrNotImplementedDefinitionError(); 112 | } 113 | 114 | /** 115 | * Check whether XDR primitive value is valid 116 | * @param {this} value - Value to check 117 | * @return {Boolean} 118 | * @abstract 119 | */ 120 | // eslint-disable-next-line no-unused-vars 121 | static isValid(value) { 122 | return false; 123 | } 124 | } 125 | 126 | export class XdrCompositeType extends XdrType { 127 | // Every descendant should implement two methods: read(reader) and write(value, writer) 128 | 129 | /** 130 | * Check whether XDR primitive value is valid 131 | * @param {this} value - Value to check 132 | * @return {Boolean} 133 | * @abstract 134 | */ 135 | // eslint-disable-next-line no-unused-vars 136 | isValid(value) { 137 | return false; 138 | } 139 | } 140 | 141 | class InvalidXdrEncodingFormatError extends TypeError { 142 | constructor(format) { 143 | super(`Invalid format ${format}, must be one of "raw", "hex", "base64"`); 144 | } 145 | } 146 | 147 | function encodeResult(buffer, format) { 148 | switch (format) { 149 | case 'raw': 150 | return buffer; 151 | case 'hex': 152 | return buffer.toString('hex'); 153 | case 'base64': 154 | return buffer.toString('base64'); 155 | default: 156 | throw new InvalidXdrEncodingFormatError(format); 157 | } 158 | } 159 | 160 | function decodeInput(input, format) { 161 | switch (format) { 162 | case 'raw': 163 | return input; 164 | case 'hex': 165 | return Buffer.from(input, 'hex'); 166 | case 'base64': 167 | return Buffer.from(input, 'base64'); 168 | default: 169 | throw new InvalidXdrEncodingFormatError(format); 170 | } 171 | } 172 | 173 | /** 174 | * Provides a "duck typed" version of the native `instanceof` for read/write. 175 | * 176 | * "Duck typing" means if the parameter _looks like_ and _acts like_ a duck 177 | * (i.e. the type we're checking), it will be treated as that type. 178 | * 179 | * In this case, the "type" we're looking for is "like XdrType" but also "like 180 | * XdrCompositeType|XdrPrimitiveType" (i.e. serializable), but also conditioned 181 | * on a particular subclass of "XdrType" (e.g. {@link Union} which extends 182 | * XdrType). 183 | * 184 | * This makes the package resilient to downstream systems that may be combining 185 | * many versions of a package across its stack that are technically compatible 186 | * but fail `instanceof` checks due to cross-pollination. 187 | */ 188 | export function isSerializableIsh(value, subtype) { 189 | return ( 190 | value !== undefined && 191 | value !== null && // prereqs, otherwise `getPrototypeOf` pops 192 | (value instanceof subtype || // quickest check 193 | // Do an initial constructor check (anywhere is fine so that children of 194 | // `subtype` still work), then 195 | (hasConstructor(value, subtype) && 196 | // ensure it has read/write methods, then 197 | typeof value.constructor.read === 'function' && 198 | typeof value.constructor.write === 'function' && 199 | // ensure XdrType is in the prototype chain 200 | hasConstructor(value, 'XdrType'))) 201 | ); 202 | } 203 | 204 | /** Tries to find `subtype` in any of the constructors or meta of `instance`. */ 205 | export function hasConstructor(instance, subtype) { 206 | do { 207 | const ctor = instance.constructor; 208 | if (ctor.name === subtype) { 209 | return true; 210 | } 211 | } while ((instance = Object.getPrototypeOf(instance))); 212 | return false; 213 | } 214 | 215 | /** 216 | * @typedef {'raw'|'hex'|'base64'} XdrEncodingFormat 217 | */ 218 | -------------------------------------------------------------------------------- /test/unit/bigint-encoder_test.js: -------------------------------------------------------------------------------- 1 | import { 2 | encodeBigIntFromBits, 3 | formatIntName, 4 | sliceBigInt 5 | } from '../../src/bigint-encoder'; 6 | 7 | describe('encodeBigIntWithPrecision', function () { 8 | it(`encodes values correctly`, () => { 9 | const testCases = [ 10 | // i64 11 | [[0], 64, false, 0n], 12 | [[-1], 64, false, -1n], 13 | [['-15258'], 64, false, -15258n], 14 | [[-0x8000000000000000n], 64, false, -0x8000000000000000n], 15 | [[0x7fffffffffffffffn], 64, false, 0x7fffffffffffffffn], 16 | [[1, -0x80000000n], 64, false, -0x7fffffffffffffffn], 17 | [[-1, -1], 64, false, -1n], 18 | [[-2, 0x7fffffffn], 64, false, 0x7ffffffffffffffen], 19 | [[345, -345], 64, false, -0x158fffffea7n], 20 | // u64 21 | [[0], 64, true, 0n], 22 | [[1n], 64, true, 1n], 23 | [[0xffffffffffffffffn], 64, true, 0xffffffffffffffffn], 24 | [[0n, 0n], 64, true, 0n], 25 | [[1, 0], 64, true, 1n], 26 | [[-1, -1], 64, true, 0xffffffffffffffffn], 27 | [[-2, -1], 64, true, 0xfffffffffffffffen], 28 | // i128 29 | [[0], 128, false, 0n], 30 | [[-1], 128, false, -1n], 31 | [['-15258'], 128, false, -15258n], 32 | [ 33 | [-0x80000000000000000000000000000000n], 34 | 128, 35 | false, 36 | -0x80000000000000000000000000000000n 37 | ], 38 | [ 39 | [0x7fffffffffffffffffffffffffffffffn], 40 | 128, 41 | false, 42 | 0x7fffffffffffffffffffffffffffffffn 43 | ], 44 | [[1, -2147483648], 128, false, -0x7fffffffffffffffffffffffn], 45 | [[-1, -1], 128, false, -1n], 46 | [ 47 | [-1, 0x7fffffffffffffffn], 48 | 128, 49 | false, 50 | 0x7fffffffffffffffffffffffffffffffn 51 | ], 52 | [ 53 | [0xffffffffffffffffn, 0x7fffffffffffffffn], 54 | 128, 55 | false, 56 | 0x7fffffffffffffffffffffffffffffffn 57 | ], 58 | [ 59 | [0, -0x8000000000000000n], 60 | 128, 61 | false, 62 | -0x80000000000000000000000000000000n 63 | ], 64 | [ 65 | [1, -0x8000000000000000n], 66 | 128, 67 | false, 68 | -0x7fffffffffffffffffffffffffffffffn 69 | ], 70 | [ 71 | [1, 0, 0, -0x80000000n], 72 | 128, 73 | false, 74 | -0x7fffffffffffffffffffffffffffffffn 75 | ], 76 | [[345, 345n, '345', 0x159], 128, false, 0x159000001590000015900000159n], 77 | // u128 78 | [[0], 128, true, 0n], 79 | [[1n], 128, true, 1n], 80 | [ 81 | [0xffffffffffffffffffffffffffffffffn], 82 | 128, 83 | true, 84 | 0xffffffffffffffffffffffffffffffffn 85 | ], 86 | [[0n, 0n], 128, true, 0n], 87 | [[1, 0], 128, true, 1n], 88 | [[-1, -1], 128, true, 0xffffffffffffffffffffffffffffffffn], 89 | [[-2, -1], 128, true, 0xfffffffffffffffffffffffffffffffen], 90 | [ 91 | [0x5cffffffffffffffn, 0x7fffffffffffffffn], 92 | 128, 93 | true, 94 | 0x7fffffffffffffff5cffffffffffffffn 95 | ], 96 | [ 97 | [1, 1, -1, -0x80000000n], 98 | 128, 99 | true, 100 | 0x80000000ffffffff0000000100000001n 101 | ], 102 | [[345, 345n, '345', 0x159], 128, false, 0x159000001590000015900000159n], 103 | // i256 104 | [[0], 256, false, 0n], 105 | [[-1], 256, false, -1n], 106 | [['-15258'], 256, false, -15258n], 107 | [ 108 | [-0x8000000000000000000000000000000000000000000000000000000000000000n], 109 | 256, 110 | false, 111 | -0x8000000000000000000000000000000000000000000000000000000000000000n 112 | ], 113 | [ 114 | [0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn], 115 | 256, 116 | false, 117 | 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn 118 | ], 119 | [ 120 | [1, -2147483648], 121 | 256, 122 | false, 123 | -0x7fffffffffffffffffffffffffffffffffffffffn 124 | ], 125 | [[-1, -1], 256, false, -1n], 126 | [ 127 | [-1, 0x7fffffffffffffffn], 128 | 256, 129 | false, 130 | 0x7fffffffffffffffffffffffffffffffffffffffffffffffn 131 | ], 132 | [ 133 | [ 134 | 0xffffffffffffffffffffffffffffffffn, 135 | 0x7fffffffffffffffffffffffffffffffn 136 | ], 137 | 256, 138 | false, 139 | 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn 140 | ], 141 | [ 142 | [0, -0x80000000000000000000000000000000n], 143 | 256, 144 | false, 145 | -0x8000000000000000000000000000000000000000000000000000000000000000n 146 | ], 147 | [ 148 | [1, -0x80000000000000000000000000000000n], 149 | 256, 150 | false, 151 | -0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn 152 | ], 153 | [ 154 | [1, 0, 0, -0x800000000000000n], 155 | 256, 156 | false, 157 | -0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn 158 | ], 159 | [ 160 | [345, 345n, '345', -0x159], 161 | 256, 162 | false, 163 | -0x158fffffffffffffea6fffffffffffffea6fffffffffffffea7n 164 | ], 165 | [ 166 | [1, 2, 3, 4, 5, 6, 7, -8], 167 | 256, 168 | false, 169 | -0x7fffffff8fffffff9fffffffafffffffbfffffffcfffffffdffffffffn 170 | ], 171 | [ 172 | [1, -2, 3, -4, 5, -6, 7, -8], 173 | 256, 174 | false, 175 | -0x7fffffff800000005fffffffa00000003fffffffc00000001ffffffffn 176 | ], 177 | // u256 178 | [[0], 256, true, 0n], 179 | [[1n], 256, true, 1n], 180 | [ 181 | [0xffffffffffffffffffffffffffffffffn], 182 | 256, 183 | true, 184 | 0xffffffffffffffffffffffffffffffffn 185 | ], 186 | [[0n, 0n], 256, true, 0n], 187 | [[1, 0], 256, true, 1n], 188 | [ 189 | [-1, -1], 190 | 256, 191 | true, 192 | 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffn 193 | ], 194 | [ 195 | [-2, -1], 196 | 256, 197 | true, 198 | 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffen 199 | ], 200 | [ 201 | [ 202 | 0x5cffffffffffffffffffffffffffffffn, 203 | 0x7fffffffffffffffffffffffffffffffn 204 | ], 205 | 256, 206 | true, 207 | 0x7fffffffffffffffffffffffffffffff5cffffffffffffffffffffffffffffffn 208 | ], 209 | [ 210 | [1, 1, -1, -0x80000000n], 211 | 256, 212 | true, 213 | 0xffffffff80000000ffffffffffffffff00000000000000010000000000000001n 214 | ], 215 | [ 216 | [ 217 | 1558245471070191615n, 218 | 1558245471070191615n, 219 | '1558245471070191615', 220 | 0x159fffffffffffffn 221 | ], 222 | 256, 223 | false, 224 | 0x159fffffffffffff159fffffffffffff159fffffffffffff159fffffffffffffn 225 | ], 226 | [ 227 | [1, 2, 3, 4, 5, 6, 7, 8], 228 | 256, 229 | false, 230 | 0x0000000800000007000000060000000500000004000000030000000200000001n 231 | ] 232 | ]; 233 | 234 | for (let [args, bits, unsigned, expected] of testCases) { 235 | try { 236 | const actual = encodeBigIntFromBits(args, bits, unsigned); 237 | expect(actual).to.eq( 238 | expected, 239 | `bigint values for ${formatIntName( 240 | bits, 241 | unsigned 242 | )} out of range: [${args.join()}]` 243 | ); 244 | } catch (e) { 245 | e.message = `Encoding [${args.join()}] => ${formatIntName( 246 | bits, 247 | unsigned 248 | )} BigInt failed with error: ${e.message}`; 249 | throw e; 250 | } 251 | } 252 | }); 253 | }); 254 | 255 | describe('sliceBigInt', function () { 256 | it(`slices values correctly`, () => { 257 | const testCases = [ 258 | [0n, 64, 64, [0n]], 259 | [0n, 256, 256, [0n]], 260 | [-1n, 64, 32, [-1n, -1n]], 261 | [0xfffffffffffffffen, 64, 32, [-2n, -1n]], 262 | [ 263 | 0x7fffffffffffffff5cffffffffffffffn, 264 | 128, 265 | 64, 266 | [0x5cffffffffffffffn, 0x7fffffffffffffffn] 267 | ], 268 | [ 269 | 0x80000000ffffffff0000000100000001n, 270 | 128, 271 | 32, 272 | [1n, 1n, -1n, -0x80000000n] 273 | ], 274 | [ 275 | -0x158fffffffffffffea6fffffffffffffea6fffffffffffffea7n, 276 | 256, 277 | 64, 278 | [345n, 345n, 345n, -345n] 279 | ], 280 | [ 281 | 0x0000000800000007000000060000000500000004000000030000000200000001n, 282 | 256, 283 | 32, 284 | [1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n] 285 | ], 286 | [ 287 | -0x7fffffff8fffffff9fffffffafffffffbfffffffcfffffffdffffffffn, 288 | 256, 289 | 32, 290 | [1n, 2n, 3n, 4n, 5n, 6n, 7n, -8n] 291 | ], 292 | [ 293 | -0x7fffffff800000005fffffffa00000003fffffffc00000001ffffffffn, 294 | 256, 295 | 32, 296 | [1n, -2n, 3n, -4n, 5n, -6n, 7n, -8n] 297 | ] 298 | ]; 299 | for (let [value, size, sliceSize, expected] of testCases) { 300 | try { 301 | const actual = sliceBigInt(value, size, sliceSize); 302 | expect(actual).to.eql( 303 | expected, 304 | `Invalid ${formatIntName( 305 | size, 306 | false 307 | )} / ${sliceSize} slicing result for ${value}` 308 | ); 309 | } catch (e) { 310 | e.message = `Slicing ${value} for ${formatIntName( 311 | size, 312 | false 313 | )} / ${sliceSize} failed with error: ${e.message}`; 314 | throw e; 315 | } 316 | } 317 | }); 318 | }); 319 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2015 Stellar Development Foundation 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. --------------------------------------------------------------------------------