Files
谢宇宁andClaude Opus 5.5 f65d10e998 Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:07:59 +07:00

181 lines
12 KiB
JavaScript

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {readFile} from 'node:fs/promises';
import ts from 'typescript';
async function moduleUrl(path, transform=code=>code){
const source=await readFile(new URL(path,import.meta.url),'utf8');
const {outputText}=ts.transpileModule(source,{compilerOptions:{target:ts.ScriptTarget.ES2022,module:ts.ModuleKind.ESNext}});
return 'data:text/javascript;base64,'+Buffer.from(transform(outputText)+'\n//# sourceURL='+new URL(path,import.meta.url).pathname).toString('base64');
}
const protocolUrl=await moduleUrl('../src/lib/api/protocol.ts');
const imagesUrl=await moduleUrl('../src/lib/client/images.ts',code=>code.replace(/from ['"]\.\.\/api\/protocol['"]/,`from '${protocolUrl}'`));
const {decodeImage,ImageLoader,shouldDecryptImage,startImageLoading}=await import(imagesUrl);
const png=Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aV1sAAAAASUVORK5CYII=','base64');
// Known output of the reference XOR routine, with extra bytes beyond its 100-byte prefix.
const encrypted=Buffer.from('u2B/fnR5e3pwNzU/fnp0Yzl5c2BwcDc0OjMyMDGMZX9jcHA3Pntzc2RJ4xqPnm9wNDYwN92qZmJ5c2FwOXJ7dplwULM5eHFidHUxMjo+ODs9NHd8cWFiJCEnISUoKCNib3xubyAhIiMkJSYnKCkqKywtLi8wMTIzNDU2Nzg5Ojs8PT4/QEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaW1xdXl9gYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXp7fH1+f4CBgoOEhYaHiImKiw==','base64');
const arrayBuffer=bytes=>Uint8Array.from(bytes).buffer;
const response=()=>new Response(encrypted);
const flush=()=>new Promise(resolve=>setImmediate(resolve));
class Element extends EventTarget {
tagName;attrs=new Map();children=[];onerror=null;
style={values:new Map(),setProperty(key,value){this.values.set(key,value);}};
constructor(tagName='IMG',attrs={}){super();this.tagName=tagName;for(const [key,value] of Object.entries(attrs))this.setAttribute(key,value);}
setAttribute(key,value){this.attrs.set(key,String(value));}
getAttribute(key){return this.attrs.get(key)??null;}
removeAttribute(key){this.attrs.delete(key);}
matches(){return this.tagName==='IMG'&&this.attrs.has('data-src')||this.tagName==='VIDEO'&&this.attrs.has('data-poster');}
querySelectorAll(){return this.children.flatMap(child=>[...(child.matches()?[child]:[]),...child.querySelectorAll()]);}
}
test('reference ciphertext decrypts exactly, leaves bytes after 100 unchanged and does not mutate input',async()=>{
const input=arrayBuffer(encrypted),before=Buffer.from(input).toString('base64');
const blob=decodeImage(input),plain=Buffer.from(await blob.arrayBuffer());
assert.equal(blob.type,'image/png');
assert.deepEqual(plain,Buffer.concat([png,Buffer.from(Array.from({length:140},(_,index)=>index))]));
assert.deepEqual(plain.subarray(100),encrypted.subarray(100));
assert.equal(Buffer.from(input).toString('base64'),before);
});
test('ordinary images retain their bytes and MIME instead of being XORed again',async()=>{
const formats=[
[png,'image/png'],[Buffer.from([0xff,0xd8,0xff,0xe0,0]),'image/jpeg'],
[Buffer.from('GIF89a plain'),'image/gif'],[Buffer.from('RIFF0000WEBPplain'),'image/webp'],
[Buffer.concat([Buffer.from('RIFF'),Buffer.from([0xc2,0xa0,0,0]),Buffer.from('WEBPplain')]),'image/webp'],
[Buffer.from('<?xml version="1.0"?><svg xmlns="http://www.w3.org/2000/svg"></svg>'),'image/svg+xml'],
[Buffer.from('\0\0\0\x20ftypavif\0\0\0\0'),'image/avif'],
];
for(const [bytes,mime] of formats){
const blob=decodeImage(arrayBuffer(bytes));assert.equal(blob.type,mime);assert.deepEqual(Buffer.from(await blob.arrayBuffer()),bytes);
}
for(const bytes of [Buffer.alloc(0),Buffer.from('<html>Error</html>'),Buffer.from([1,2])])assert.throws(()=>decodeImage(arrayBuffer(bytes)),/图片格式无效/);
});
test('only HTTP images need fetching; local assets, blobs and data images bypass decryption',async()=>{
let calls=0;const loader=new ImageLoader({request:async()=>{calls++;return response();}});
try{
for(const url of ['/assets/logo.svg','/icon.svg','data:image/png;base64,abc','blob:https://example.com/id']){
const node=new Element();assert.equal(shouldDecryptImage(url),false);
assert.equal(await loader.processImage(url,node),url);assert.equal(node.getAttribute('src'),url);
}
assert.equal(calls,0);assert.equal(shouldDecryptImage('https://example.com/a'),true);
const node=new Element();assert.equal(await loader.processImage('javascript:alert(1)',node),'');assert.equal(node.getAttribute('data-decrypt-failed'),'1');
}finally{loader.dispose();}
});
test('duplicate images share one request and decrypted blob cache across DOM targets',async()=>{
let calls=0;const loader=new ImageLoader({request:async()=>{calls++;return response();}});
try{
const nodes=[new Element(),new Element(),new Element('VIDEO')];
const urls=await Promise.all(nodes.map((node,index)=>loader.processImage('https://example.com/shared',node,index===2?'poster':'img')));
assert.equal(calls,1);assert.match(urls[0],/^blob:/);assert.ok(urls.every(url=>url===urls[0]));
assert.equal(nodes[2].getAttribute('poster'),urls[0]);
assert.equal((await fetch(urls[0])).headers.get('content-type'),'image/png');
assert.equal(await loader.fetchDecrypt('https://example.com/shared'),urls[0]);assert.equal(calls,1);
}finally{loader.dispose();}
});
test('fetch concurrency is bounded and high-priority images precede queued list covers',async()=>{
const pending=new Map(),started=[];let active=0,maxActive=0;
const loader=new ImageLoader({concurrency:2,request:async url=>{
started.push(url);active++;maxActive=Math.max(maxActive,active);
return new Promise(resolve=>pending.set(url,()=>{active--;resolve(response());}));
}});
try{
const jobs=['a','b','c'].map(key=>loader.fetchDecrypt('https://example.com/'+key));
jobs.push(loader.fetchDecrypt('https://example.com/hero',true));
assert.deepEqual(started,['https://example.com/a','https://example.com/b']);
pending.get(started[0])();await jobs[0];await flush();
assert.equal(started[2],'https://example.com/hero');
pending.get(started[1])();await jobs[1];await flush();assert.equal(started[3],'https://example.com/c');
pending.get(started[2])();pending.get(started[3])();await Promise.all(jobs);
assert.equal(maxActive,2);
}finally{loader.dispose();}
});
test('request failures show a fallback, release the queue and remain retryable',async()=>{
let attempts=0;
const loader=new ImageLoader({concurrency:1,request:async()=>{
attempts++;if(attempts===1)return new Response('error',{status:503});if(attempts===2)throw Error('offline');return response();
}});
try{
const node=new Element();
for(let index=0;index<2;index++){
assert.equal(await loader.processImage('https://example.com/retry',node,'img','/icon.svg'),'');
assert.equal(node.getAttribute('src'),'/icon.svg');assert.equal(node.getAttribute('data-decrypt-failed'),'1');
}
const url=await loader.processImage('https://example.com/retry',node);
assert.match(url,/^blob:/);assert.equal(node.getAttribute('data-decrypt-failed'),null);assert.equal(attempts,3);
node.onerror();assert.equal(node.getAttribute('src'),'/placeholder.png');assert.equal(node.onerror,null);
}finally{loader.dispose();}
});
test('timed-out requests abort and allow later images to load',async()=>{
let aborted=false;
const loader=new ImageLoader({concurrency:1,timeoutMs:5,request:async(url,{signal})=>{
if(url.endsWith('/hang'))return new Promise((_resolve,reject)=>signal.addEventListener('abort',()=>{aborted=true;reject(new DOMException('Aborted','AbortError'));},{once:true}));
return response();
}});
try{
const failed=assert.rejects(loader.fetchDecrypt('https://example.com/hang'),{name:'AbortError'});
const next=loader.fetchDecrypt('https://example.com/next');
await failed;assert.equal(aborted,true);assert.match(await next,/^blob:/);
}finally{loader.dispose();}
});
test('late image results cannot overwrite a newer source or its fallback',async()=>{
const pending=new Map();
const loader=new ImageLoader({request:async url=>new Promise(resolve=>pending.set(url,resolve))});
try{
const node=new Element();
const old=loader.processImage('https://example.com/old',node);
const current=loader.processImage('https://example.com/new',node);
pending.get('https://example.com/new')(response());const url=await current;
pending.get('https://example.com/old')(new Response('error',{status:500}));assert.equal(await old,'');
assert.equal(node.getAttribute('src'),url);assert.equal(node.getAttribute('data-decrypt-failed'),null);
}finally{loader.dispose();}
});
test('disposing the loader aborts requests, rejects queued work and revokes cached blobs',async()=>{
const loader=new ImageLoader({concurrency:1,request:async(url,{signal})=>url.endsWith('/ready')?response():new Promise((_resolve,reject)=>signal.addEventListener('abort',()=>reject(new DOMException('Aborted','AbortError')),{once:true}))});
const url=await loader.fetchDecrypt('https://example.com/ready');
const active=assert.rejects(loader.fetchDecrypt('https://example.com/active'));
const queued=assert.rejects(loader.fetchDecrypt('https://example.com/queued'),/已停止/);
loader.dispose();await active;await queued;
await assert.rejects(fetch(url));await assert.rejects(loader.fetchDecrypt('https://example.com/ready'),/已停止/);
});
test('initial and dynamic markup load, lazy images wait for visibility, and changed sources reload',async()=>{
const originals=Object.fromEntries(['window','document','HTMLElement','MutationObserver','IntersectionObserver','fetch'].map(key=>[key,globalThis[key]]));
const requests=[];let mutations,intersection;
class MutationObserver {constructor(callback){this.callback=callback;mutations=this;}observe(){}disconnect(){}}
class IntersectionObserver {
observed=new Set();constructor(callback,options){this.callback=callback;this.options=options;intersection=this;}
observe(node){this.observed.add(node);}unobserve(node){this.observed.delete(node);}disconnect(){this.observed.clear();}
show(node){this.callback([{target:node,isIntersecting:true}]);}
}
const hero=new Element('IMG',{'data-src':'https://example.com/hero',fetchpriority:'high'});
const card=new Element('IMG',{'data-src':'https://example.com/card',loading:'lazy'});
const video=new Element('VIDEO',{'data-poster':'https://example.com/poster'});
const body=new Element('BODY');body.children=[hero,card,video];
globalThis.HTMLElement=Element;globalThis.MutationObserver=MutationObserver;globalThis.IntersectionObserver=IntersectionObserver;
globalThis.document={body,querySelectorAll:()=>body.querySelectorAll()};globalThis.window=new EventTarget();globalThis.window.matchMedia=()=>({matches:true});
globalThis.fetch=async url=>{requests.push(url);return response();};
let runtime;
try{
runtime=startImageLoading();assert.equal(startImageLoading(),runtime);await flush();
assert.deepEqual(requests,['https://example.com/hero','https://example.com/poster']);assert.match(video.getAttribute('poster'),/^blob:/);
assert.equal(intersection.options.rootMargin,'180px 0px');assert.equal(intersection.observed.has(card),true);
intersection.show(card);await flush();assert.equal(requests.at(-1),'https://example.com/card');
const added=new Element('IMG',{'data-src':'https://example.com/dynamic'});
mutations.callback([{type:'childList',addedNodes:[added],removedNodes:[]}]);await flush();assert.match(added.getAttribute('src'),/^blob:/);
added.setAttribute('data-src','https://example.com/changed');mutations.callback([{type:'attributes',target:added}]);await flush();assert.equal(requests.at(-1),'https://example.com/changed');
const reinserted=new Element('IMG',{'data-src':'https://example.com/reinserted',loading:'lazy'});
mutations.callback([{type:'childList',addedNodes:[reinserted],removedNodes:[]}]);assert.equal(intersection.observed.has(reinserted),true);
mutations.callback([{type:'childList',addedNodes:[],removedNodes:[reinserted]}]);assert.equal(intersection.observed.has(reinserted),false);
mutations.callback([{type:'childList',addedNodes:[reinserted],removedNodes:[]}]);intersection.show(reinserted);await flush();assert.equal(requests.at(-1),'https://example.com/reinserted');
assert.equal(typeof globalThis.window.processImage,'function');
}finally{runtime?.dispose();for(const [key,value] of Object.entries(originals)){if(value===undefined)delete globalThis[key];else globalThis[key]=value;}}
});