Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Disallow unregistered classes in JSON RPC interface and match by name #1820

Merged
merged 5 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
22 changes: 18 additions & 4 deletions yarn-project/foundation/src/json-rpc/class_converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@ export class ClassConverter {
* @returns If it is a registered class.
*/
isRegisteredClass(obj: any) {
return this.toName.has(obj);
const name = obj.prototype.constructor.name;
return this.toName.has(obj) || this.isRegisteredClassName(name);
}
/**
* Convert a JSON-like object to a class object.
Expand All @@ -182,10 +183,23 @@ export class ClassConverter {
* @returns The class object.
*/
toJsonObj(classObj: any): JsonEncodedClass | StringEncodedClass {
const result = this.toName.get(classObj.constructor);
assert(result, `Could not find class in lookup.`);
const [type, encoding] = result;
const { type, encoding } = this.lookupObject(classObj);
const data = encoding === 'string' ? classObj.toString() : classObj.toJSON();
return { type: type!, data };
}

/**
* Loads the corresponding type for this class based on constructor first and constructor name if not found.
* Constructor match works in the event of a minifier changing function names, and constructor name match
* works in the event of duplicated instances of node modules being loaded (see #1826).
* @param classObj - Object to lookup in the registered types.
* @returns Registered type name and encoding.
*/
private lookupObject(classObj: any) {
const nameResult = this.toName.get(classObj.constructor);
if (nameResult) return { type: nameResult[0], encoding: nameResult[1] };
const classResult = this.toClass.get(classObj.constructor.name);
if (classResult) return { type: classObj.constructor.name, encoding: classResult[1] };
throw new Error(`Could not find class ${classObj.constructor.name} in lookup.`);
}
}
32 changes: 32 additions & 0 deletions yarn-project/foundation/src/json-rpc/convert.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Buffer } from 'buffer';

import { ClassConverter } from './class_converter.js';
import { convertBigintsInObj, convertFromJsonObj, convertToJsonObj } from './convert.js';
import { ToStringClass as ToStringClassA } from './fixtures/class_a.js';
import { ToStringClass as ToStringClassB } from './fixtures/class_b.js';
import { TestNote } from './fixtures/test_state.js';

const TEST_BASE64 = 'YmFzZTY0IGRlY29kZXI=';
Expand All @@ -24,3 +26,33 @@ test('does not convert a string', () => {
expect(convertBigintsInObj('hello')).toEqual('hello');
expect(convertBigintsInObj({ msg: 'hello' })).toEqual({ msg: 'hello' });
});

test('converts a registered class', () => {
const cc = new ClassConverter({ ToStringClass: ToStringClassA });
const obj = { content: new ToStringClassA('a', 'b') };
const serialised = convertToJsonObj(cc, obj);
const deserialised = convertFromJsonObj(cc, serialised) as { content: ToStringClassA };
expect(deserialised.content).toBeInstanceOf(ToStringClassA);
expect(deserialised.content.x).toEqual('a');
expect(deserialised.content.y).toEqual('b');
});

test('converts a class by name in the event of duplicate modules being loaded', () => {
const cc = new ClassConverter({ ToStringClass: ToStringClassA });
const obj = { content: new ToStringClassB('a', 'b') };
const serialised = convertToJsonObj(cc, obj);
const deserialised = convertFromJsonObj(cc, serialised) as { content: ToStringClassA };
expect(deserialised.content).toBeInstanceOf(ToStringClassA);
expect(deserialised.content.x).toEqual('a');
expect(deserialised.content.y).toEqual('b');
});

test('converts a class by constructor instead of name in the event of minified bundle', () => {
const cc = new ClassConverter({ NotMinifiedToStringClassName: ToStringClassA });
const obj = { content: new ToStringClassA('a', 'b') };
const serialised = convertToJsonObj(cc, obj);
const deserialised = convertFromJsonObj(cc, serialised) as { content: ToStringClassA };
expect(deserialised.content).toBeInstanceOf(ToStringClassA);
expect(deserialised.content.x).toEqual('a');
expect(deserialised.content.y).toEqual('b');
});
10 changes: 8 additions & 2 deletions yarn-project/foundation/src/json-rpc/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,10 @@ export function convertFromJsonObj(cc: ClassConverter, obj: any): any {
}
return newObj;
}

// Throw if this is a non-primitive class that was not registered
if (typeof obj === 'object' && Object.getPrototypeOf(obj) === Object.getPrototypeOf({})) {
spalladino marked this conversation as resolved.
Show resolved Hide resolved
throw new Error(`Class ${obj.constructor.name} not registered for deserialisation`);
}
// Leave alone, assume JSON primitive
return obj;
}
Expand Down Expand Up @@ -118,7 +121,10 @@ export function convertToJsonObj(cc: ClassConverter, obj: any): any {
}
return newObj;
}

// Throw if this is a non-primitive class that was not registered
if (typeof obj === 'object' && Object.getPrototypeOf(obj) === Object.getPrototypeOf({})) {
throw new Error(`Object ${obj.constructor.name} not registered for serialisation`);
}
// Leave alone, assume JSON primitive
return obj;
}
15 changes: 15 additions & 0 deletions yarn-project/foundation/src/json-rpc/fixtures/class_a.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Test class for testing string converter.
*/
export class ToStringClass {
constructor(/** A value */ public readonly x: string, /** Another value */ public readonly y: string) {}

toString(): string {
return [this.x, this.y].join('-');
}

static fromString(value: string) {
const [x, y] = value.split('-');
return new ToStringClass(x, y);
}
}
15 changes: 15 additions & 0 deletions yarn-project/foundation/src/json-rpc/fixtures/class_b.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Test class for testing string converter.
*/
export class ToStringClass {
constructor(/** A value */ public readonly x: string, /** Another value */ public readonly y: string) {}

toString(): string {
return [this.x, this.y].join('-');
}

static fromString(value: string) {
const [x, y] = value.split('-');
return new ToStringClass(x, y);
}
}