/** * 增量 SHA-256 与分块导入摘要测试,使用 Node crypto 的独立实现作为结果参照。 * 覆盖填充边界、不同 update 块宽、重复最终化读取,以及异步读块/让出时取消的迟到结果隔离。 */ import {test} from 'node:test'; import assert from 'node:assert/strict'; import {createHash,randomBytes} from 'node:crypto'; import {createSha256} from '../../src/lib/canvas-runtime/sha256.ts'; import {importSourceDigest} from '../../src/lib/canvas-runtime/import-integrity.ts'; // 用 Node createHash 计算参照摘要;既不复用待测算法,也不以自身输出作为预期值。 const expected=bytes=>createHash('sha256').update(bytes).digest('hex'); // 空值、标准字符串、百万个 a 及长度 0–259 的随机输入,分别按六种块宽更新;hex 可重复读取,但最终化后禁止 update。 test('incremental SHA-256 matches known vectors, every padding boundary and varied chunk splits',()=>{ for(const bytes of [Buffer.alloc(0),Buffer.from('abc'),Buffer.from('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'),Buffer.alloc(1000000,97),...Array.from({length:260},(_,n)=>randomBytes(n))]){ for(const chunk of [1,7,63,64,65,8192]){const hash=createSha256();for(let offset=0;offsethash.update(new Uint8Array()),/digest_finalized/);} } }); // 1 MiB+13 字节按 64 KiB 读取为 17 块并让出 16 次;摘要匹配 Node,且没有整块源 Blob 读取。 test('source digest reads bounded slices, yields and never materializes the entire Blob',async()=>{ const data=randomBytes(1024*1024+13),reads=[];let yields=0; // 禁止对整个源 Blob 调 arrayBuffer,slice 记录请求块长并交给真实 Blob 读字节。 class SlicedBlob extends Blob{arrayBuffer(){throw Error('whole Blob read');}slice(start,end){reads.push(end-start);return super.slice(start,end);}} assert.equal(await importSourceDigest(new SlicedBlob([data]),undefined,{chunkBytes:65536,yieldTask:async()=>{yields++;}}),expected(data));assert.equal(reads.length,17);assert.ok(reads.every(size=>size<=65536));assert.equal(yields,16); }); // 分别在读块挂起时 abort 和 yield 回调内 abort;迟到块及调度返回后都不能完成摘要,统一拒绝 import_aborted。 test('source digest cancellation fences a late chunk and a scheduler yield',async()=>{ let release,entered;const gate=new Promise(resolve=>release=resolve),ready=new Promise(resolve=>entered=resolve),controller=new AbortController();/* 读出的分块再套一层 gate,保证 abort 发生在 arrayBuffer 真正返回之前。 */ class Chunk extends Blob{async arrayBuffer(){entered();await gate;return super.arrayBuffer();}}/* 保留真实 slice 字节,但让每一块都经过上述可暂停读取。 */ class Source extends Blob{slice(...args){return new Chunk([super.slice(...args)]);}} const pending=importSourceDigest(new Source(['abc']),controller.signal);await ready;controller.abort();release();await assert.rejects(pending,/import_aborted/);const second=new AbortController();await assert.rejects(importSourceDigest(new Blob(['abcdef']),second.signal,{chunkBytes:2,yieldTask:async()=>second.abort()}),/import_aborted/); });