Skip to content
Merged
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
8 changes: 7 additions & 1 deletion packages/rrweb/src/replay/canvas/2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ export default async function canvasMutation({
target,
imageMap,
errorHandler,
enforceCanvasArgAllowlist,
}: {
event: Parameters<Replayer['applyIncremental']>[0];
mutations: canvasMutationCommand[];
target: HTMLCanvasElement;
imageMap: Replayer['imageMap'];
errorHandler: Replayer['warnCanvasMutationFailed'];
enforceCanvasArgAllowlist?: boolean;
}): Promise<void> {
const ctx = target.getContext('2d');

Expand All @@ -29,7 +31,11 @@ export default async function canvasMutation({
// step 1, deserialize args, they may be async
const mutationArgsPromises = mutations.map(
async (mutation: canvasMutationCommand): Promise<unknown[]> => {
return Promise.all(mutation.args.map(deserializeArg(imageMap, ctx)));
return Promise.all(
mutation.args.map(
deserializeArg(imageMap, ctx, undefined, enforceCanvasArgAllowlist),
),
);
},
);
const args = await Promise.all(mutationArgsPromises);
Expand Down
53 changes: 49 additions & 4 deletions packages/rrweb/src/replay/canvas/deserialize-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type { CanvasArg, SerializedCanvasArg } from '@rrweb/types';

// TODO: add ability to wipe this list
type GLVarMap = Map<string, any[]>;

Check warning on line 6 in packages/rrweb/src/replay/canvas/deserialize-args.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/canvas/deserialize-args.ts#L6

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
const webGLVarMap: Map<
CanvasRenderingContext2D | WebGLRenderingContext | WebGL2RenderingContext,
GLVarMap
Expand All @@ -24,9 +24,36 @@
contextMap.set(ctor, []);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return contextMap.get(ctor) as any[];

Check warning on line 27 in packages/rrweb/src/replay/canvas/deserialize-args.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/canvas/deserialize-args.ts#L27

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
}

/**
* The `rr_type` values that the canvas recorder rebuilds by calling a
* constructor of that name — see `record/observers/canvas/serialize-args.ts`
* for the matching serialization. A recording is untrusted input at replay
* time, so `rr_type` is matched against this set rather than resolved as an
* arbitrary global.
*/
const canvasArgConstructors = new Set([
'Int8Array',
'Int16Array',
'Int32Array',
'Uint8Array',
'Uint8ClampedArray',
'Uint16Array',
'Uint32Array',
'Float32Array',
'Float64Array',
'DataView',
'ImageData',
// normally serialized as base64, but reachable in `args` form through
// recordings made by older clients and through nested `DataView` args.
'ArrayBuffer',
// wraps nested args in recordings made by older clients — see the
// `preloadAllImages` tests for the shape.
'Array',
]);

export function isSerializedArg(arg: unknown): arg is SerializedCanvasArg {
return Boolean(arg && typeof arg === 'object' && 'rr_type' in arg);
}
Expand All @@ -41,13 +68,19 @@
preload?: {
isUnchanged: boolean;
},
enforceCanvasArgAllowlist = false,
): (arg: CanvasArg) => Promise<any> {

Check warning on line 72 in packages/rrweb/src/replay/canvas/deserialize-args.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/canvas/deserialize-args.ts#L72

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
return async (arg: CanvasArg): Promise<any> => {

Check warning on line 73 in packages/rrweb/src/replay/canvas/deserialize-args.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/canvas/deserialize-args.ts#L73

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
if (arg && typeof arg === 'object' && 'rr_type' in arg) {
if (preload) preload.isUnchanged = false;
if (arg.rr_type === 'ImageBitmap' && 'args' in arg) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const args = await deserializeArg(imageMap, ctx, preload)(arg.args);
const args = await deserializeArg(
imageMap,
ctx,
preload,
enforceCanvasArgAllowlist,
)(arg.args);
// eslint-disable-next-line prefer-spread
return await createImageBitmap.apply(null, args);
} else if ('index' in arg) {
Expand All @@ -57,13 +90,21 @@
return variableListFor(ctx, name)[index];
} else if ('args' in arg) {
const { rr_type: name, args } = arg;
if (enforceCanvasArgAllowlist && !canvasArgConstructors.has(name)) {
console.warn(
`[replayer] refusing to construct canvas arg of unknown type: ${name}`,
);
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const ctor = window[name as keyof Window];

// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call
return new ctor(
...(await Promise.all(
args.map(deserializeArg(imageMap, ctx, preload)),
args.map(
deserializeArg(imageMap, ctx, preload, enforceCanvasArgAllowlist),
),
)),
);
} else if ('base64' in arg) {
Expand All @@ -80,7 +121,9 @@
}
} else if ('data' in arg && arg.rr_type === 'Blob') {
const blobContents = await Promise.all(
arg.data.map(deserializeArg(imageMap, ctx, preload)),
arg.data.map(
deserializeArg(imageMap, ctx, preload, enforceCanvasArgAllowlist),
),
);
const blob = new Blob(blobContents, {
type: arg.type,
Expand All @@ -89,7 +132,9 @@
}
} else if (Array.isArray(arg)) {
const result = await Promise.all(
arg.map(deserializeArg(imageMap, ctx, preload)),
arg.map(
deserializeArg(imageMap, ctx, preload, enforceCanvasArgAllowlist),
),
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return result;
Expand Down
4 changes: 4 additions & 0 deletions packages/rrweb/src/replay/canvas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,15 @@ export default async function canvasMutation({
imageMap,
canvasEventMap,
errorHandler,
enforceCanvasArgAllowlist,
}: {
event: Parameters<Replayer['applyIncremental']>[0];
mutation: canvasMutationData;
target: HTMLCanvasElement;
imageMap: Replayer['imageMap'];
canvasEventMap: Replayer['canvasEventMap'];
errorHandler: Replayer['warnCanvasMutationFailed'];
enforceCanvasArgAllowlist?: boolean;
}): Promise<void> {
try {
const precomputedMutation: canvasMutationParam =
Expand All @@ -41,6 +43,7 @@ export default async function canvasMutation({
target,
imageMap,
errorHandler,
enforceCanvasArgAllowlist,
});
}
return;
Expand All @@ -52,6 +55,7 @@ export default async function canvasMutation({
target,
imageMap,
errorHandler,
enforceCanvasArgAllowlist,
});
} catch (error) {
errorHandler(mutation, error);
Expand Down
6 changes: 5 additions & 1 deletion packages/rrweb/src/replay/canvas/webgl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
try {
if (type === CanvasContext.WebGL) {
return (
target.getContext('webgl')! || target.getContext('experimental-webgl')

Check warning on line 15 in packages/rrweb/src/replay/canvas/webgl.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/canvas/webgl.ts#L15

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
);
}
return target.getContext('webgl2')!;

Check warning on line 18 in packages/rrweb/src/replay/canvas/webgl.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/canvas/webgl.ts#L18

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
} catch (e) {
return null;
}
Expand All @@ -36,7 +36,7 @@

function saveToWebGLVarMap(
ctx: WebGLRenderingContext | WebGL2RenderingContext,
result: any,

Check warning on line 39 in packages/rrweb/src/replay/canvas/webgl.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/canvas/webgl.ts#L39

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!result?.constructor) return; // probably null or undefined
Expand All @@ -57,12 +57,14 @@
type,
imageMap,
errorHandler,
enforceCanvasArgAllowlist,
}: {
mutation: canvasMutationCommand;
target: HTMLCanvasElement;
type: CanvasContext;
imageMap: Replayer['imageMap'];
errorHandler: Replayer['warnCanvasMutationFailed'];
enforceCanvasArgAllowlist?: boolean;
}): Promise<void> {
try {
const ctx = getContext(target, type);
Expand All @@ -75,7 +77,7 @@
if (mutation.setter) {
// skip some read-only type checks
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(ctx as any)[mutation.property] = mutation.args[0];

Check warning on line 80 in packages/rrweb/src/replay/canvas/webgl.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/canvas/webgl.ts#L80

[@typescript-eslint/no-explicit-any] Unexpected any. Specify a different type.
return;
}
const original = ctx[
Expand All @@ -86,7 +88,9 @@
) => void;

const args = await Promise.all(
mutation.args.map(deserializeArg(imageMap, ctx)),
mutation.args.map(
deserializeArg(imageMap, ctx, undefined, enforceCanvasArgAllowlist),
),
);
const result = original.apply(ctx, args);
saveToWebGLVarMap(ctx, result);
Expand Down
1 change: 1 addition & 0 deletions packages/rrweb/src/replay/embedded/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const SERIALIZABLE_CONFIG_KEYS = [
'insertStyleRules',
'triggerFocus',
'UNSAFE_replayCanvas',
'enforceCanvasArgAllowlist',
'cspContent',
'pauseAnimation',
'mouseTail',
Expand Down
21 changes: 19 additions & 2 deletions packages/rrweb/src/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@
insertStyleRules: [],
triggerFocus: true,
UNSAFE_replayCanvas: false,
enforceCanvasArgAllowlist: false,
pauseAnimation: true,
mouseTail: defaultMouseTailConfig,
useVirtualDom: true, // Virtual-dom optimization is enabled by default.
Expand Down Expand Up @@ -243,6 +244,7 @@
imageMap: this.imageMap,
canvasEventMap: this.canvasEventMap,
errorHandler: this.warnCanvasMutationFailed.bind(this),
enforceCanvasArgAllowlist: this.config.enforceCanvasArgAllowlist,
});
},
applyInput: this.applyInput.bind(this),
Expand Down Expand Up @@ -1091,12 +1093,12 @@
if (builtNode.nodeName === 'DIALOG')
collectedDialogs.add(builtNode as HTMLDialogElement);
this.collectIframeAndAttachDocument(collectedIframes, builtNode);
const sn = (mirror as TMirror).getMeta(builtNode as unknown as TNode);

Check warning on line 1096 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1096

[@typescript-eslint/no-unsafe-assignment] Unsafe assignment of an `any` value.
if (
sn?.type === NodeType.Element &&

Check warning on line 1098 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1098

[@typescript-eslint/no-unsafe-member-access] Unsafe member access .type on an `any` value.
sn?.tagName.toUpperCase() === 'HTML'

Check warning on line 1099 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1099

[@typescript-eslint/no-unsafe-member-access] Unsafe member access .tagName on an `any` value.

Check warning on line 1099 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1099

[@typescript-eslint/no-unsafe-call] Unsafe call of an `any` typed value.
) {
const { documentElement, head } = iframeEl.contentDocument!;

Check warning on line 1101 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1101

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
this.insertStyleRules(
documentElement as HTMLElement | RRElement,
head as HTMLElement | RRElement,
Expand All @@ -1115,14 +1117,14 @@
};

buildNodeWithSN(mutation.node, {
doc: iframeEl.contentDocument! as Document,

Check warning on line 1120 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1120

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
mirror: mirror as Mirror,
hackCss: true,
skipChild: false,
afterAppend,
cache: this.cache,
});
afterAppend(iframeEl.contentDocument! as Document, mutation.node.id);

Check warning on line 1127 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1127

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.

for (const { mutationInQueue, builtNode } of collectedIframes) {
this.attachDocumentToIframe(mutationInQueue, builtNode);
Expand Down Expand Up @@ -1239,7 +1241,7 @@
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const imgd = ctx?.createImageData(canvas.width, canvas.height);
ctx?.putImageData(imgd!, 0, 0);

Check warning on line 1244 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1244

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
}
}
private async deserializeAndPreloadCanvasEvents(
Expand All @@ -1254,7 +1256,14 @@
const commands = await Promise.all(
data.commands.map(async (c) => {
const args = await Promise.all(
c.args.map(deserializeArg(this.imageMap, null, status)),
c.args.map(
deserializeArg(
this.imageMap,
null,
status,
this.config.enforceCanvasArgAllowlist,
),
),
);
return { ...c, args };
}),
Expand All @@ -1263,7 +1272,14 @@
this.canvasEventMap.set(event, { ...data, commands });
} else {
const args = await Promise.all(
data.args.map(deserializeArg(this.imageMap, null, status)),
data.args.map(
deserializeArg(
this.imageMap,
null,
status,
this.config.enforceCanvasArgAllowlist,
),
),
);
if (status.isUnchanged === false)
this.canvasEventMap.set(event, { ...data, args });
Expand Down Expand Up @@ -1515,6 +1531,7 @@
imageMap: this.imageMap,
canvasEventMap: this.canvasEventMap,
errorHandler: this.warnCanvasMutationFailed.bind(this),
enforceCanvasArgAllowlist: this.config.enforceCanvasArgAllowlist,
});
}
break;
Expand Down Expand Up @@ -1560,7 +1577,7 @@
// Only apply virtual dom optimization if the fast-forward process has node mutation. Because the cost of creating a virtual dom tree and executing the diff algorithm is usually higher than directly applying other kind of events.
if (this.config.useVirtualDom && !this.usingVirtualDom && isSync) {
this.usingVirtualDom = true;
buildFromDom(this.iframe.contentDocument!, this.mirror, this.virtualDom);

Check warning on line 1580 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1580

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
// If these legacy missing nodes haven't been resolved, they should be converted to virtual nodes.
if (Object.keys(this.legacy_missingNodeRetryMap).length) {
for (const key in this.legacy_missingNodeRetryMap) {
Expand Down Expand Up @@ -1677,7 +1694,7 @@
// If the parent is attached a shadow dom after it's created, it won't have a shadow root.
if (!hasShadowRoot(parent)) {
(parent as Element | RRElement).attachShadow({ mode: 'open' });
parent = (parent as Element | RRElement).shadowRoot! as Node | RRNode;

Check warning on line 1697 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1697

[@typescript-eslint/no-non-null-assertion] Forbidden non-null assertion.
} else parent = parent.shadowRoot as Node | RRNode;
}

Expand Down Expand Up @@ -1745,16 +1762,16 @@
type TNode = typeof mirror extends Mirror ? Node : RRNode;
type TMirror = typeof mirror extends Mirror ? Mirror : RRDOMMirror;

const parentSn = (mirror as TMirror).getMeta(parent as TNode);

Check warning on line 1765 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1765

[@typescript-eslint/no-unsafe-assignment] Unsafe assignment of an `any` value.
if (
parentSn &&
parentSn.type === NodeType.Element &&

Check warning on line 1768 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1768

[@typescript-eslint/no-unsafe-member-access] Unsafe member access .type on an `any` value.
mutation.node.type === NodeType.Text
) {
const prospectiveSiblings = Array.isArray(parent.childNodes)
? parent.childNodes
: Array.from(parent.childNodes);
if (parentSn.tagName === 'textarea') {

Check warning on line 1774 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1774

[@typescript-eslint/no-unsafe-member-access] Unsafe member access .tagName on an `any` value.
// This should be redundant now as we are either recording the value or the childNode, and not both
// keeping around for backwards compatibility with old bad double data, see

Expand All @@ -1766,7 +1783,7 @@
}
}
} else if (
parentSn.tagName === 'style' &&

Check warning on line 1786 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1786

[@typescript-eslint/no-unsafe-member-access] Unsafe member access .tagName on an `any` value.
prospectiveSiblings.length === 1
) {
// https://github.com/rrweb-io/rrweb/pull/1417
Expand All @@ -1784,7 +1801,7 @@
}
}
}
} else if (parentSn?.type === NodeType.Document) {

Check warning on line 1804 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L1804

[@typescript-eslint/no-unsafe-member-access] Unsafe member access .type on an `any` value.
/**
* Sometimes the document object is changed or reopened and the MutationObserver is disconnected, so the removal of child elements can't be detected and recorded.
* After the change of document, we may get another mutation which adds a new doctype or a HTML element, while the old one still exists in the dom.
Expand Down Expand Up @@ -2054,14 +2071,14 @@
if (!target) {
return this.debugNodeNotFound(d, d.id);
}
const sn = this.mirror.getMeta(target);

Check warning on line 2074 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L2074

[@typescript-eslint/no-unsafe-assignment] Unsafe assignment of an `any` value.
if (target === this.iframe.contentDocument) {
this.iframe.contentWindow?.scrollTo({
top: d.y,
left: d.x,
behavior: isSync ? 'auto' : 'smooth',
});
} else if (sn?.type === NodeType.Document) {

Check warning on line 2081 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L2081

[@typescript-eslint/no-unsafe-member-access] Unsafe member access .type on an `any` value.
// nest iframe content document
(target as Document).defaultView?.scrollTo({
top: d.y,
Expand Down Expand Up @@ -2213,7 +2230,7 @@
* that hasn't been processed yet
* - Dynamic stylesheets: Constructed stylesheets or adopted stylesheets
* may not be fully synchronized
* - Nested rules: Rules inside @media/@supports require the parent rule

Check warning on line 2233 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L2233

[tsdoc/syntax] tsdoc-characters-after-block-tag: The token "@media" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@"

Check warning on line 2233 in packages/rrweb/src/replay/index.ts

View workflow job for this annotation

GitHub Actions / ESLint Report Analysis

packages/rrweb/src/replay/index.ts#L2233

[tsdoc/syntax] tsdoc-at-sign-in-word: The "@" character looks like part of a TSDoc tag; use a backslash to escape it
* to exist first
*/
private applyStyleDeclaration(
Expand Down
8 changes: 8 additions & 0 deletions packages/rrweb/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ export type playerConfig = {
insertStyleRules: string[];
triggerFocus: boolean;
UNSAFE_replayCanvas: boolean;
/**
* Rebuild canvas mutation args only from the constructors the recorder
* emits, dropping any other `rr_type` instead of resolving it off `window`
* (see `replay/canvas/deserialize-args.ts`). Off by default so the embedding
* app can roll it out; canvas replay of args this list doesn't cover
* degrades to a blank draw rather than failing the session.
*/
enforceCanvasArgAllowlist: boolean;
/**
* Optional Content-Security-Policy applied to the replay iframe. When set, a
* `<meta http-equiv="Content-Security-Policy">` carrying this policy is added
Expand Down
117 changes: 117 additions & 0 deletions packages/rrweb/test/replay/deserialize-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,123 @@ describe('deserializeArg', () => {
expect(deserialized.size).toEqual(expected.size);
});

describe('constructor allowlist', () => {
// the allowlist is opt-in; `enforceCanvasArgAllowlist` is the 4th arg
const enforcing = () => deserializeArg(new Map(), context, undefined, true);

const recorderEmittedTypes = [
'Int8Array',
'Int16Array',
'Int32Array',
'Uint8Array',
'Uint8ClampedArray',
'Uint16Array',
'Uint32Array',
'Float32Array',
'Float64Array',
];

it.each(recorderEmittedTypes)(
'should deserialize %s values while enforcing',
async (rr_type) => {
expect(await enforcing()({ rr_type, args: [[1, 2, 3, 4]] })).toEqual(
new (window[rr_type as keyof Window] as Uint8ArrayConstructor)([
1, 2, 3, 4,
]),
);
},
);

it('should deserialize ImageData while enforcing', async () => {
expect(
await enforcing()({
rr_type: 'ImageData',
args: [{ rr_type: 'Uint8ClampedArray', args: [[1, 2, 3, 4]] }, 1, 1],
}),
).toEqual(new ImageData(new Uint8ClampedArray([1, 2, 3, 4]), 1, 1));
});

it('should deserialize DataView over a base64 ArrayBuffer while enforcing', async () => {
expect(
await enforcing()({
rr_type: 'DataView',
args: [
{ rr_type: 'ArrayBuffer', base64: 'AAAAAAAAAAAAAAAAAAAAAA==' },
0,
16,
],
}),
).toStrictEqual(new DataView(new ArrayBuffer(16), 0, 16));
});

it('should deserialize the Array wrapper older clients emit', async () => {
expect(
await enforcing()({
rr_type: 'Array',
args: [{ rr_type: 'Float32Array', args: [[1, 2]] }],
}),
).toEqual([new Float32Array([1, 2])]);
});

it('should leave the src and data branches alone while enforcing', async () => {
const image = new Image();
image.src = 'http://example.com/image.png';
expect(
await enforcing()({
rr_type: 'HTMLImageElement',
src: 'http://example.com/image.png',
}),
).toStrictEqual(image);

expect(
await enforcing()({
rr_type: 'Blob',
data: [{ rr_type: 'ArrayBuffer', base64: 'AQIABA==' }],
type: 'image/png',
}),
).toEqual(
new Blob([new Uint8Array([1, 2, 0, 4]).buffer], {
type: 'image/png',
}),
);
});

it.each(['Function', 'Promise', 'XMLHttpRequest', 'Object'])(
'should not construct %s while enforcing',
async (rr_type) => {
expect(await enforcing()({ rr_type, args: [] })).toBeNull();
},
);

it('should not evaluate a string passed to a type it does not know', async () => {
const canary = '__deserializeArgCanary';
delete (globalThis as Record<string, unknown>)[canary];

const deserialized = await enforcing()({
rr_type: 'Function',
args: [`globalThis[${JSON.stringify(canary)}] = true;`],
});

expect(deserialized).toBeNull();
expect((globalThis as Record<string, unknown>)[canary]).toBeUndefined();
});

it('should reject unknown types nested in an arg list', async () => {
expect(
await enforcing()([1, { rr_type: 'Function', args: ['return 1'] }, 3]),
).toEqual([1, null, 3]);
});

it('should not filter anything when enforcement is off (the default)', async () => {
expect(
await deserializeArg(
new Map(),
context,
)({ rr_type: 'Object', args: [] }),
).toEqual({});
});
});

describe('isUnchanged', () => {
it('should set isUnchanged:true when non of the args are changed', async () => {
const status = {
Expand Down
Loading
Loading