Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 51 additions & 16 deletions CHANGELOG.md
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file looks like it has changed and been reformatted which seems unrelated to the issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm prettier must have done that I can revert the fmting changes

Original file line number Diff line number Diff line change
Expand Up @@ -6,41 +6,71 @@ project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased

### Fixed
* 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)).

- 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)).

- Decoding Array and VarArray now fast fails when the array length exceeds
remaining bytes to decode ([#132](https://github.com/stellar/js-xdr/pull/132))

## [v3.1.2](https://github.com/stellar/js-xdr/compare/v3.1.1...v3.1.2)

### Fixed
* Increase robustness of compatibility across multiple `js-xdr` instances in an environment ([#122](https://github.com/stellar/js-xdr/pull/122)).

- Increase robustness of compatibility across multiple `js-xdr` instances in an
environment ([#122](https://github.com/stellar/js-xdr/pull/122)).

## [v3.1.1](https://github.com/stellar/js-xdr/compare/v3.1.0...v3.1.1)

### Fixed
* 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)).

- 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)).

## [v3.1.0](https://github.com/stellar/js-xdr/compare/v3.0.1...v3.1.0)

### Added
* 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)).

- 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)).

### Fixed
* 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)).

- 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)).

## [v3.0.1](https://github.com/stellar/js-xdr/compare/v3.0.0...v3.0.1)

### Fixes
- This package is now being published to `@stellar/js-xdr` on NPM.
- The versions at `js-xdr` are now considered **deprecated** ([#111](https://github.com/stellar/js-xdr/pull/111)).
- 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)).

- This package is now being published to `@stellar/js-xdr` on NPM.
- The versions at `js-xdr` are now considered **deprecated**
([#111](https://github.com/stellar/js-xdr/pull/111)).
- 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)).

## [v3.0.0](https://github.com/stellar/js-xdr/compare/v2.0.0...v3.0.0)

### Breaking Change
- 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.:

- 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.:

```diff
-new Hyper({ low: 1, high: 1 }); // representing (1 << 32) + 1 = 4294967297n
Expand All @@ -49,10 +79,10 @@ project adheres to [Semantic Versioning](http://semver.org/).
+new Hyper(1, 1);
```


## [v2.0.0](https://github.com/stellar/js-xdr/compare/v1.3.0...v2.0.0)

- Refactor XDR serialization/deserialization logic ([#91](https://github.com/stellar/js-xdr/pull/91)).
- Refactor XDR serialization/deserialization logic
([#91](https://github.com/stellar/js-xdr/pull/91)).
- Replace `long` dependency with native `BigInt` arithmetics.
- Replace `lodash` dependency with built-in Array and Object methods, iterators.
- Add `buffer` dependency for WebPack browser polyfill.
Expand All @@ -61,23 +91,28 @@ project adheres to [Semantic Versioning](http://semver.org/).
- Always check that the entire read buffer is consumed (#32 fixed).
- Check actual byte size of the string on write (#33 fixed).
- Fix babel-polyfill build warnings (#34 fixed).
- Upgrade dependencies to their latest versions ([#92](https://github.com/stellar/js-xdr/pull/92)).
- Upgrade dependencies to their latest versions
([#92](https://github.com/stellar/js-xdr/pull/92)).

## [v1.3.0](https://github.com/stellar/js-xdr/compare/v1.2.0...v1.3.0)

- Inline and modernize the `cursor` dependency ([#](https://github.com/stellar/js-xdr/pull/63)).
- Inline and modernize the `cursor` dependency
([#](https://github.com/stellar/js-xdr/pull/63)).

## [v1.2.0](https://github.com/stellar/js-xdr/compare/v1.1.4...v1.2.0)

- 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)).
- 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)).

## [v1.1.4](https://github.com/stellar/js-xdr/compare/v1.1.3...v1.1.4)

- Remove `core-js` dependency ([#45](https://github.com/stellar/js-xdr/pull/45)).
- Remove `core-js` dependency
([#45](https://github.com/stellar/js-xdr/pull/45)).

## [v1.1.3](https://github.com/stellar/js-xdr/compare/v1.1.2...v1.1.3)

- Split out reference class to it's own file to avoid circular import ([#39](https://github.com/stellar/js-xdr/pull/39)).
- Split out reference class to it's own file to avoid circular import
([#39](https://github.com/stellar/js-xdr/pull/39)).

## [v1.1.2](https://github.com/stellar/js-xdr/compare/v1.1.1...v1.1.2)

Expand Down
8 changes: 7 additions & 1 deletion src/array.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { XdrCompositeType } from './xdr-type';
import { XdrWriterError } from './errors';
import { XdrReaderError, XdrWriterError } from './errors';

export class Array extends XdrCompositeType {
constructor(childType, length) {
Expand All @@ -12,6 +12,12 @@ export class Array extends XdrCompositeType {
* @inheritDoc
*/
read(reader) {
if (this._length > reader.remainingBytes()) {
throw new XdrReaderError(
`insufficient bytes to decode Array of length ${this._length}`
);
}

Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fast-fail check compares array length (number of elements) with remaining bytes, which are incompatible units. This check will only catch cases where the number of elements exceeds the number of bytes, but XDR elements typically consume multiple bytes (e.g., Int consumes 4 bytes).

For example, an Array of 2 Int elements requires 8 bytes, but this check would pass if 4 bytes remain (since 2 ≤ 4), leading to a later "read outside boundary" error instead of the intended fast-fail behavior.

This check is too weak to provide the intended protection. Consider either:

  1. Checking if the child type has a known fixed size and multiplying accordingly
  2. Removing this check if the child size cannot be reliably determined
  3. Documenting that this check only provides minimal protection for egregious cases
Suggested change
if (this._length > reader.remainingBytes()) {
throw new XdrReaderError(
`insufficient bytes to decode Array of length ${this._length}`
);
}
// Fast-fail only when the child type has a known fixed byte size.
let elementSize = null;
if (typeof this._childType?.getFixedSize === 'function') {
const size = this._childType.getFixedSize();
if (typeof size === 'number' && size >= 0) {
elementSize = size;
}
} else if (typeof this._childType?.fixedSize === 'number' && this._childType.fixedSize >= 0) {
elementSize = this._childType.fixedSize;
}
if (elementSize !== null) {
const requiredBytes = elementSize * this._length;
if (requiredBytes > reader.remainingBytes()) {
throw new XdrReaderError(
`insufficient bytes to decode Array of length ${this._length}`
);
}
}

Copilot uses AI. Check for mistakes.
// allocate array of specified length
const result = new global.Array(this._length);
// read values
Expand Down
8 changes: 8 additions & 0 deletions src/serialization/xdr-reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ export class XdrReader {
this._index = 0;
}

/**
* Remaining unread bytes in the source buffer
* @return {Number}
*/
remainingBytes() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be a get property since there are no side effects, just an idea though

return this._length - this._index;
}

/**
* Read byte array from the buffer
* @param {Number} size - Bytes to read
Expand Down
6 changes: 6 additions & 0 deletions src/var-array.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ export class VarArray extends XdrCompositeType {
`saw ${length} length VarArray, max allowed is ${this._maxLength}`
);

if (length > reader.remainingBytes()) {
throw new XdrReaderError(
`insufficient bytes to decode VarArray of length ${length}`
);
}
Comment on lines +29 to +33
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fast-fail check compares array length (number of elements) with remaining bytes, which are incompatible units. While this check works for the current test case (0 bytes remaining), it provides insufficient protection in general.

For example, if 6 bytes remain after reading the length prefix and the array declares 2 Int elements, the check evaluates 2 > 6 (false), so no error is thrown. However, 2 Int elements require 8 bytes, causing a later "read outside boundary" error instead of an immediate fast-fail.

The check should either:

  1. Account for the known fixed size of child elements when applicable
  2. Be documented as providing only minimal protection for egregious cases where element count exceeds byte count

Copilot uses AI. Check for mistakes.

const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = this._childType.read(reader);
Expand Down
24 changes: 23 additions & 1 deletion test/unit/array_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,32 @@ describe('Array#read', function () {

it("throws XdrReaderError when the byte stream isn't large enough", function () {
expect(() => read(many, [0x00, 0x00, 0x00, 0x00])).to.throw(
/read outside the boundary/i
/(read outside the boundary|insufficient bytes)/i
);
});

it('fast-fails before decoding child elements when remaining bytes are insufficient', function () {
let calls = 0;
class FixedSizeChild {
static read() {
calls += 1;
return 0;
}

static write() {}

static isValid() {
return true;
}
}

const fixed = new XDR.Array(FixedSizeChild, 2);
const reader = new XdrReader([0x00, 0x00, 0x00, 0x01]);

expect(() => fixed.read(reader)).to.throw(/insufficient bytes/i);
expect(calls).to.eql(0);
});
Comment on lines +52 to +96
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test expects a fast-fail error when decoding an Array of 2 elements with only 4 bytes available. However, the fast-fail check compares 2 > 4 (array length vs bytes), which is false, so no error will be thrown. The test will fail because:

  1. No exception is thrown (line 48 expectation fails)
  2. The mock FixedSizeChild.read() is called twice, setting calls=2 instead of the expected 0 (line 49 expectation fails)

This test appears to be based on an incorrect understanding of how the fast-fail check works. The check only triggers when array length exceeds remaining bytes as numbers, not when insufficient bytes exist for the actual data.

Copilot uses AI. Check for mistakes.

function read(arr, bytes) {
let reader = new XdrReader(bytes);
return arr.read(reader);
Expand Down
22 changes: 22 additions & 0 deletions test/unit/var-array_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,28 @@ describe('VarArray#read', function () {
expect(() => read([0x00, 0x00, 0x00, 0x03])).to.throw(/read error/i);
});

it('fast-fails when declared length cannot fit remaining bytes for fixed-size child', function () {
let calls = 0;
class FixedSizeChild {
static read() {
calls += 1;
return 0;
}

static write() {}

static isValid() {
return true;
}
}

const arr = new XDR.VarArray(FixedSizeChild, 10);
const io = new XdrReader([0x00, 0x00, 0x00, 0x02]);

expect(() => arr.read(io)).to.throw(/insufficient bytes/i);
expect(calls).to.eql(0);
});

function read(bytes) {
let io = new XdrReader(bytes);
return subject.read(io);
Expand Down
Loading