import { Flags } from './constants.js'; import { equalBytes, type Bytes } from './bytes.js'; import { IntegrityError } from './errors.js'; import { MAX_ENC_PAYLOAD, openFrame, parseEnvelope, peekEnvelopeLength } from './frame.js'; import { decodeFirstFrame, type ItemMeta } from './item.js'; import type { ReaderContext } from './reader.js'; /** 字节队列:接收任意切分的块,按需取出前 n 个字节。 */ class ByteQueue { readonly #parts: Uint8Array[] = []; #length = 0; get length(): number { return this.#length; } push(chunk: Uint8Array): void { if (chunk.length === 0) return; this.#parts.push(chunk); this.#length += chunk.length; } /** 复制前 n 个字节,不消耗。 */ peek(n: number): Bytes { const out = new Uint8Array(n); let filled = 0; for (const part of this.#parts) { if (filled >= n) break; const take = Math.min(part.length, n - filled); out.set(part.subarray(0, take), filled); filled += take; } return out; } take(n: number): Bytes { const out = new Uint8Array(n); let filled = 0; while (filled < n) { const head = this.#parts[0] as Uint8Array; const need = n - filled; if (head.length <= need) { out.set(head, filled); filled += head.length; this.#parts.shift(); } else { out.set(head.subarray(0, need), filled); this.#parts[0] = head.subarray(need); filled += need; } } this.#length -= n; return out; } } async function* bytesOf(source: ReadableStream | AsyncIterable): AsyncGenerator { if (typeof ReadableStream !== 'undefined' && source instanceof ReadableStream) { const reader = source.getReader(); let finished = false; try { for (;;) { const { done, value } = await reader.read(); if (done) { finished = true; return; } yield value; } } finally { if (!finished) await reader.cancel().catch(() => {}); reader.releaseLock(); } } yield* source as AsyncIterable; } /** * 把「封装首尾相接」的字节流(GET /items/{id}/frames 的响应体)切成一个个完整封装。 * 连接中途断开会在末尾留下半个封装:这是正常的网络情况,不是篡改,所以直接丢弃, * 调用方根据 decryptItemStream 报告的 nextIndex 续传即可。 */ export async function* splitEnvelopes( source: ReadableStream | AsyncIterable, ): AsyncGenerator { const queue = new ByteQueue(); for await (const chunk of bytesOf(source)) { queue.push(chunk); for (;;) { if (queue.length < 26) break; const total = peekEnvelopeLength(queue.peek(26), MAX_ENC_PAYLOAD); if (total === null || queue.length < total) break; yield queue.take(total); } } } export interface DecryptStreamOptions { /** 这次响应从哪一帧开始(拉取时的 from 参数),默认 0。 */ fromIndex?: number; /** 期望的条目 id(base64url 解码后的字节)。提供后,混入别的条目的帧会被拒绝。 */ itemId?: Uint8Array; } export interface ItemDecodeResult { /** 已收到已认证的 FINAL。 */ complete: boolean; /** 下一个期望的帧序号。没收完时用它作为 from 续传(不会重复计下载次数)。 */ nextIndex: number; /** 第 0 帧里的元数据;从中间帧续传时没有。 */ meta: ItemMeta | undefined; } /** * 流式解密一个条目:边收边解边产出内容,内存占用只有一帧。可以放进 Worker 里运行。 * * 逐帧校验:认证、条目 id、帧序号连续、标志一致、FINAL 之后不再有帧。 * 任何一项失败都会抛 IntegrityError(流会出错),已产出的内容不可信的部分由调用方丢弃。 * 流正常结束但没有 FINAL 不是错误(连接可能被断开),此时 complete 为 false。 */ export async function* decryptItemFrames( ctx: ReaderContext, source: ReadableStream | AsyncIterable, opts: DecryptStreamOptions = {}, ): AsyncGenerator { let expected = opts.fromIndex ?? 0; let itemId = opts.itemId; let flagsMask: number | undefined; let finalSeen = false; let meta: ItemMeta | undefined; for await (const bytes of splitEnvelopes(source)) { const env = parseEnvelope(bytes); const isEnc = (env.flags & Flags.Enc) !== 0; if (isEnc !== (ctx.mode === 'enc')) throw new IntegrityError('wrong_mode'); if (!itemId) itemId = env.itemId; else if (!equalBytes(itemId, env.itemId)) throw new IntegrityError('item_mismatch'); if (finalSeen) throw new IntegrityError('frame_after_final'); if (env.frameIndex !== expected) throw new IntegrityError('gap'); const itemFlags = env.flags & (Flags.Gated | Flags.Snapshot | Flags.Control); if (flagsMask === undefined) flagsMask = itemFlags; else if (flagsMask !== itemFlags) throw new IntegrityError('flags_changed'); let data: Bytes; if (ctx.mode === 'enc') { data = await openFrame(await ctx.itemKey(env.itemId), ctx.channelId, env); } else { data = env.payload; } if (env.frameIndex === 0) { const first = decodeFirstFrame(data); meta = first.meta; data = first.content; } expected++; if ((env.flags & Flags.Final) !== 0) finalSeen = true; if (data.length > 0) yield data; } return { complete: finalSeen, nextIndex: expected, meta }; } /** 把 decryptItemFrames 包成 ReadableStream,便于直接 pipe 到文件。 */ export function decryptItemStream( ctx: ReaderContext, source: ReadableStream | AsyncIterable, opts: DecryptStreamOptions = {}, ): { content: ReadableStream; result: Promise } { const gen = decryptItemFrames(ctx, source, opts); let resolve!: (r: ItemDecodeResult) => void; let reject!: (e: unknown) => void; const result = new Promise((res, rej) => { resolve = res; reject = rej; }); result.catch(() => {}); // 错误通过流本身传播,避免未处理的 rejection const content = new ReadableStream({ async pull(controller) { try { const r = await gen.next(); if (r.done) { resolve(r.value); controller.close(); } else { controller.enqueue(r.value); } } catch (e) { reject(e); controller.error(e); } }, async cancel(reason) { reject(reason ?? new DOMException('解密已取消', 'AbortError')); await gen.return({ complete: false, nextIndex: opts.fromIndex ?? 0, meta: undefined }); }, }); return { content, result }; }