999 lines · 32.9 KB · TypeScript
RawDownload
import { describe, expect, it, vi } from "vite-plus/test"; import {  MAX_FAVICON_CANDIDATES,  MAX_FAVICON_RESPONSE_BYTES,  captureFavicon,  selectFaviconCandidates,} from "./FaviconCapture.ts"; const PNG = "data:image/png;base64,cG5n";const SOURCE_PNG = Buffer.alloc(24);Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(SOURCE_PNG);SOURCE_PNG.writeUInt32BE(1, 16);SOURCE_PNG.writeUInt32BE(1, 20);const SOURCE_PNG_URL = `data:image/png;base64,${SOURCE_PNG.toString("base64")}`; function sourceGif(  width: number,  height: number,  frameWidth = width,  frameHeight = height,  additionalFrames: ReadonlyArray<{    readonly left?: number;    readonly top?: number;    readonly width: number;    readonly height: number;  }> = [],): Buffer {  const frames = [{ width: frameWidth, height: frameHeight }, ...additionalFrames];  const buffer = Buffer.alloc(13 + frames.length * 12 + 1);  buffer.write("GIF89a", 0, "ascii");  buffer.writeUInt16LE(width, 6);  buffer.writeUInt16LE(height, 8);  let offset = 13;  for (const frame of frames) {    buffer[offset] = 0x2c;    buffer.writeUInt16LE(frame.left ?? 0, offset + 1);    buffer.writeUInt16LE(frame.top ?? 0, offset + 3);    buffer.writeUInt16LE(frame.width, offset + 5);    buffer.writeUInt16LE(frame.height, offset + 7);    offset += 10;    buffer[offset] = 2;    buffer[offset + 1] = 0;    offset += 2;  }  buffer[offset] = 0x3b;  return buffer;} function sourceJpeg(  width: number,  height: number,  orientations: number | ReadonlyArray<number> = [],): Buffer {  const frame = Buffer.from([    0xff,    0xd8,    0xff,    0xc0,    0x00,    0x07,    0x08,    height >>> 8,    height & 0xff,    width >>> 8,    width & 0xff,  ]);  const app1Segments = (typeof orientations === "number" ? [orientations] : orientations).map(    (orientation) => sourceJpegExifSegment([orientation]),  );  return Buffer.concat([frame.subarray(0, 2), ...app1Segments, frame.subarray(2)]);} function sourceJpegApp1Segment(payload: Buffer): Buffer {  const app1 = Buffer.alloc(4 + payload.byteLength);  app1[0] = 0xff;  app1[1] = 0xe1;  app1.writeUInt16BE(payload.byteLength + 2, 2);  payload.copy(app1, 4);  return app1;} function sourceJpegExifSegment(  orientations: ReadonlyArray<number>,  options?: {    readonly byteOrder?: "II" | "MM";    readonly magic?: number;    readonly padding?: number;  },): Buffer {  const exif = Buffer.alloc(20 + orientations.length * 12);  exif.write("Exif\0\0", 0, "binary");  exif[5] = options?.padding ?? 0;  const littleEndian = options?.byteOrder !== "MM";  exif.write(littleEndian ? "II" : "MM", 6, "ascii");  const writeUInt16 = (value: number, offset: number) =>    littleEndian ? exif.writeUInt16LE(value, offset) : exif.writeUInt16BE(value, offset);  const writeUInt32 = (value: number, offset: number) =>    littleEndian ? exif.writeUInt32LE(value, offset) : exif.writeUInt32BE(value, offset);  writeUInt16(options?.magic ?? 42, 8);  writeUInt32(8, 10);  writeUInt16(orientations.length, 14);  orientations.forEach((orientation, index) => {    const entryOffset = 16 + index * 12;    writeUInt16(0x0112, entryOffset);    writeUInt16(3, entryOffset + 2);    writeUInt32(1, entryOffset + 4);    writeUInt16(orientation, entryOffset + 8);  });  return sourceJpegApp1Segment(exif);} function sourceJpegWithApp1Segments(  width: number,  height: number,  segments: ReadonlyArray<Buffer>,): Buffer {  const frame = sourceJpeg(width, height);  return Buffer.concat([frame.subarray(0, 2), ...segments, frame.subarray(2)]);} function sourceJpegWithOrientationEntries(  width: number,  height: number,  orientations: ReadonlyArray<number>,): Buffer {  return sourceJpegWithApp1Segments(width, height, [sourceJpegExifSegment(orientations)]);} function sourceJpegWithEndianAlias(alias: number, byteOrder: "II" | "MM"): Buffer {  const exif = sourceJpegExifSegment([6], { byteOrder });  exif[10] = alias;  exif[11] = alias;  return sourceJpegWithApp1Segments(64, 32, [exif]);} function sourceJpegExifWithSubIfd(options: {  readonly rootOrientation?: number;  readonly subIfdFirst?: boolean;  readonly subIfdOrientation: number;}): Buffer {  const rootEntries = options.rootOrientation === undefined ? 1 : 2;  const rootIfdOffset = 14;  const subIfdOffset = rootIfdOffset + 2 + rootEntries * 12 + 4;  const exif = Buffer.alloc(subIfdOffset + 2 + 12 + 4);  exif.write("Exif\0\0", 0, "binary");  exif.write("II", 6, "ascii");  exif.writeUInt16LE(42, 8);  exif.writeUInt32LE(8, 10);  exif.writeUInt16LE(rootEntries, rootIfdOffset);   const writeOrientation = (offset: number, orientation: number) => {    exif.writeUInt16LE(0x0112, offset);    exif.writeUInt16LE(3, offset + 2);    exif.writeUInt32LE(1, offset + 4);    exif.writeUInt16LE(orientation, offset + 8);  };  const writeSubIfdPointer = (offset: number) => {    exif.writeUInt16LE(0x8769, offset);    exif.writeUInt16LE(4, offset + 2);    exif.writeUInt32LE(1, offset + 4);    exif.writeUInt32LE(subIfdOffset - 6, offset + 8);  };   const firstRootEntryOffset = rootIfdOffset + 2;  if (options.rootOrientation === undefined) {    writeSubIfdPointer(firstRootEntryOffset);  } else if (options.subIfdFirst) {    writeSubIfdPointer(firstRootEntryOffset);    writeOrientation(firstRootEntryOffset + 12, options.rootOrientation);  } else {    writeOrientation(firstRootEntryOffset, options.rootOrientation);    writeSubIfdPointer(firstRootEntryOffset + 12);  }   exif.writeUInt16LE(1, subIfdOffset);  writeOrientation(subIfdOffset + 2, options.subIfdOrientation);  return sourceJpegApp1Segment(exif);} function sourceJpegExifWithSubIfdPointers(options: {  readonly pointerCount: number;  readonly subIfdEntries: number;}): Buffer {  const { pointerCount, subIfdEntries } = options;  const rootIfdOffset = 14;  const rootEntries = pointerCount + 1;  const subIfdOffset = rootIfdOffset + 2 + rootEntries * 12 + 4;  const exif = Buffer.alloc(subIfdOffset + 2 + subIfdEntries * 12 + 4);  exif.write("Exif\0\0", 0, "binary");  exif.write("II", 6, "ascii");  exif.writeUInt16LE(42, 8);  exif.writeUInt32LE(8, 10);  exif.writeUInt16LE(rootEntries, rootIfdOffset);  for (let index = 0; index < pointerCount; index += 1) {    const entryOffset = rootIfdOffset + 2 + index * 12;    exif.writeUInt16LE(0x8769, entryOffset);    exif.writeUInt16LE(4, entryOffset + 2);    exif.writeUInt32LE(1, entryOffset + 4);    exif.writeUInt32LE(subIfdOffset - 6, entryOffset + 8);  }  const orientationOffset = rootIfdOffset + 2 + pointerCount * 12;  exif.writeUInt16LE(0x0112, orientationOffset);  exif.writeUInt16LE(3, orientationOffset + 2);  exif.writeUInt32LE(1, orientationOffset + 4);  exif.writeUInt16LE(6, orientationOffset + 8);   exif.writeUInt16LE(subIfdEntries, subIfdOffset);  for (let index = 0; index < subIfdEntries; index += 1) {    const entryOffset = subIfdOffset + 2 + index * 12;    exif.writeUInt16LE(1, entryOffset);    exif.writeUInt16LE(3, entryOffset + 2);    exif.writeUInt32LE(1, entryOffset + 4);  }  return sourceJpegApp1Segment(exif);} function sourceJpegExifWithOverlappingSubIfds(pointerCount: number, subIfdEntries: number): Buffer {  const rootIfdOffset = 14;  const rootEntries = pointerCount + 1;  const subIfdOffset = rootIfdOffset + 2 + rootEntries * 12 + 4;  const exif = Buffer.alloc(subIfdOffset + pointerCount * 2 + 2 + subIfdEntries * 12);  exif.write("Exif\0\0", 0, "binary");  exif.write("II", 6, "ascii");  exif.writeUInt32LE(8, 10);  exif.writeUInt16LE(rootEntries, rootIfdOffset);  for (let index = 0; index < pointerCount; index += 1) {    const entryOffset = rootIfdOffset + 2 + index * 12;    exif.writeUInt16LE(0x8769, entryOffset);    exif.writeUInt16LE(4, entryOffset + 2);    exif.writeUInt32LE(1, entryOffset + 4);    exif.writeUInt32LE(subIfdOffset + index * 2 - 6, entryOffset + 8);    exif.writeUInt16LE(subIfdEntries, subIfdOffset + index * 2);  }  const orientationOffset = rootIfdOffset + 2 + pointerCount * 12;  exif.writeUInt16LE(0x0112, orientationOffset);  exif.writeUInt16LE(3, orientationOffset + 2);  exif.writeUInt32LE(1, orientationOffset + 4);  exif.writeUInt16LE(6, orientationOffset + 8);  return sourceJpegApp1Segment(exif);} function sourceWebp(width: number, height: number): Buffer {  const buffer = Buffer.alloc(30);  buffer.write("RIFF", 0, "ascii");  buffer.write("WEBP", 8, "ascii");  buffer.write("VP8X", 12, "ascii");  buffer.writeUIntLE(width - 1, 24, 3);  buffer.writeUIntLE(height - 1, 27, 3);  return buffer;} function sourceIco(embedded: Buffer): Buffer {  const buffer = Buffer.alloc(22 + embedded.byteLength);  buffer.writeUInt16LE(1, 2);  buffer.writeUInt16LE(1, 4);  buffer.writeUInt32LE(embedded.byteLength, 14);  buffer.writeUInt32LE(22, 18);  embedded.copy(buffer, 22);  return buffer;} function makeUnsafePng(): Buffer {  const buffer = Buffer.from(SOURCE_PNG);  buffer.writeUInt32BE(4096, 16);  buffer.writeUInt32BE(4096, 20);  return buffer;} function sourcePng(width: number, height: number): Buffer {  const buffer = Buffer.from(SOURCE_PNG);  buffer.writeUInt32BE(width, 16);  buffer.writeUInt32BE(height, 20);  return buffer;} function makeUnsafeDib(): Buffer {  const buffer = Buffer.alloc(40);  buffer.writeUInt32LE(40, 0);  buffer.writeInt32LE(4096, 4);  buffer.writeInt32LE(4096, 8);  return buffer;} function makeWebContents(options?: {  readonly fetch?: (url: string, init?: RequestInit) => Promise<Response>;  readonly rasterize?: (code: string) => Promise<unknown>;}) {  const fetch = vi.fn(    options?.fetch ??      (async () =>        new Response(new Uint8Array(SOURCE_PNG), {          headers: { "content-type": "image/png" },        })),  );  const executeJavaScriptInIsolatedWorld = vi.fn(    async (_worldId: number, scripts: ReadonlyArray<{ readonly code: string }>) =>      options?.rasterize ? options.rasterize(scripts[0]?.code ?? "") : PNG,  );  return {    webContents: {      session: { fetch },      executeJavaScriptInIsolatedWorld,    } as never,    executeJavaScriptInIsolatedWorld,    fetch,  };} const JPEG_LANDSCAPE_LAYOUT = {  draw: "context.drawImage(bitmap, 0, 8, 32, 16)",  resizeHeight: 16,  resizeWidth: 32,} as const;const JPEG_PORTRAIT_LAYOUT = {  draw: "context.drawImage(bitmap, 8, 0, 16, 32)",  resizeHeight: 32,  resizeWidth: 16,} as const; async function expectJpegLayout(  source: Buffer,  layout: typeof JPEG_LANDSCAPE_LAYOUT | typeof JPEG_PORTRAIT_LAYOUT,): Promise<void> {  const { webContents } = makeWebContents({    rasterize: async (code) => {      expect(code).toContain(`resizeWidth: ${layout.resizeWidth}`);      expect(code).toContain(`resizeHeight: ${layout.resizeHeight}`);      expect(code).toContain(layout.draw);      return PNG;    },  });   expect(    await captureFavicon({      webContents,      pageUrl: "https://example.com/page",      candidates: [`data:image/jpeg;base64,${source.toString("base64")}`],      signal: new AbortController().signal,    }),  ).toEqual({ kind: "captured", dataUrl: PNG });} describe("selectFaviconCandidates", () => {  it("filters and deduplicates before applying the candidate cap", () => {    const valid = Array.from(      { length: MAX_FAVICON_CANDIDATES + 2 },      (_, index) => `https://example.com/favicon-${index}.png`,    );    expect(      selectFaviconCandidates([        ...Array.from({ length: 64 }, () => "javascript:alert(1)"),        valid[0]!,        valid[0]!,        ...valid.slice(1),      ]),    ).toEqual(valid.slice(0, MAX_FAVICON_CANDIDATES));  });   it("bounds raw candidate scanning independently of the usable-candidate cap", () => {    const oversizedInvalid = `javascript:${"x".repeat(2_048)}`;    expect(      selectFaviconCandidates([        ...Array.from({ length: 128 }, () => oversizedInvalid),        "https://example.com/too-late.png",      ]),    ).toEqual([]);  });}); describe("captureFavicon", () => {  it.each([    {      label: "same-origin",      pageUrl: "https://example.com/page",      faviconUrl: "https://example.com/favicon.png",      credentials: "include",    },    {      label: "cross-origin",      pageUrl: "https://example.com/page",      faviconUrl: "https://cdn.example.net/favicon.png",      credentials: "omit",    },  ])("uses the explicit credential policy for $label requests", async (testCase) => {    const { webContents, fetch } = makeWebContents();    const result = await captureFavicon({      webContents,      pageUrl: testCase.pageUrl,      candidates: [testCase.faviconUrl],      signal: new AbortController().signal,    });     expect(result).toEqual({ kind: "captured", dataUrl: PNG });    expect(fetch).toHaveBeenCalledWith(      testCase.faviconUrl,      expect.objectContaining({ credentials: testCase.credentials, redirect: "error" }),    );  });   it("decodes base64 and percent-encoded inline images without fetching", async () => {    const { webContents, fetch, executeJavaScriptInIsolatedWorld } = makeWebContents();    const percentEncodedPng = [...SOURCE_PNG]      .map((byte) => `%${byte.toString(16).padStart(2, "0")}`)      .join("");     for (const candidate of [SOURCE_PNG_URL, `data:image/png,${percentEncodedPng}`]) {      expect(        await captureFavicon({          webContents,          pageUrl: "https://example.com/page",          candidates: [candidate],          signal: new AbortController().signal,        }),      ).toEqual({ kind: "captured", dataUrl: PNG });    }     expect(fetch).not.toHaveBeenCalled();    expect(executeJavaScriptInIsolatedWorld).toHaveBeenCalledTimes(2);  });   it("tries the next candidate after an ordinary rejection", async () => {    const { webContents, fetch } = makeWebContents({      fetch: async (url) =>        url.endsWith("first.png")          ? new Response(null, { status: 404 })          : new Response(new Uint8Array(SOURCE_PNG), {              headers: { "content-type": "image/png" },            }),    });     expect(      await captureFavicon({        webContents,        pageUrl: "https://example.com/page",        candidates: ["https://example.com/first.png", "https://example.com/second.png"],        signal: new AbortController().signal,      }),    ).toEqual({ kind: "captured", dataUrl: PNG });    expect(fetch).toHaveBeenCalledTimes(2);  });   it("cancels a rejected response body before trying the next candidate", async () => {    const cancel = vi.fn();    const body = new ReadableStream<Uint8Array>({      start(controller) {        controller.enqueue(new Uint8Array(1));      },      cancel,    });    const { webContents, fetch } = makeWebContents({      fetch: async (url) =>        url.endsWith("first.png")          ? new Response(body, { status: 404 })          : new Response(new Uint8Array(SOURCE_PNG), {              headers: { "content-type": "image/png" },            }),    });     expect(      await captureFavicon({        webContents,        pageUrl: "https://example.com/page",        candidates: ["https://example.com/first.png", "https://example.com/second.png"],        signal: new AbortController().signal,      }),    ).toEqual({ kind: "captured", dataUrl: PNG });    expect(cancel).toHaveBeenCalledOnce();    expect(fetch).toHaveBeenCalledTimes(2);  });   it("stops a pending fetch when its capture is aborted", async () => {    const controller = new AbortController();    const { webContents } = makeWebContents({      fetch: (_url, init) =>        new Promise<Response>((_resolve, reject) => {          init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), {            once: true,          });        }),    });    const capture = captureFavicon({      webContents,      pageUrl: "https://example.com/page",      candidates: ["https://example.com/favicon.png"],      signal: controller.signal,    });    controller.abort();    expect(await capture).toEqual({ kind: "none" });  });   it("ends candidate fallback when the overall capture deadline expires", async () => {    const timeoutController = new AbortController();    const timeout = vi.spyOn(AbortSignal, "timeout").mockReturnValue(timeoutController.signal);    const { webContents, fetch } = makeWebContents({      fetch: (url, init) => {        if (url.endsWith("first.png")) return Promise.resolve(new Response(null, { status: 404 }));        return new Promise<Response>((_resolve, reject) => {          init?.signal?.addEventListener("abort", () => reject(init.signal?.reason), {            once: true,

Lines 1–500 of 999.