import { extensionOf, kindOfExtension, kindOfMime } from "./kinds.ts"; import { defaultRegistry, downloadRenderer } from "./registry.ts"; import { sniff } from "./sniff.ts"; import { readBounded, safeSourceUrl } from "./source.ts"; import type { ResolveOptions, ViewerKind, ViewerPlan, ViewerSource } from "./types.ts"; export async function resolve( source: ViewerSource, options: ResolveOptions = {}, ): Promise { const ext = extensionOf(source.name); let kind = kindOfMime(source.mime ?? source.blob?.type ?? "") ?? kindOfExtension(ext); const fallback = (reason: string): ViewerPlan => ({ kind, renderer: downloadRenderer, fallback: [], needsConvert: false, sandboxed: false, reason, }); if ([".exe", ".dll"].includes(ext)) return fallback("Executable files cannot be previewed"); const registry = options.registry ?? defaultRegistry; const candidates = (k: ViewerKind, extension = ext) => registry.candidates(k, extension); // No renderer for the kind is not an oversized file: leave the cap open so the // ladder reports why nothing can display it rather than "exceeds preview limit". const capFor = (k: ViewerKind) => { const caps = candidates(k).map((r) => r.maxInlineBytes ?? 2 * 1024 * 1024); return caps.length ? Math.max(...caps) : Infinity; }; try { let size = source.blob?.size ?? source.size; let bytes: Uint8Array; if (source.blob) { bytes = new Uint8Array(await source.blob.slice(0, 4096).arrayBuffer()); } else { const url = source.src && safeSourceUrl(source.src); if (!url) return fallback("No readable file source"); const request = options.fetch ?? fetch; const head = await request(url, { method: "HEAD", signal: options.signal }); if (head.ok) { const length = head.headers.get("content-length"); if (length !== null && /^\d+$/.test(length)) size = Math.max(size ?? 0, Number(length)); kind = kindOfMime(head.headers.get("content-type") ?? "") ?? kind; } if (size !== undefined && size > capFor(kind)) return fallback("File exceeds preview limit"); const response = await request(url, { headers: { Range: "bytes=0-4095" }, signal: options.signal, }); bytes = await readBounded(response, 4096, true); } const detected = sniff(bytes); if (detected.executable) return fallback("Executable files cannot be previewed"); const previous = kind; // Plain UTF-8 retains code classification; active markup and signatures win. // RTF is the one office format that is genuinely text, so `doc` survives a // text signature too — otherwise a .rtf loses its extension and its reader. // A generic container signature (OLE, ZIP, unrecognised binary) says only // "not text", so a .docx or .psd keeps the kind its extension implies. const generic = detected.kind === "binary" || detected.kind === "archive"; const keepsExtensionKind = (detected.kind === "text" && ["text", "code", "markup", "sheet", "doc"].includes(kind)) || (generic && ["doc", "sheet", "archive", "binary"].includes(kind)); kind = keepsExtensionKind ? kind : detected.kind; // An extension override must not bypass conflicting signatures. const selectedExt = kind === previous || (detected.kind === "text" && previous === "binary") ? ext : ""; const available = []; let needsConvert = false; for (const renderer of candidates(kind, selectedExt)) { // A tier 3 renderer needs a converter, and `load` is how it proves one is // reachable. Without that check it could never report itself unavailable. if (renderer.tier === 3 && !renderer.load) continue; if (size !== undefined && size > (renderer.maxInlineBytes ?? 2 * 1024 * 1024)) continue; try { await renderer.load?.(options); if (renderer.tier === 3) needsConvert = true; available.push(renderer); } catch { /* Optional dependency unavailable. */ } } const renderer = available.shift(); if (!renderer) return fallback("Preview unavailable for this file"); return { kind, renderer, fallback: [...available, downloadRenderer], needsConvert: needsConvert && renderer.tier === 3, sandboxed: kind === "markup" || kind === "vector", }; } catch { return fallback("File could not be loaded"); } }