@agoric/marshal
Advanced tools
Comparing version 0.3.0 to 0.3.1
@@ -6,2 +6,17 @@ # Change Log | ||
## [0.3.1](https://github.com/Agoric/agoric-sdk/compare/@agoric/marshal@0.3.0...@agoric/marshal@0.3.1) (2021-02-16) | ||
### Bug Fixes | ||
* **marshal:** reject getters in pass-by-ref, even if it returns a function ([#2438](https://github.com/Agoric/agoric-sdk/issues/2438)) ([b9368b6](https://github.com/Agoric/agoric-sdk/commit/b9368b6ee16a5562a622551539eff2b8708f0fdd)), closes [#2436](https://github.com/Agoric/agoric-sdk/issues/2436) | ||
* Correlate sent errors with received errors ([73b9cfd](https://github.com/Agoric/agoric-sdk/commit/73b9cfd33cf7842bdc105a79592028649cb1c92a)) | ||
* Far and Remotable do unverified local marking rather than WeakMap ([#2361](https://github.com/Agoric/agoric-sdk/issues/2361)) ([ab59ab7](https://github.com/Agoric/agoric-sdk/commit/ab59ab779341b9740827b7c4cca4680e7b7212b2)) | ||
* review comments ([7db7e5c](https://github.com/Agoric/agoric-sdk/commit/7db7e5c4c569dfedff8d748dd58893218b0a2458)) | ||
* use assert rather than FooError constructors ([f860c5b](https://github.com/Agoric/agoric-sdk/commit/f860c5bf5add165a08cb5bd543502857c3f57998)) | ||
# [0.3.0](https://github.com/Agoric/agoric-sdk/compare/@agoric/marshal@0.2.7...@agoric/marshal@0.3.0) (2020-12-10) | ||
@@ -8,0 +23,0 @@ |
export { | ||
REMOTE_STYLE, | ||
mustPassByPresence, | ||
getInterfaceOf, | ||
@@ -8,3 +7,2 @@ pureCopy, | ||
getErrorConstructor, | ||
mustPassByRemote, | ||
sameValueZero, | ||
@@ -14,2 +12,3 @@ passStyleOf, | ||
Remotable, | ||
Far, | ||
} from './src/marshal'; |
{ | ||
"name": "@agoric/marshal", | ||
"version": "0.3.0", | ||
"version": "0.3.1", | ||
"description": "marshal", | ||
@@ -38,9 +38,9 @@ "parsers": { | ||
"dependencies": { | ||
"@agoric/assert": "^0.2.0", | ||
"@agoric/eventual-send": "^0.13.0", | ||
"@agoric/assert": "^0.2.1", | ||
"@agoric/eventual-send": "^0.13.1", | ||
"@agoric/nat": "^2.0.1", | ||
"@agoric/promise-kit": "^0.2.0" | ||
"@agoric/promise-kit": "^0.2.1" | ||
}, | ||
"devDependencies": { | ||
"@agoric/install-ses": "^0.5.0", | ||
"@agoric/install-ses": "^0.5.1", | ||
"ava": "^3.12.1", | ||
@@ -76,3 +76,3 @@ "esm": "^3.2.25", | ||
}, | ||
"gitHead": "f9fab94b70ed22d7fcccb261af11ed8dd6348614" | ||
"gitHead": "93ccbba4efc9e375032becfdf7005502f9c4db18" | ||
} |
@@ -7,3 +7,3 @@ // @ts-check | ||
import Nat from '@agoric/nat'; | ||
import { assert, details as d, q } from '@agoric/assert'; | ||
import { assert, details as X, q } from '@agoric/assert'; | ||
import { isPromise } from '@agoric/promise-kit'; | ||
@@ -13,17 +13,35 @@ | ||
const { | ||
getPrototypeOf, | ||
setPrototypeOf, | ||
getOwnPropertyDescriptors, | ||
is, | ||
isFrozen, | ||
fromEntries, | ||
prototype: objectPrototype, | ||
} = Object; | ||
const { ownKeys } = Reflect; | ||
// TODO: Use just 'remote' when we're willing to make a breaking change. | ||
export const REMOTE_STYLE = 'presence'; | ||
// TODO, remove the mustPassByPresence alias when we make a breaking change. | ||
// eslint-disable-next-line no-use-before-define | ||
export { mustPassByRemote as mustPassByPresence }; | ||
const PASS_STYLE = Symbol.for('passStyle'); | ||
/** | ||
* @type {WeakMap<Object, InterfaceSpec>} | ||
*/ | ||
const remotableToInterface = new WeakMap(); | ||
/** @type {MarshalGetInterfaceOf} */ | ||
export function getInterfaceOf(maybeRemotable) { | ||
return remotableToInterface.get(maybeRemotable); | ||
export function getInterfaceOf(val) { | ||
if (typeof val !== 'object' || val === null) { | ||
return undefined; | ||
} | ||
if (val[PASS_STYLE] !== REMOTE_STYLE) { | ||
return undefined; | ||
} | ||
assert(isFrozen(val), X`Remotable ${val} must be frozen`, TypeError); | ||
const iface = val[Symbol.toStringTag]; | ||
assert.typeof( | ||
iface, | ||
'string', | ||
X`Remotable interface currently can only be a string`, | ||
); | ||
return iface; | ||
} | ||
@@ -69,2 +87,6 @@ | ||
// Object.entries(obj) takes a snapshot (even if a Proxy). | ||
// Since we already know it is a copyRecord or copyArray, we | ||
// know that Object.entries is safe enough. On a copyRecord it | ||
// will represent all the own properties. On a copyArray it | ||
// will represent all the own properties except for the length. | ||
Object.entries(obj).forEach(([prop, value]) => { | ||
@@ -96,4 +118,7 @@ copy[prop] = pureCopy(value, already); | ||
case REMOTE_STYLE: { | ||
throw TypeError( | ||
`Input value ${passStyle} cannot be copied as it must be passed by reference`, | ||
assert.fail( | ||
X`Input value ${q( | ||
passStyle, | ||
)} cannot be copied as it must be passed by reference`, | ||
TypeError, | ||
); | ||
@@ -103,7 +128,10 @@ } | ||
case 'promise': { | ||
throw TypeError(`Promises cannot be copied`); | ||
assert.fail(X`Promises cannot be copied`, TypeError); | ||
} | ||
default: | ||
throw TypeError(`Input value ${passStyle} is not recognized as data`); | ||
assert.fail( | ||
X`Input value ${q(passStyle)} is not recognized as data`, | ||
TypeError, | ||
); | ||
} | ||
@@ -144,8 +172,9 @@ } | ||
} | ||
const proto = Object.getPrototypeOf(val); | ||
const proto = getPrototypeOf(val); | ||
const { name } = val; | ||
const EC = getErrorConstructor(name); | ||
if (!EC || EC.prototype !== proto) { | ||
throw TypeError( | ||
`Errors must inherit from an error class .prototype ${val}`, | ||
assert.fail( | ||
X`Errors must inherit from an error class .prototype ${val}`, | ||
TypeError, | ||
); | ||
@@ -159,14 +188,16 @@ } | ||
...restDescs | ||
} = Object.getOwnPropertyDescriptors(val); | ||
const restNames = Object.keys(restDescs); | ||
if (restNames.length >= 1) { | ||
throw new TypeError(`Unexpected own properties in error: ${restNames}`); | ||
} | ||
} = getOwnPropertyDescriptors(val); | ||
const restKeys = ownKeys(restDescs); | ||
assert( | ||
restKeys.length === 0, | ||
X`Unexpected own properties in error: ${q(restKeys)}`, | ||
TypeError, | ||
); | ||
if (mDesc) { | ||
if (typeof mDesc.value !== 'string') { | ||
throw new TypeError(`Malformed error object: ${val}`); | ||
} | ||
if (mDesc.enumerable) { | ||
throw new TypeError(`An error's .message must not be enumerable`); | ||
} | ||
assert.typeof(mDesc.value, 'string', X`Malformed error object: ${val}`); | ||
assert( | ||
!mDesc.enumerable, | ||
X`An error's .message must not be enumerable`, | ||
TypeError, | ||
); | ||
} | ||
@@ -184,25 +215,33 @@ return true; | ||
} | ||
if (Object.getPrototypeOf(val) !== Array.prototype) { | ||
throw new TypeError(`Malformed array: ${val}`); | ||
} | ||
assert( | ||
getPrototypeOf(val) === Array.prototype, | ||
X`Malformed array: ${val}`, | ||
TypeError, | ||
); | ||
const len = val.length; | ||
const descs = Object.getOwnPropertyDescriptors(val); | ||
const descs = getOwnPropertyDescriptors(val); | ||
for (let i = 0; i < len; i += 1) { | ||
const desc = descs[i]; | ||
if (!desc) { | ||
throw new TypeError(`Arrays must not contain holes: ${i}`); | ||
} | ||
if (!('value' in desc)) { | ||
throw new TypeError(`Arrays must not contain accessors: ${i}`); | ||
} | ||
if (typeof desc.value === 'function') { | ||
throw new TypeError(`Arrays must not contain methods: ${i}`); | ||
} | ||
if (!desc.enumerable) { | ||
throw new TypeError(`Array elements must be enumerable: ${i}`); | ||
} | ||
assert(desc, X`Arrays must not contain holes: ${q(i)}`, TypeError); | ||
assert( | ||
'value' in desc, | ||
X`Arrays must not contain accessors: ${q(i)}`, | ||
TypeError, | ||
); | ||
assert( | ||
typeof desc.value !== 'function', | ||
X`Arrays must not contain methods: ${q(i)}`, | ||
TypeError, | ||
); | ||
assert( | ||
desc.enumerable, | ||
X`Array elements must be enumerable: ${q(i)}`, | ||
TypeError, | ||
); | ||
} | ||
if (Object.keys(descs).length !== len + 1) { | ||
throw new TypeError(`Arrays must not have non-indexes: ${val}`); | ||
} | ||
assert( | ||
ownKeys(descs).length === len + 1, | ||
X`Arrays must not have non-indexes: ${val}`, | ||
TypeError, | ||
); | ||
return true; | ||
@@ -216,11 +255,18 @@ } | ||
function isPassByCopyRecord(val) { | ||
if (Object.getPrototypeOf(val) !== Object.prototype) { | ||
if (getPrototypeOf(val) !== objectPrototype) { | ||
return false; | ||
} | ||
const descEntries = Object.entries(Object.getOwnPropertyDescriptors(val)); | ||
if (descEntries.length === 0) { | ||
const descs = getOwnPropertyDescriptors(val); | ||
const descKeys = ownKeys(descs); | ||
if (descKeys.length === 0) { | ||
// empty non-array objects are pass-by-remote, not pass-by-copy | ||
// TODO Beware: Unmarked empty records will become pass-by-copy | ||
// See https://github.com/Agoric/agoric-sdk/issues/2018 | ||
return false; | ||
} | ||
for (const [_key, desc] of descEntries) { | ||
for (const descKey of descKeys) { | ||
if (typeof descKey === 'symbol') { | ||
return false; | ||
} | ||
const desc = descs[descKey]; | ||
if (typeof desc.value === 'function') { | ||
@@ -230,14 +276,19 @@ return false; | ||
} | ||
for (const [key, desc] of descEntries) { | ||
if (typeof key === 'symbol') { | ||
throw new TypeError( | ||
`Records must not have symbol-named properties: ${String(key)}`, | ||
); | ||
} | ||
if (!('value' in desc)) { | ||
throw new TypeError(`Records must not contain accessors: ${key}`); | ||
} | ||
if (!desc.enumerable) { | ||
throw new TypeError(`Record fields must be enumerable: ${key}`); | ||
} | ||
for (const descKey of descKeys) { | ||
assert.typeof( | ||
descKey, | ||
'string', | ||
X`Pass by copy records can only have string-named own properties`, | ||
); | ||
const desc = descs[descKey]; | ||
assert( | ||
!('get' in desc), | ||
X`Records must not contain accessors: ${q(descKey)}`, | ||
TypeError, | ||
); | ||
assert( | ||
desc.enumerable, | ||
X`Record fields must be enumerable: ${q(descKey)}`, | ||
TypeError, | ||
); | ||
} | ||
@@ -247,2 +298,49 @@ return true; | ||
const makeRemotableProto = (oldProto, allegedName) => { | ||
assert( | ||
oldProto === objectPrototype || oldProto === null, | ||
X`For now, remotables cannot inherit from anything unusual`, | ||
); | ||
// Assign the arrow function to a variable to set its .name. | ||
const toString = () => `[${allegedName}]`; | ||
return harden({ | ||
__proto__: oldProto, | ||
[PASS_STYLE]: REMOTE_STYLE, | ||
toString, | ||
[Symbol.toStringTag]: allegedName, | ||
}); | ||
}; | ||
const assertRemotableProto = val => { | ||
assert.typeof(val, 'object', X`cannot serialize non-objects like ${val}`); | ||
assert(!Array.isArray(val), X`Arrays cannot be pass-by-remote`); | ||
assert(val !== null, X`null cannot be pass-by-remote`); | ||
const protoProto = getPrototypeOf(val); | ||
assert( | ||
protoProto === objectPrototype || protoProto === null, | ||
X`The Remotable Proto marker cannot inherit from anything unusual`, | ||
); | ||
assert(isFrozen(val), X`The Remotable proto must be frozen`); | ||
const { | ||
// @ts-ignore | ||
[PASS_STYLE]: { value: passStyleValue }, | ||
// @ts-ignore | ||
toString: { value: toStringValue }, | ||
// @ts-ignore | ||
[Symbol.toStringTag]: { value: toStringTagValue }, | ||
...rest | ||
} = getOwnPropertyDescriptors(val); | ||
assert( | ||
ownKeys(rest).length === 0, | ||
X`Unexpect properties on Remotable Proto ${ownKeys(rest)}`, | ||
); | ||
assert( | ||
passStyleValue === REMOTE_STYLE, | ||
X`Expected ${q(REMOTE_STYLE)}, not ${q(passStyleValue)}`, | ||
); | ||
assert.typeof(toStringValue, 'function', X`toString must be a function`); | ||
assert.typeof(toStringTagValue, 'string', X`@@toStringTag must be a string`); | ||
}; | ||
/** | ||
@@ -257,23 +355,22 @@ * Ensure that val could become a legitimate remotable. This is used | ||
// throws exception if cannot | ||
if (typeof val !== 'object') { | ||
throw new Error(`cannot serialize non-objects like ${val}`); | ||
} | ||
if (Array.isArray(val)) { | ||
throw new Error(`Arrays cannot be pass-by-remote`); | ||
} | ||
if (val === null) { | ||
throw new Error(`null cannot be pass-by-remote`); | ||
} | ||
assert.typeof(val, 'object', X`cannot serialize non-objects like ${val}`); | ||
assert(!Array.isArray(val), X`Arrays cannot be pass-by-remote`); | ||
assert(val !== null, X`null cannot be pass-by-remote`); | ||
const names = Object.getOwnPropertyNames(val); | ||
names.forEach(name => { | ||
if (typeof val[name] !== 'function') { | ||
throw new Error( | ||
`cannot serialize objects with non-methods like the .${name} in ${val}`, | ||
); | ||
// return false; | ||
} | ||
const descs = getOwnPropertyDescriptors(val); | ||
const keys = ownKeys(descs); // enumerable-and-not, string-or-Symbol | ||
keys.forEach(key => { | ||
assert( | ||
// @ts-ignore | ||
!('get' in descs[key]), | ||
X`cannot serialize objects with getters like ${q(String(key))} in ${val}`, | ||
); | ||
assert.typeof( | ||
val[key], | ||
'function', | ||
X`cannot serialize objects with non-methods like ${q( | ||
String(key), | ||
)} in ${val}`, | ||
); | ||
}); | ||
// ok! | ||
} | ||
@@ -284,16 +381,10 @@ | ||
*/ | ||
export function mustPassByRemote(val) { | ||
if (!Object.isFrozen(val)) { | ||
throw new Error(`cannot serialize non-frozen objects like ${val}`); | ||
} | ||
function assertRemotable(val) { | ||
assert(isFrozen(val), X`cannot serialize non-frozen objects like ${val}`); | ||
if (getInterfaceOf(val) === undefined) { | ||
// Not a registered Remotable, so check its contents. | ||
assertCanBeRemotable(val); | ||
} | ||
assertCanBeRemotable(val); | ||
// It's not a registered Remotable, so enforce the prototype check. | ||
const p = Object.getPrototypeOf(val); | ||
if (p !== null && p !== Object.prototype) { | ||
mustPassByRemote(p); | ||
const p = getPrototypeOf(val); | ||
if (p !== null && p !== objectPrototype) { | ||
assertRemotableProto(p); | ||
} | ||
@@ -316,3 +407,3 @@ } | ||
export function sameValueZero(x, y) { | ||
return x === y || Object.is(x, y); | ||
return x === y || is(x, y); | ||
} | ||
@@ -325,3 +416,3 @@ | ||
* 2: pass-by-copy: all string-named own properties are data, not methods | ||
* the object must inherit from Object.prototype or null | ||
* the object must inherit from objectPrototype or null | ||
* 3: the empty object is pass-by-remote, for identity comparison | ||
@@ -366,15 +457,15 @@ * | ||
// TODO Hilbert hotel | ||
throw new Error(`property "${QCLASS}" reserved`); | ||
assert.fail(X`property ${q(QCLASS)} reserved`); | ||
} | ||
if (!Object.isFrozen(val)) { | ||
throw new Error( | ||
`Cannot pass non-frozen objects like ${val}. Use harden()`, | ||
); | ||
} | ||
assert( | ||
isFrozen(val), | ||
X`Cannot pass non-frozen objects like ${val}. Use harden()`, | ||
); | ||
if (isPromise(val)) { | ||
return 'promise'; | ||
} | ||
if (typeof val.then === 'function') { | ||
throw new Error(`Cannot pass non-promise thenables`); | ||
} | ||
assert( | ||
typeof val.then !== 'function', | ||
X`Cannot pass non-promise thenables`, | ||
); | ||
if (isPassByCopyError(val)) { | ||
@@ -389,7 +480,7 @@ return 'copyError'; | ||
} | ||
mustPassByRemote(val); | ||
assertRemotable(val); | ||
return REMOTE_STYLE; | ||
} | ||
case 'function': { | ||
throw new Error(`Bare functions like ${val} are disabled for now`); | ||
assert.fail(X`Bare functions like ${val} are disabled for now`); | ||
} | ||
@@ -405,3 +496,3 @@ case 'undefined': | ||
default: { | ||
throw new TypeError(`Unrecognized typeof ${typestr}`); | ||
assert.fail(X`Unrecognized typeof ${q(typestr)}`, TypeError); | ||
} | ||
@@ -444,5 +535,3 @@ } | ||
const index = Nat(allegedIndex); | ||
if (index >= ibids.length) { | ||
throw new RangeError(`ibid out of range: ${index}`); | ||
} | ||
assert(index < ibids.length, X`ibid out of range: ${index}`, RangeError); | ||
const result = ibids[index]; | ||
@@ -459,6 +548,9 @@ if (unfinishedIbids.has(result)) { | ||
case 'forbidCycles': { | ||
throw new TypeError(`Ibid cycle at ${index}`); | ||
assert.fail(X`Ibid cycle at ${q(index)}`, TypeError); | ||
} | ||
default: { | ||
throw new TypeError(`Unrecognized cycle policy: ${cyclePolicy}`); | ||
assert.fail( | ||
X`Unrecognized cycle policy: ${q(cyclePolicy)}`, | ||
TypeError, | ||
); | ||
} | ||
@@ -503,3 +595,13 @@ } | ||
convertSlotToVal = defaultSlotToValFn, | ||
{ marshalName = 'anon-marshal' } = {}, | ||
) { | ||
assert.typeof(marshalName, 'string'); | ||
// Ascending numbers identifying the sending of errors relative to this | ||
// marshal instance. | ||
let errorCount = 0; | ||
const nextErrorId = () => { | ||
errorCount += 1; | ||
return `error:${marshalName}#${errorCount}`; | ||
}; | ||
/** | ||
@@ -525,6 +627,23 @@ * @template Slot | ||
if (iface !== undefined) { | ||
/* | ||
if (iface === undefined && passStyleOf(val) === REMOTE_STYLE) { | ||
// iface = `Alleged: remotable at slot ${slotIndex}`; | ||
if ( | ||
getPrototypeOf(val) === objectPrototype && | ||
ownKeys(val).length === 0 | ||
) { | ||
// For now, skip the diagnostic if we have a pure empty object | ||
} else { | ||
try { | ||
assert.fail(X`Serialize ${val} generates needs iface`); | ||
} catch (err) { | ||
console.info(err); | ||
} | ||
} | ||
} | ||
*/ | ||
if (iface === undefined) { | ||
return harden({ | ||
[QCLASS]: 'slot', | ||
iface, | ||
index: slotIndex, | ||
@@ -535,2 +654,3 @@ }); | ||
[QCLASS]: 'slot', | ||
iface, | ||
index: slotIndex, | ||
@@ -540,6 +660,32 @@ }); | ||
function makeReplacer(slots, slotMap) { | ||
/** | ||
* @template Slot | ||
* @type {Serialize<Slot>} | ||
*/ | ||
const serialize = root => { | ||
const slots = []; | ||
// maps val (promise or remotable) to index of slots[] | ||
const slotMap = new Map(); | ||
const ibidTable = makeReplacerIbidTable(); | ||
return function replacer(_, val) { | ||
/** | ||
* Just consists of data that rounds trips to plain data. | ||
* | ||
* @typedef {any} PlainJSONData | ||
*/ | ||
/** | ||
* Must encode `val` into plain JSON data *canonically*, such that | ||
* `sameStructure(v1, v2)` implies | ||
* `JSON.stringify(encode(v1)) === JSON.stringify(encode(v2))` | ||
* For each record, we only accept sortable property names | ||
* (no anonymous symbols) and on the encoded form. The sort | ||
* order of these names must be the same as their enumeration | ||
* order, so a `JSON.stringify` of the encoded form agrees with | ||
* a canonical-json stringify of the encoded form. | ||
* | ||
* @param {Passable} val | ||
* @returns {PlainJSONData} | ||
*/ | ||
const encode = val => { | ||
// First we handle all primitives. Some can be represented directly as | ||
@@ -563,3 +709,3 @@ // JSON, and some must be encoded as [QCLASS] composites. | ||
} | ||
if (Object.is(val, -0)) { | ||
if (is(val, -0)) { | ||
return 0; | ||
@@ -589,3 +735,3 @@ } | ||
default: { | ||
throw assert.fail(d`Unsupported symbol ${q(String(val))}`); | ||
assert.fail(X`Unsupported symbol ${q(String(val))}`); | ||
} | ||
@@ -606,8 +752,11 @@ } | ||
switch (passStyle) { | ||
case 'copyRecord': | ||
case 'copyRecord': { | ||
// Currently copyRecord allows only string keys so this will | ||
// work. If we allow sortable symbol keys, this will need to | ||
// become more interesting. | ||
const names = ownKeys(val).sort(); | ||
return fromEntries(names.map(name => [name, encode(val[name])])); | ||
} | ||
case 'copyArray': { | ||
// console.log(`canPassByCopy: ${val}`); | ||
// Purposely in-band for readability, but creates need for | ||
// Hilbert hotel. | ||
return val; | ||
return val.map(encode); | ||
} | ||
@@ -622,6 +771,16 @@ case 'copyError': { | ||
// with the correlation. | ||
const errorId = nextErrorId(); | ||
assert.note(val, X`Sent as ${errorId}`); | ||
// TODO we need to instead log to somewhere hidden | ||
// to be revealed when correlating with the received error. | ||
// By sending this to `console.log`, under swingset this is | ||
// enabled by `agoric start --reset -v` and not enabled without | ||
// the `-v` flag. | ||
console.log('Temporary logging of sent error', val); | ||
return harden({ | ||
[QCLASS]: 'error', | ||
errorId, | ||
message: `${val.message}`, | ||
name: `${val.name}`, | ||
message: `${val.message}`, | ||
}); | ||
@@ -639,3 +798,3 @@ } | ||
default: { | ||
throw new TypeError(`unrecognized passStyle ${passStyle}`); | ||
assert.fail(X`unrecognized passStyle ${q(passStyle)}`, TypeError); | ||
} | ||
@@ -646,17 +805,10 @@ } | ||
}; | ||
} | ||
/** | ||
* @template Slot | ||
* @type {Serialize<Slot>} | ||
*/ | ||
function serialize(val) { | ||
const slots = []; | ||
const slotMap = new Map(); // maps val (promise or remotable) to | ||
// index of slots[] | ||
const encoded = encode(root); | ||
return harden({ | ||
body: JSON.stringify(val, makeReplacer(slots, slotMap)), | ||
body: JSON.stringify(encoded), | ||
slots, | ||
}); | ||
} | ||
}; | ||
@@ -707,5 +859,7 @@ function makeFullRevive(slots, cyclePolicy) { | ||
const qclass = rawTree[QCLASS]; | ||
if (typeof qclass !== 'string') { | ||
throw new TypeError(`invalid qclass typeof ${typeof qclass}`); | ||
} | ||
assert.typeof( | ||
qclass, | ||
'string', | ||
X`invalid qclass typeof ${q(typeof qclass)}`, | ||
); | ||
switch (qclass) { | ||
@@ -726,7 +880,7 @@ // Encoding of primitives not handled by JSON | ||
case 'bigint': { | ||
if (typeof rawTree.digits !== 'string') { | ||
throw new TypeError( | ||
`invalid digits typeof ${typeof rawTree.digits}`, | ||
); | ||
} | ||
assert.typeof( | ||
rawTree.digits, | ||
'string', | ||
X`invalid digits typeof ${q(typeof rawTree.digits)}`, | ||
); | ||
/* eslint-disable-next-line no-undef */ | ||
@@ -744,14 +898,20 @@ return BigInt(rawTree.digits); | ||
case 'error': { | ||
if (typeof rawTree.name !== 'string') { | ||
throw new TypeError( | ||
`invalid error name typeof ${typeof rawTree.name}`, | ||
); | ||
assert.typeof( | ||
rawTree.name, | ||
'string', | ||
X`invalid error name typeof ${q(typeof rawTree.name)}`, | ||
); | ||
assert.typeof( | ||
rawTree.message, | ||
'string', | ||
X`invalid error message typeof ${q(typeof rawTree.message)}`, | ||
); | ||
const EC = getErrorConstructor(`${rawTree.name}`) || Error; | ||
const error = harden(new EC(`${rawTree.message}`)); | ||
ibidTable.register(error); | ||
if (typeof rawTree.errorId === 'string') { | ||
// errorId is a late addition so be tolerant of its absence. | ||
assert.note(error, X`Received as ${rawTree.errorId}`); | ||
} | ||
if (typeof rawTree.message !== 'string') { | ||
throw new TypeError( | ||
`invalid error message typeof ${typeof rawTree.message}`, | ||
); | ||
} | ||
const EC = getErrorConstructor(`${rawTree.name}`) || Error; | ||
return ibidTable.register(harden(new EC(`${rawTree.message}`))); | ||
return error; | ||
} | ||
@@ -766,3 +926,3 @@ | ||
// TODO reverse Hilbert hotel | ||
throw new TypeError(`unrecognized ${QCLASS} ${qclass}`); | ||
assert.fail(X`unrecognized ${q(QCLASS)} ${q(qclass)}`, TypeError); | ||
} | ||
@@ -779,4 +939,5 @@ } | ||
const result = ibidTable.start({}); | ||
const names = Object.getOwnPropertyNames(rawTree); | ||
const names = ownKeys(rawTree); | ||
for (const name of names) { | ||
assert.typeof(name, 'string'); | ||
result[name] = fullRevive(rawTree[name]); | ||
@@ -794,10 +955,11 @@ } | ||
function unserialize(data, cyclePolicy = 'forbidCycles') { | ||
if (data.body !== `${data.body}`) { | ||
throw new Error( | ||
`unserialize() given non-capdata (.body is ${data.body}, not string)`, | ||
); | ||
} | ||
if (!Array.isArray(data.slots)) { | ||
throw new Error(`unserialize() given non-capdata (.slots are not Array)`); | ||
} | ||
assert.typeof( | ||
data.body, | ||
'string', | ||
X`unserialize() given non-capdata (.body is ${data.body}, not string)`, | ||
); | ||
assert( | ||
Array.isArray(data.slots), | ||
X`unserialize() given non-capdata (.slots are not Array)`, | ||
); | ||
const rawTree = harden(JSON.parse(data.body)); | ||
@@ -823,8 +985,14 @@ const fullRevive = makeFullRevive(data.slots, cyclePolicy); | ||
* "Alleged: ", to serve as the alleged name. More general ifaces are not yet | ||
* implemented. This is temporary. | ||
* @param {object} [props={}] Own-properties are copied to the remotable | ||
* implemented. This is temporary. We include the | ||
* "Alleged" as a reminder that we do not yet have SwingSet or Comms Vat | ||
* support for ensuring this is according to the vat hosting the object. | ||
* Currently, Alice can tell Bob about Carol, where VatA (on Alice's behalf) | ||
* misrepresents Carol's `iface`. VatB and therefore Bob will then see | ||
* Carol's `iface` as misrepresented by VatA. | ||
* @param {undefined} [props=undefined] Currently may only be undefined. | ||
* That plan is that own-properties are copied to the remotable | ||
* @param {object} [remotable={}] The object used as the remotable | ||
* @returns {object} remotable, modified for debuggability | ||
*/ | ||
function Remotable(iface = 'Remotable', props = {}, remotable = {}) { | ||
function Remotable(iface = 'Remotable', props = undefined, remotable = {}) { | ||
// TODO unimplemented | ||
@@ -834,3 +1002,3 @@ assert.typeof( | ||
'string', | ||
d`Interface ${iface} must be a string; unimplemented`, | ||
X`Interface ${iface} must be a string; unimplemented`, | ||
); | ||
@@ -840,9 +1008,11 @@ // TODO unimplemented | ||
iface === 'Remotable' || iface.startsWith('Alleged: '), | ||
d`For now, iface ${iface} must be "Remotable" or begin with "Alleged: "; unimplemented`, | ||
X`For now, iface ${q( | ||
iface, | ||
)} must be "Remotable" or begin with "Alleged: "; unimplemented`, | ||
); | ||
iface = pureCopy(harden(iface)); | ||
// TODO: When iface is richer than just string, we need to get the allegedName | ||
// in a different way. | ||
const allegedName = iface; | ||
assert(props === undefined, X`Remotable props not yet implemented ${props}`); | ||
@@ -852,48 +1022,25 @@ // Fail fast: check that the unmodified object is able to become a Remotable. | ||
// Ensure that the remotable isn't already registered. | ||
if (remotableToInterface.has(remotable)) { | ||
throw Error(`Remotable ${remotable} is already mapped to an interface`); | ||
} | ||
// A prototype for debuggability. | ||
const oldRemotableProto = harden(Object.getPrototypeOf(remotable)); | ||
// Fail fast: create a fresh empty object with the old | ||
// prototype in order to check it against our rules. | ||
mustPassByRemote(harden(Object.create(oldRemotableProto))); | ||
// Assign the arrow function to a variable to set its .name. | ||
const toString = () => `[${allegedName}]`; | ||
const remotableProto = harden( | ||
Object.create(oldRemotableProto, { | ||
toString: { | ||
value: toString, | ||
}, | ||
[Symbol.toStringTag]: { | ||
value: allegedName, | ||
}, | ||
}), | ||
// Ensure that the remotable isn't already marked. | ||
assert( | ||
!(PASS_STYLE in remotable), | ||
X`Remotable ${remotable} is already marked as a ${q( | ||
remotable[PASS_STYLE], | ||
)}`, | ||
); | ||
const remotableProto = makeRemotableProto( | ||
getPrototypeOf(remotable), | ||
allegedName, | ||
); | ||
// Take a static copy of the properties. | ||
const propEntries = Object.entries(props); | ||
// Take a static copy of the enumerable own properties as data properties. | ||
// const propDescs = getOwnPropertyDescriptors({ ...props }); | ||
const mutateHardenAndCheck = target => { | ||
// Add the snapshotted properties. | ||
/** @type {PropertyDescriptorMap} */ | ||
const newProps = {}; | ||
propEntries.forEach(([prop, value]) => (newProps[prop] = { value })); | ||
Object.defineProperties(target, newProps); | ||
// Set the prototype for debuggability. | ||
Object.setPrototypeOf(target, remotableProto); | ||
harden(remotableProto); | ||
// defineProperties(target, propDescs); | ||
setPrototypeOf(target, remotableProto); | ||
harden(target); | ||
assertCanBeRemotable(target); | ||
return target; | ||
}; | ||
// Fail fast: check a fresh remotable to see if our rules fit. | ||
const throwawayRemotable = Object.create(oldRemotableProto); | ||
mutateHardenAndCheck(throwawayRemotable); | ||
mutateHardenAndCheck({}); | ||
@@ -906,3 +1053,2 @@ // Actually finish the new remotable. | ||
assert(iface !== undefined); // To make TypeScript happy | ||
remotableToInterface.set(remotable, iface); | ||
return remotable; | ||
@@ -913,1 +1059,15 @@ } | ||
export { Remotable }; | ||
/** | ||
* A concise convenience for the most common `Remotable` use. | ||
* | ||
* @param {string} farName This name will be prepended with `Alleged: ` | ||
* for now to form the `Remotable` `iface` argument. | ||
* @param {object} [remotable={}] The object used as the remotable | ||
* @returns {object} remotable, modified for debuggability | ||
*/ | ||
const Far = (farName, remotable = {}) => | ||
Remotable(`Alleged: ${farName}`, undefined, remotable); | ||
harden(Far); | ||
export { Far }; |
@@ -128,5 +128,11 @@ /** | ||
* @param {ConvertSlotToVal=} convertSlotToVal | ||
* @param {MakeMarshalOptions=} options | ||
* @returns {Marshal} | ||
*/ | ||
/** | ||
* @typedef MakeMarshalOptions | ||
* @property {string=} marshalName | ||
*/ | ||
// ///////////////////////////////////////////////////////////////////////////// | ||
@@ -133,0 +139,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
62036
1104
Updated@agoric/assert@^0.2.1
Updated@agoric/promise-kit@^0.2.1