Files
WRNexusJS/packages/viewer/src/resolve.ts
T

96 lines
4.3 KiB
TypeScript

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<ViewerPlan> {
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");
}
}