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

feat(pass-style,marshal): ByteArray, a binary Passable #2414

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
17 changes: 16 additions & 1 deletion packages/marshal/src/encodePassable.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@endo/pass-style';

/**
* @import {CopyRecord, PassStyle, Passable, RemotableObject as Remotable} from '@endo/pass-style'
* @import {CopyRecord, PassStyle, Passable, RemotableObject as Remotable, ByteArray} from '@endo/pass-style'
*/

import { b, q, Fail } from '@endo/errors';
Expand Down Expand Up @@ -462,6 +462,17 @@ const decodeLegacyArray = (encoded, decodePassable, skip = 0) => {
return harden(elements);
};

/**
* @param {ByteArray} byteArray
* @param {(byteArray: ByteArray) => string} _encodePassable
* @returns {string}
*/
const encodeByteArray = (byteArray, _encodePassable) => {
// TODO implement
Fail`encodePassable(copyData) not yet implemented: ${byteArray}`;
return ''; // Just for the type
};

const encodeRecord = (record, encodeArray, encodePassable) => {
const names = recordNames(record);
const values = recordValues(record, names);
Expand Down Expand Up @@ -626,6 +637,9 @@ const makeInnerEncode = (encodeStringSuffix, encodeArray, options) => {
case 'copyArray': {
return encodeArray(passable, innerEncode);
}
case 'byteArray': {
return encodeByteArray(passable, innerEncode);
}
case 'copyRecord': {
return encodeRecord(passable, encodeArray, innerEncode);
}
Expand Down Expand Up @@ -870,6 +884,7 @@ export const passStylePrefixes = {
tagged: ':',
promise: '?',
copyArray: '[^',
byteArray: 'a',
boolean: 'b',
number: 'f',
bigint: 'np',
Expand Down
4 changes: 4 additions & 0 deletions packages/marshal/src/encodeToCapData.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ export const makeEncodeToCapData = (encodeOptions = {}) => {
case 'copyArray': {
return passable.map(encodeToCapDataRecur);
}
case 'byteArray': {
// TODO implement
throw Fail`marsal of byteArray not yet implemented: ${passable}`;
}
case 'tagged': {
return {
[QCLASS]: 'tagged',
Expand Down
4 changes: 4 additions & 0 deletions packages/marshal/src/encodeToSmallcaps.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ export const makeEncodeToSmallcaps = (encodeOptions = {}) => {
case 'copyArray': {
return passable.map(encodeToSmallcapsRecur);
}
case 'byteArray': {
// TODO implement
throw Fail`marsal of byteArray not yet implemented: ${passable}`;
}
case 'tagged': {
return {
'#tag': encodeToSmallcapsRecur(getTag(passable)),
Expand Down
22 changes: 22 additions & 0 deletions packages/marshal/src/rankOrder.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export const trivialComparator = (left, right) =>
const passStyleRanks = /** @type {PassStyleRanksRecord} */ (
fromEntries(
entries(passStylePrefixes)
// TODO Until byteArray prefix is chosen
.filter(([_style, prefixes]) => prefixes.length >= 1)
// Sort entries by ascending prefix.
.sort(([_leftStyle, leftPrefixes], [_rightStyle, rightPrefixes]) => {
return trivialComparator(leftPrefixes, rightPrefixes);
Expand Down Expand Up @@ -209,6 +211,26 @@ export const makeComparatorKit = (compareRemotables = (_x, _y) => NaN) => {
// If array X is a prefix of array Y, then X has an earlier rank than Y.
return comparator(left.length, right.length);
}
case 'byteArray': {
const leftArray = new Uint8Array(left.slice(0));
const rightArray = new Uint8Array(right.slice(0));
const byteLen = Math.min(left.byteLength, right.byteLength);
for (let i = 0; i < byteLen; i += 1) {
const leftByte = leftArray[i];
const rightByte = rightArray[i];
if (leftByte < rightByte) {
return -1;
}
if (leftByte > rightByte) {
return 1;
}
}
// If all corresponding bytes are the same,
// then according to their lengths.
// Thus, if the data of ByteArray X is a prefix of
// the data of ByteArray Y, then X is smaller than Y.
return comparator(left.byteLength, right.byteLength);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should switch to shortlex. See https://en.wikipedia.org/wiki/Shortlex_order

That way, encodePassable could encode as prefix followed by the unescaped content.

}
case 'tagged': {
// Lexicographic by `[Symbol.toStringTag]` then `.payload`.
const labelComp = comparator(getTag(left), getTag(right));
Expand Down
6 changes: 4 additions & 2 deletions packages/marshal/test/marshal-stringify.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ test('marshal stringify errors', t => {
t.throws(() => stringify({}), {
message: /Cannot pass non-frozen objects like .*. Use harden()/,
});
// @ts-expect-error intentional error
// at-ts-ignore rather than at-expect-error because of disagreement
// @ts-ignore intentional error
t.throws(() => stringify(harden(new Uint8Array(1))), {
message: 'Cannot pass mutable typed arrays like "[Uint8Array]".',
});
// @ts-expect-error intentional error
// at-ts-ignore rather than at-expect-error because of disagreement
// @ts-ignore intentional error
t.throws(() => stringify(harden(new Int16Array(1))), {
message: 'Cannot pass mutable typed arrays like "[Int16Array]".',
});
Expand Down
1 change: 1 addition & 0 deletions packages/pass-style/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@endo/env-options": "workspace:^",
"@endo/errors": "workspace:^",
"@endo/eventual-send": "workspace:^",
"@endo/immutable-arraybuffer": "workspace:^",
"@endo/promise-kit": "workspace:^",
"@fast-check/ava": "^1.1.5"
},
Expand Down
57 changes: 57 additions & 0 deletions packages/pass-style/src/byteArray.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { X } from '@endo/errors';
import {
transferBufferToImmutable,
isBufferImmutable,
} from '@endo/immutable-arraybuffer';
import { assertChecker } from './passStyle-helpers.js';

const { getPrototypeOf, getOwnPropertyDescriptor } = Object;
const { ownKeys, apply } = Reflect;

const AnImmutableArrayBuffer = transferBufferToImmutable(new ArrayBuffer(0));

/**
* As proposed, this will be the same as `ArrayBuffer.prototype`. As shimmed,
* this will be a hidden intrinsic that inherits from `ArrayBuffer.prototype`.
* Either way, get this in a way that we can trust it after lockdown, and
* require that all immutable ArrayBuffers directly inherit from it.
*/
const ImmutableArrayBufferPrototype = getPrototypeOf(AnImmutableArrayBuffer);

// @ts-expect-error ok to implicitly assert the access is found
const immutableGetter = getOwnPropertyDescriptor(
ImmutableArrayBufferPrototype,
'immutable',
).get;

/**
* @param {unknown} candidate
* @param {import('./types.js').Checker} [check]
* @returns {boolean}
*/
const canBeValid = (candidate, check = undefined) =>
(candidate instanceof ArrayBuffer && isBufferImmutable(candidate)) ||
(!!check && check(false, X`Immutable ArrayBuffer expected: ${candidate}`));

/**
* @type {import('./internal-types.js').PassStyleHelper}
*/
export const ByteArrayHelper = harden({
styleName: 'byteArray',

canBeValid,

assertValid: (candidate, _passStyleOfRecur) => {
canBeValid(candidate, assertChecker);
getPrototypeOf(candidate) === ImmutableArrayBufferPrototype ||
assert.fail(X`Malformed ByteArray ${candidate}`, TypeError);
// @ts-expect-error assume immutableGetter was found
apply(immutableGetter, candidate, []) ||
assert.fail(X`Must be an immutable ArrayBuffer: ${candidate}`);
ownKeys(candidate).length === 0 ||
assert.fail(
X`ByteArrays must not have own properties: ${candidate}`,
TypeError,
);
},
});
9 changes: 8 additions & 1 deletion packages/pass-style/src/deeplyFulfilled.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { passStyleOf } from './passStyleOf.js';
import { makeTagged } from './makeTagged.js';

/**
* @import {Passable, Primitive, CopyRecord, CopyArray, CopyTagged, RemotableObject} from '@endo/pass-style'
* @import {Passable, ByteArray, CopyRecord, CopyArray, CopyTagged, RemotableObject} from '@endo/pass-style'
*/

const { ownKeys } = Reflect;
Expand Down Expand Up @@ -105,6 +105,13 @@ export const deeplyFulfilled = async val => {
// @ts-expect-error not assignable to type 'DeeplyAwaited<T>'
return E.when(Promise.all(valPs), vals => harden(vals));
}
case 'byteArray': {
const bytes = /** @type {ByteArray} */ (val);
// @ts-expect-error Why
// "Type 'ArrayBuffer' is not assignable to type 'DeeplyAwaited<T>'."?
// TODO fix.
return bytes;
}
case 'tagged': {
const tgd = /** @type {CopyTagged} */ (val);
const tag = getTag(tgd);
Expand Down
3 changes: 3 additions & 0 deletions packages/pass-style/src/passStyleOf.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { X, Fail, q, annotateError, makeError } from '@endo/errors';
import { isObject, isTypedArray, PASS_STYLE } from './passStyle-helpers.js';

import { CopyArrayHelper } from './copyArray.js';
import { ByteArrayHelper } from './byteArray.js';
import { CopyRecordHelper } from './copyRecord.js';
import { TaggedHelper } from './tagged.js';
import {
Expand Down Expand Up @@ -43,6 +44,7 @@ const makeHelperTable = passStyleHelpers => {
const HelperTable = {
__proto__: null,
copyArray: undefined,
byteArray: undefined,
copyRecord: undefined,
tagged: undefined,
error: undefined,
Expand Down Expand Up @@ -216,6 +218,7 @@ export const passStyleOf =
(globalThis && globalThis[PassStyleOfEndowmentSymbol]) ||
makePassStyleOf([
CopyArrayHelper,
ByteArrayHelper,
CopyRecordHelper,
TaggedHelper,
ErrorHelper,
Expand Down
34 changes: 33 additions & 1 deletion packages/pass-style/src/typeGuards.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Fail, q } from '@endo/errors';
import { passStyleOf } from './passStyleOf.js';

/** @import {CopyArray, CopyRecord, Passable, RemotableObject} from './types.js' */
/**
* @import {CopyArray, CopyRecord, Passable, RemotableObject, ByteArray} from './types.js'
*/

/**
* Check whether the argument is a pass-by-copy array, AKA a "copyArray"
Expand All @@ -13,6 +15,16 @@
const isCopyArray = arr => passStyleOf(arr) === 'copyArray';
harden(isCopyArray);

/**
* Check whether the argument is a pass-by-copy binary data, AKA a "byteArray"
* in @endo/marshal terms
*
* @param {Passable} arr
* @returns {arr is ByteArray}
*/
const isByteArray = arr => passStyleOf(arr) === 'byteArray';
harden(isByteArray);

/**
* Check whether the argument is a pass-by-copy record, AKA a
* "copyRecord" in @endo/marshal terms
Expand All @@ -35,7 +47,7 @@
/**
* @param {any} array
* @param {string=} optNameOfArray
* @returns {asserts array is CopyArray<any>}

Check warning on line 50 in packages/pass-style/src/typeGuards.js

View workflow job for this annotation

GitHub Actions / lint

Invalid JSDoc @returns type "array"; prefer: "Array"
*/
const assertCopyArray = (array, optNameOfArray = 'Alleged array') => {
const passStyle = passStyleOf(array);
Expand All @@ -47,6 +59,24 @@
harden(assertCopyArray);

/**
* @callback AssertByteArray
* @param {Passable} array
* @param {string=} optNameOfArray
* @returns {asserts array is ByteArray}

Check warning on line 65 in packages/pass-style/src/typeGuards.js

View workflow job for this annotation

GitHub Actions / lint

Invalid JSDoc @returns type "array"; prefer: "Array"
*/

/** @type {AssertByteArray} */
const assertByteArray = (array, optNameOfArray = 'Alleged byteArray') => {
const passStyle = passStyleOf(array);
passStyle === 'byteArray' ||
Fail`${q(
optNameOfArray,
)} ${array} must be a pass-by-copy binary data, not ${q(passStyle)}`;
};
harden(assertByteArray);

/**
* @callback AssertRecord
* @param {any} record
* @param {string=} optNameOfRecord
* @returns {asserts record is CopyRecord<any>}
Expand Down Expand Up @@ -80,8 +110,10 @@
export {
assertRecord,
assertCopyArray,
assertByteArray,
assertRemotable,
isRemotable,
isRecord,
isCopyArray,
isByteArray,
};
22 changes: 17 additions & 5 deletions packages/pass-style/src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ export type PrimitiveStyle =
| 'string'
| 'symbol';

export type ContainerStyle = 'copyRecord' | 'copyArray' | 'tagged';
export type ContainerStyle =
| 'copyRecord'
| 'copyArray'
| 'byteArray'
| 'tagged';

export type PassStyle =
| PrimitiveStyle
Expand All @@ -49,6 +53,7 @@ export type PassByCopy =
| Primitive
| Error
| CopyArray
| ByteArray
| CopyRecord
| CopyTagged;

Expand All @@ -67,6 +72,7 @@ export type PassByRef =
* | 'string' | 'symbol'). (Passable considers `void` to be `undefined`.)
* * Containers aggregate other Passables into
* * sequences as CopyArrays (PassStyle 'copyArray'), or
* * sequences of 8-bit bytes (PassStyle 'byteArray'), or
* * string-keyed dictionaries as CopyRecords (PassStyle 'copyRecord'), or
* * higher-level types as CopyTaggeds (PassStyle 'tagged').
* * PassableCaps (PassStyle 'remotable' | 'promise') expose local values to
Expand All @@ -86,10 +92,12 @@ export type Passable<

export type Container<PC extends PassableCap, E extends Error> =
| CopyArrayI<PC, E>
| ByteArrayI
| CopyRecordI<PC, E>
| CopyTaggedI<PC, E>;
interface CopyArrayI<PC extends PassableCap, E extends Error>
extends CopyArray<Passable<PC, E>> {}
interface ByteArrayI extends ByteArray {}
interface CopyRecordI<PC extends PassableCap, E extends Error>
extends CopyRecord<Passable<PC, E>> {}
interface CopyTaggedI<PC extends PassableCap, E extends Error>
Expand All @@ -116,10 +124,9 @@ export type PassStyleOf = {
/**
* A Passable is PureData when its entire data structure is free of PassableCaps
* (remotables and promises) and error objects.
* PureData is an arbitrary composition of primitive values into CopyArray
* and/or
* CopyRecord and/or CopyTagged containers (or a single primitive value with no
* container), and is fully pass-by-copy.
* PureData is an arbitrary composition of primitive values into CopyArray,
* ByteArray, CopyRecord, and/or CopyTagged containers
* (or a single primitive value with no container), and is fully pass-by-copy.
*
* This restriction assures absence of side effects and interleaving risks *given*
* that none of the containers can be a Proxy instance.
Expand Down Expand Up @@ -156,6 +163,11 @@ export type PassableCap = Promise<any> | RemotableObject;
*/
export type CopyArray<T extends Passable = any> = Array<T>;

/**
* A `ByteArray` is a normal hardened immutable `ArrayBuffer`
*/
export type ByteArray = ArrayBuffer;

/**
* A Passable dictionary in which each key is a string and each value is Passable.
*/
Expand Down
3 changes: 3 additions & 0 deletions packages/patterns/src/keys/checkKey.js
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,9 @@ const checkKeyInternal = (val, check) => {
// A copyArray is a key iff all its children are keys
return val.every(checkIt);
}
case 'byteArray': {
return true;
}
case 'tagged': {
const tag = getTag(val);
switch (tag) {
Expand Down
Loading
Loading