Skip to content

Commit

Permalink
fix: Correctly decode optional struct fields (#564)
Browse files Browse the repository at this point in the history
* Fix incorrect decoding of optional fields
* Refactor record parsing loop
  • Loading branch information
frederikrothenberger committed Apr 20, 2022
1 parent 4dae0c9 commit 47ee650
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 8 deletions.
6 changes: 5 additions & 1 deletion docs/generated/changelog.html
Expand Up @@ -10,7 +10,11 @@
<h1>Agent-JS Changelog</h1>

<section>
<h2>Version 0.10.5</h2>
<h2>Version 0.11.1</h2>
<ul>
<li>Fix for a corner case that could lead to incorrect decoding of record types.</li>
</ul>
<h2>Version 0.11.0</h2>
<ul>
<li>
makeNonce now returns unique values. Previously only the first byte of the nonce was
Expand Down
20 changes: 20 additions & 0 deletions packages/candid/src/idl.test.ts
Expand Up @@ -609,3 +609,23 @@ test('decode / encode unknown nested record', () => {
const decodedValue2 = IDL.decode([recordType], fromHexString(reencoded))[0] as any;
expect(decodedValue2).toEqual(value);
});

test('should correctly decode expected optional fields with lower hash than required fields', () => {
const HttpResponse = IDL.Record({
body: IDL.Text,
headers: IDL.Vec(IDL.Tuple(IDL.Text, IDL.Text)),
streaming_strategy: IDL.Opt(IDL.Text),
status_code: IDL.Int,
upgrade: IDL.Opt(IDL.Bool),
});
const encoded =
'4449444c036c04a2f5ed880471c6a4a19806019ce9c69906029aa1b2f90c7c6d7f6e7e010003666f6f000101c801';
const value = IDL.decode([HttpResponse], fromHexString(encoded))[0];
expect(value).toEqual({
body: 'foo',
headers: [],
status_code: BigInt(200),
streaming_strategy: [],
upgrade: [true],
});
});
39 changes: 32 additions & 7 deletions packages/candid/src/idl.ts
Expand Up @@ -927,18 +927,43 @@ export class RecordClass extends ConstructType<Record<string, any>> {
throw new Error('Not a record type');
}
const x: Record<string, any> = {};
let idx = 0;
for (const [hash, type] of record._fields) {
if (idx >= this._fields.length || idlLabelToId(this._fields[idx][0]) !== idlLabelToId(hash)) {
// skip field

let expectedRecordIdx = 0;
let actualRecordIdx = 0;
while (actualRecordIdx < record._fields.length) {
const [hash, type] = record._fields[actualRecordIdx];

if (expectedRecordIdx >= this._fields.length) {
// skip unexpected left over fields present on the wire
type.decodeValue(b, type);
actualRecordIdx++;
continue;
}
const [expectKey, expectType] = this._fields[idx];

const [expectKey, expectType] = this._fields[expectedRecordIdx];
if (idlLabelToId(this._fields[expectedRecordIdx][0]) !== idlLabelToId(hash)) {
// the current field on the wire does not match the expected field

// skip expected optional fields that are not present on the wire
if (expectType instanceof OptClass || expectType instanceof ReservedClass) {
x[expectKey] = [];
expectedRecordIdx++;
continue;
}

// skip unexpected interspersed fields present on the wire
type.decodeValue(b, type);
actualRecordIdx++;
continue;
}

x[expectKey] = expectType.decodeValue(b, type);
idx++;
expectedRecordIdx++;
actualRecordIdx++;
}
for (const [expectKey, expectType] of this._fields.slice(idx)) {

// initialize left over expected optional fields
for (const [expectKey, expectType] of this._fields.slice(expectedRecordIdx)) {
if (expectType instanceof OptClass || expectType instanceof ReservedClass) {
// TODO this assumes null value in opt is represented as []
x[expectKey] = [];
Expand Down

0 comments on commit 47ee650

Please sign in to comment.