import { afterEach, describe, expect, it, vi } from 'vitest'; import { evaluateScriptCallback, evaluateScriptData } from './evaluate-script'; afterEach(() => { vi.unstubAllGlobals(); }); describe('serialized script data', () => { it.each(['__DATA__', 'window.__DATA__', 'globalThis.__DATA__', 'self.__DATA__'])('normalizes the global target %s', async (target) => { await expect(evaluateScriptData(String.raw`window.__DATA__ = { title: "Hello\u0020世界", tags: ["a", "b"] };`, target)).resolves.toEqual({ title: 'Hello 世界', tags: ['a', 'b'] }); }); it('resolves variables, nested assignments, and data-only unary and logical expressions', async () => { const source = 'var title="A & B", unused=analytics(); window.pixiv=window.pixiv||{}; pixiv.novel={title, count:-2, available:!0, absent:void 0};'; await expect(evaluateScriptData(source, 'window.pixiv.novel')).resolves.toEqual({ title: 'A & B', count: -2, available: true, absent: undefined }); }); it('reads parent-object assignments and static bracket targets', async () => { await expect(evaluateScriptData('window.pixiv={novel:{title:"Book"}};', 'self["pixiv"].novel')).resolves.toEqual({ title: 'Book' }); }); it('reads Nuxt IIFE parameters and object mutations without changing outer bindings', async () => { const source = 'var a="outer";window.__NUXT__=(function(a,b,c){a.title=b;a.tags=c;return {data:[a,a],error:null}}({},"News",["tag"]));'; const value = await evaluateScriptData<{ data: Array<{ title: string; tags: string[] }>; error: null }>(source, '__NUXT__'); expect(value).toEqual({ data: [ { title: 'News', tags: ['tag'] }, { title: 'News', tags: ['tag'] }, ], error: null, }); expect(value.data[0]).toBe(value.data[1]); await expect(evaluateScriptData(source, 'a')).resolves.toBe('outer'); }); it('preserves repeated var declarations and evaluates unused IIFE arguments in order', async () => { const source = 'var a={value:1};var a;window.__DATA__=(function(x){return x})(a,a.value=2);'; await expect(evaluateScriptData(source, '__DATA__')).resolves.toEqual({ value: 2 }); }); it('reads bounded Nuxt sparse-array arguments without calling the host Array constructor', async () => { const source = 'window.__NUXT__=(function(a){a[1]={title:"News"};return {data:a}})(Array(3));'; const value = await evaluateScriptData<{ data: unknown[] }>(source, '__NUXT__'); expect(value.data).toHaveLength(3); expect(0 in value.data).toBe(false); expect(value.data[1]).toEqual({ title: 'News' }); expect(2 in value.data).toBe(false); }); it.each([ 'window.__DATA__=Array(-1);', 'window.__DATA__=Array(1.5);', 'window.__DATA__=Array(10000);', 'window.__DATA__=Array("3");', 'window.__DATA__=Array(1,2);', 'var Array=undefined;window.__DATA__=Array(3);', 'window.__DATA__=(function(Array){return Array(3)})(undefined);', 'window.__DATA__=(function(){var a=Array(3);var Array;return a})();', 'window.__DATA__=Array(3);function Array(){return ["shadowed"]}', 'window.__DATA__=(function Array(){return Array(3)})();', 'window.__DATA__=(function(){return Array(3);function Array(){return ["shadowed"]}})();', 'window.__DATA__=Array(3);if(false){var Array;}', 'window.__DATA__=Array(3);class Array {}', 'window.__DATA__=Array(3);var {constructorAlias:Array}={};', 'window.__DATA__=Array(3);var [Array]=[];', 'window.__DATA__=Array(3);var {nested:{Array=undefined}}={};', 'window.__DATA__=Array(3);var {...Array}={};', 'window.__DATA__=Array(3);if(false){var [Array]=[];}', 'window.__DATA__=(function(){return Array(3);try{}catch(error){var Array}})();', ])('rejects invalid or shadowed array constructors: %s', async (source) => { await expect(evaluateScriptData(source, '__DATA__')).rejects.toThrow(/bounded|Shadowed|Unsupported|simple variable names/); }); it('does not hoist bindings from nested functions or unrelated lexical blocks', async () => { const source = 'window.__DATA__=Array(3);function unrelated(){var Array;}if(false){let Array;class Other{}}'; const value = await evaluateScriptData(source, '__DATA__'); expect(value).toHaveLength(3); expect(0 in value).toBe(false); }); it('enforces a total complexity budget across multiple sparse arrays', async () => { await expect(evaluateScriptData('window.__DATA__=[Array(6000),Array(6000)];', '__DATA__')).rejects.toThrow('complexity limit'); }); it('does not call unrelated application code or access the host global object', async () => { const fetchSpy = vi.fn(); vi.stubGlobal('fetch', fetchSpy); const value = await evaluateScriptData('globalThis.__scriptDataSideEffect="changed";fetch("https://example.invalid/");window.__DATA__={ok:true};', '__DATA__'); expect(value).toEqual({ ok: true }); expect(fetchSpy).not.toHaveBeenCalled(); expect('__scriptDataSideEffect' in globalThis).toBe(false); }); it.each([ 'window.__DATA__ = fetch("https://example.invalid/");', 'window.__DATA__ = Function("return 42")();', 'window.__DATA__ = { get secret() { return fetch("https://example.invalid/"); } };', 'window.__DATA__ = (function(){ while(true){} return {}; })();', 'window.__DATA__ = (function(){ return {}; })(fetch("https://example.invalid/"));', ])('rejects unsupported expressions required by the target without executing them', async (source) => { const fetchSpy = vi.fn(); vi.stubGlobal('fetch', fetchSpy); await expect(evaluateScriptData(source, '__DATA__')).rejects.toThrow(/Unsupported|ordinary/); expect(fetchSpy).not.toHaveBeenCalled(); }); it.each(['__proto__', 'constructor', 'prototype'])('rejects the unsafe property %s', async (key) => { await expect(evaluateScriptData(`window.__DATA__={"${key}":{polluted:true}};`, '__DATA__')).rejects.toThrow('Unsafe script data property'); await expect(evaluateScriptData(`var value={};value["${key}"]={polluted:true};window.__DATA__=value;`, '__DATA__')).rejects.toThrow('Unsafe script data property'); expect(Object.hasOwn(Object.prototype, 'polluted')).toBe(false); }); it.each(['window.__DATA__()', 'window[computed]', 'window.__DATA__; unrelated()', 'window.__proto__'])('rejects unsupported target paths: %s', async (target) => { await expect(evaluateScriptData('window.__DATA__={};', target)).rejects.toThrow(); }); it('reports missing targets and unresolved variable dependencies', async () => { await expect(evaluateScriptData('unrelated();', '__DATA__')).rejects.toThrow('target was not found'); await expect(evaluateScriptData('var data=unrelated();window.__DATA__=data;', '__DATA__')).rejects.toThrow('Unsupported script data expression'); }); it.each(['if(true){window.__DATA__.value=2;}', 'Object.assign(window.__DATA__,{value:2});', 'window.__DATA__.value++;', 'window.__DATA__[unknown()]={};'])( 'rejects unsupported code that could change extracted data: %s', async (suffix) => { await expect(evaluateScriptData(`window.__DATA__={value:1};${suffix}`, '__DATA__')).rejects.toThrow('Unsupported script data'); } ); it.each(['if(true){window.__DATA__.newField=2;}', 'var ignored=Object.assign(window.__DATA__,{value:2});', 'window.ignored=Object.assign(window.__DATA__,{value:2});'])( 'rejects mutations hidden in new properties and unused assignments: %s', async (suffix) => { await expect(evaluateScriptData(`window.__DATA__={};${suffix}`, '__DATA__')).rejects.toThrow('Unsupported script data'); } ); it('rejects cyclic data and excessively large sources or sparse arrays', async () => { await expect(evaluateScriptData('var a={};a.self=a;window.__DATA__=a;', '__DATA__')).rejects.toThrow('Cyclic script data'); await expect(evaluateScriptData(' '.repeat(2_000_001), '__DATA__')).rejects.toThrow('size limit'); await expect(evaluateScriptData('window.__DATA__=(function(){var a=[];a[100000]={};return a})();', '__DATA__')).rejects.toThrow('bounded numeric indexes'); }); }); describe('serialized callback data', () => { it('extracts a DWR callback argument without invoking the callback', async () => { const callback = vi.fn(); vi.stubGlobal('dwr', { engine: { _remoteHandleCallback: callback } }); const source = '(function(){if(!window.dwr)return;var s0=[],s1={};s1.post={title:"Entry"};s0[0]=s1;dwr.engine._remoteHandleCallback("493053","0",s0);})();'; await expect(evaluateScriptCallback(source, 'dwr.engine._remoteHandleCallback')).resolves.toEqual([{ post: { title: 'Entry' } }]); expect(callback).not.toHaveBeenCalled(); }); it('supports empty public DWR responses and explicit argument positions', async () => { const source = '//#DWR-INSERT\n//#DWR-REPLY\ndwr.engine._remoteHandleCallback("493053","0",[]);'; await expect(evaluateScriptCallback(source, 'window.dwr.engine._remoteHandleCallback')).resolves.toEqual([]); await expect(evaluateScriptCallback(source, 'dwr.engine._remoteHandleCallback', 0)).resolves.toBe('493053'); }); it('evaluates all callback arguments in order and does not keep an earlier successful capture after an invalid one', async () => { const source = 'var a={value:1};dwr.engine._remoteHandleCallback(a.value=2,"0",a);'; await expect(evaluateScriptCallback(source, 'dwr.engine._remoteHandleCallback')).resolves.toEqual({ value: 2 }); await expect(evaluateScriptCallback(`${source}dwr.engine._remoteHandleCallback("0","0",unknown());`, 'dwr.engine._remoteHandleCallback')).rejects.toThrow(); }); it('rejects missing callbacks and external calls used as the data argument', async () => { const fetchSpy = vi.fn(); vi.stubGlobal('fetch', fetchSpy); await expect(evaluateScriptCallback('unrelated();', 'dwr.engine._remoteHandleCallback')).rejects.toThrow('callback was not found'); await expect(evaluateScriptCallback('dwr.engine._remoteHandleCallback("0","0",fetch("https://example.invalid/"));', 'dwr.engine._remoteHandleCallback')).rejects.toThrow(); await expect(evaluateScriptCallback('', 'callback', -1)).rejects.toThrow('non-negative integer'); await expect(evaluateScriptCallback('dwr["engine._remoteHandleCallback"]("0","0",[]);', 'dwr.engine._remoteHandleCallback')).rejects.toThrow('callback was not found'); expect(fetchSpy).not.toHaveBeenCalled(); }); });