| 1 | export interface SessionAttachmentChunk { |
| 2 | data?: string; |
| 3 | nextOffset: number; |
| 4 | done: boolean; |
| 5 | } |
| 6 | |
| 7 | const previewBudgetBytes = 64 << 20; |
| 8 | |
| 9 | export async function readSessionAttachmentDataURL( |
| 10 | tabId: string, |
| 11 | digest: string, |
| 12 | mime: string, |
| 13 | read: (tabId: string, digest: string, offset: number) => Promise<SessionAttachmentChunk>, |
| 14 | ): Promise<string> { |
| 15 | const chunks: Uint8Array[] = []; |
| 16 | let offset = 0; |
| 17 | let total = 0; |
| 18 | for (;;) { |
| 19 | const chunk = await read(tabId, digest, offset); |
| 20 | if (chunk.data) { |
| 21 | const bytes = Uint8Array.from(atob(chunk.data), (c) => c.charCodeAt(0)); |
| 22 | total += bytes.length; |
| 23 | if (total > previewBudgetBytes) { |
| 24 | throw new Error("attachment preview exceeds preview budget"); |
| 25 | } |
| 26 | chunks.push(bytes); |
| 27 | } |
| 28 | if (chunk.done) break; |
| 29 | if (!Number.isFinite(chunk.nextOffset) || chunk.nextOffset <= offset) { |
| 30 | throw new Error("attachment read did not advance"); |
| 31 | } |
| 32 | offset = chunk.nextOffset; |
| 33 | } |
| 34 | const merged = new Uint8Array(total); |
| 35 | let cursor = 0; |
| 36 | for (const part of chunks) { |
| 37 | merged.set(part, cursor); |
| 38 | cursor += part.length; |
| 39 | } |
| 40 | const binary: string[] = []; |
| 41 | for (let i = 0; i < merged.length; i += 32768) binary.push(String.fromCharCode(...merged.subarray(i, i + 32768))); |
| 42 | return `data:${mime || "image/png"};base64,${btoa(binary.join(""))}`; |
| 43 | } |
| 44 |