perf(images): probe raster headers instead of decoding whole payloads, memoize repo icon validation (#18421)

* perf(images): measure raster headers from a probe and memoize repo icon validation

`getRepos()` re-sanitizes every repo on every call, and an uploaded/file repo
icon costs a full base64 decode of its data URI each time. Three fixes:

- `writeQuartet` destructured a mutable array, which sends V8 through the
  iterator protocol once per four input characters; index reads plus a length
  counter produce identical bytes.
- `decodeBase64Prefix` decoded the whole payload despite only the first bytes
  being needed. `exceedsRasterImagePreviewLimits` now probes 64 bytes and
  widens x16 until the header measures, and only re-runs the original
  full-payload decode when the verdict would suppress a preview.
- `sanitizeRepoIcon`'s src validation is memoized per icon source with a
  bounded FIFO map, reusing the `memoizeTitleClassification` idiom (now a
  shared `memoizeByStringKey`).

* perf(images): key icon-validation memo on the persisted icon object

Replaces the per-source 64-entry FIFO string-key memo with a WeakMap keyed on
the persisted repoIcon object that hydrateRepo already receives, storing
{src, source, supported} and re-checking both fields on a hit.

Retention becomes zero by construction (entries die with state.repos[i].repoIcon),
so there is no cap to evict live icons and no dead icon strings held after a repo
or icon is replaced. The identity re-check makes an in-place mutation unable to
serve a stale verdict. Drops bounded-string-key-memo.ts and reverts the collateral
terminal-title-classification-memo refactor.
This commit is contained in:
Neil
2026-09-03 20:56:29 -07:00
committed by GitHub
parent 97e5eb8886
commit 6415b1dc22
4 changed files with 535 additions and 24 deletions
@@ -0,0 +1,345 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { decodeBase64Prefix, exceedsRasterImagePreviewLimits } from './raster-image-base64-preview'
import type * as RasterImageDimensionsModule from './raster-image-dimensions'
import { readRasterImageDimensions } from './raster-image-dimensions'
import {
isKnownRasterImageMimeType,
isRasterImagePreviewDimensions,
RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
} from './raster-image-preview-limits'
/** The pre-change verdict: one full-payload decode, then one dimension read. */
function unprobedExceeds(content: string, mimeType: string | undefined): boolean {
if (!isKnownRasterImageMimeType(mimeType)) {
return false
}
const prefix = referenceDecodeBase64Prefix(content, RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES)
if (!prefix) {
return false
}
const dimensions = readRasterImageDimensions(prefix)
return dimensions !== null && !isRasterImagePreviewDimensions(dimensions)
}
// Why a module mock: the byte length handed to the dimension reader is the direct measure of how
// much of the payload the preview check decoded, and it is the only observable difference between
// the early-stopping probe and the full-payload decode it replaces.
const { dimensionReadLengths } = vi.hoisted(() => ({ dimensionReadLengths: [] as number[] }))
vi.mock('./raster-image-dimensions', async (importOriginal) => {
const actual = await importOriginal<typeof RasterImageDimensionsModule>()
return {
...actual,
readRasterImageDimensions: (bytes: Uint8Array) => {
dimensionReadLengths.push(bytes.byteLength)
return actual.readRasterImageDimensions(bytes)
}
}
})
// ── Reference decoder: the pre-change implementation, verbatim ──────────────────────────────────
const BASE64_PADDING = -2
const INVALID_BASE64 = -1
function base64Value(code: number): number {
if (code >= 65 && code <= 90) {
return code - 65
}
if (code >= 97 && code <= 122) {
return code - 71
}
if (code >= 48 && code <= 57) {
return code + 4
}
if (code === 43) {
return 62
}
if (code === 47) {
return 63
}
if (code === 61) {
return BASE64_PADDING
}
return INVALID_BASE64
}
function isWhitespace(code: number): boolean {
return code === 9 || code === 10 || code === 12 || code === 13 || code === 32
}
function referenceWriteQuartet(
output: Uint8Array,
offset: number,
quartet: readonly number[]
): { bytesWritten: number; padded: boolean } | null {
const [a, b, c, d] = quartet
if (a === undefined || b === undefined || a < 0 || b < 0) {
return null
}
if (c === BASE64_PADDING) {
if (d !== BASE64_PADDING) {
return null
}
if (offset < output.length) {
output[offset] = (a << 2) | (b >> 4)
}
return { bytesWritten: Math.min(1, output.length - offset), padded: true }
}
if (c === undefined || c < 0) {
return null
}
if (offset < output.length) {
output[offset] = (a << 2) | (b >> 4)
}
if (offset + 1 < output.length) {
output[offset + 1] = ((b & 15) << 4) | (c >> 2)
}
if (d === BASE64_PADDING) {
return { bytesWritten: Math.min(2, output.length - offset), padded: true }
}
if (d === undefined || d < 0) {
return null
}
if (offset + 2 < output.length) {
output[offset + 2] = ((c & 3) << 6) | d
}
return { bytesWritten: Math.min(3, output.length - offset), padded: false }
}
function referenceDecodeBase64Prefix(content: string, maxBytes: number): Uint8Array | null {
const capacity = Math.min(maxBytes, Math.ceil(content.length / 4) * 3)
const output = new Uint8Array(capacity)
const quartet: number[] = []
let outputLength = 0
let padded = false
for (let index = 0; index < content.length && outputLength < capacity; index += 1) {
const code = content.charCodeAt(index)
if (isWhitespace(code)) {
continue
}
if (padded) {
return null
}
const value = base64Value(code)
if (value === INVALID_BASE64) {
return null
}
quartet.push(value)
if (quartet.length !== 4) {
continue
}
const decoded = referenceWriteQuartet(output, outputLength, quartet)
if (!decoded) {
return null
}
outputLength += decoded.bytesWritten
padded = decoded.padded
quartet.length = 0
}
if (!padded && outputLength < capacity && quartet.length > 0) {
if (quartet.length === 1 || quartet.includes(BASE64_PADDING)) {
return null
}
while (quartet.length < 4) {
quartet.push(BASE64_PADDING)
}
const decoded = referenceWriteQuartet(output, outputLength, quartet)
if (!decoded) {
return null
}
outputLength += decoded.bytesWritten
}
return output.subarray(0, outputLength)
}
// ── Fixtures ───────────────────────────────────────────────────────────────────────────────────
function pngBytes(totalBytes: number, width: number, height: number): Buffer {
const bytes = Buffer.alloc(Math.max(totalBytes, 24))
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(bytes)
bytes.writeUInt32BE(13, 8)
bytes.write('IHDR', 12, 'ascii')
bytes.writeUInt32BE(width, 16)
bytes.writeUInt32BE(height, 20)
for (let index = 24; index < bytes.length; index += 1) {
bytes[index] = (index * 31 + 7) & 0xff
}
return bytes
}
/** SOI, `metadataBytes` of APP2 padding (real cameras chain many segments), then SOF0. */
function jpegBytes(
metadataBytes: number,
width: number,
height: number,
trailingBytes = 4096
): Buffer {
const parts: Buffer[] = [Buffer.from([0xff, 0xd8])]
for (let written = 0; written < metadataBytes;) {
const size = Math.min(65_533, metadataBytes - written)
const header = Buffer.alloc(4)
header.writeUInt16BE(0xffe2)
header.writeUInt16BE(size + 2, 2)
parts.push(header, Buffer.alloc(size))
written += size
}
const sof = Buffer.alloc(11)
sof.writeUInt16BE(0xffc0)
sof.writeUInt16BE(8, 2)
sof[4] = 8
sof.writeUInt16BE(height, 5)
sof.writeUInt16BE(width, 7)
parts.push(sof, Buffer.alloc(trailingBytes))
return Buffer.concat(parts)
}
function gifBytes(width: number, height: number): Buffer {
const gif = Buffer.alloc(64)
gif.write('GIF89a', 0, 'ascii')
gif.writeUInt16LE(width, 6)
gif.writeUInt16LE(height, 8)
return gif
}
function webpBytes(width: number, height: number): Buffer {
const webp = Buffer.alloc(64)
webp.write('RIFF', 0, 'ascii')
webp.writeUInt32LE(50, 4)
webp.write('WEBP', 8, 'ascii')
webp.write('VP8X', 12, 'ascii')
webp.writeUInt32LE(10, 16)
webp.writeUIntLE(width - 1, 24, 3)
webp.writeUIntLE(height - 1, 27, 3)
return webp
}
const DECODE_FIXTURES: { label: string; content: string }[] = [
{ label: 'png', content: pngBytes(24, 512, 512).toString('base64') },
{ label: 'png padded once', content: pngBytes(26, 512, 512).toString('base64') },
{ label: 'png padded twice', content: pngBytes(25, 512, 512).toString('base64') },
{ label: 'png 70 KiB', content: pngBytes(70_000, 512, 512).toString('base64') },
{ label: 'jpeg', content: jpegBytes(0, 640, 480).toString('base64') },
{ label: 'jpeg 70 KiB exif', content: jpegBytes(70_000, 4000, 3000).toString('base64') },
{ label: 'gif', content: gifBytes(320, 240).toString('base64') },
{ label: 'webp', content: webpBytes(800, 600).toString('base64') },
{ label: 'empty', content: '' },
{ label: 'one character', content: 'A' },
{ label: 'two characters', content: 'AB' },
{ label: 'three characters', content: 'ABC' },
{ label: 'invalid character', content: 'AB*D' },
{ label: 'invalid tail', content: `${pngBytes(24, 4, 4).toString('base64')}!!!` },
{ label: 'padding mid-payload', content: 'AAAA=AAA' },
{ label: 'lone padding in tail', content: 'AAAAAB=' },
{ label: 'single padding', content: 'AAAAAA==' },
{ label: 'double padding', content: 'AAAAAAA=' },
{ label: 'stray padding after padding', content: 'AAAA====' },
{
label: 'line-wrapped png',
content: pngBytes(70_000, 512, 512)
.toString('base64')
.replace(/(.{76})/g, '$1\r\n')
},
{ label: 'leading and trailing whitespace', content: `\n\t ${'AAAA'} \r\n` },
{ label: 'truncated png header', content: pngBytes(24, 512, 512).toString('base64').slice(0, 18) }
]
const DECODE_CAPS = [
0,
1,
2,
3,
4,
23,
24,
25,
63,
64,
65,
1024,
RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
]
describe('decodeBase64Prefix', () => {
it('decodes byte-for-byte identically to the reference implementation', () => {
for (const { label, content } of DECODE_FIXTURES) {
for (const maxBytes of DECODE_CAPS) {
const expected = referenceDecodeBase64Prefix(content, maxBytes)
const actual = decodeBase64Prefix(content, maxBytes)
const detail = `${label} @ maxBytes=${maxBytes}`
if (expected === null) {
expect(actual, detail).toBeNull()
continue
}
expect(actual, detail).not.toBeNull()
expect(Array.from(actual!), detail).toEqual(Array.from(expected))
}
}
})
it('stops at the byte cap instead of decoding the whole payload', () => {
const content = pngBytes(70_000, 512, 512).toString('base64')
expect(decodeBase64Prefix(content, 32)?.byteLength).toBe(32)
})
})
describe('exceedsRasterImagePreviewLimits', () => {
beforeEach(() => {
dimensionReadLengths.length = 0
})
it('measures a large image from its first bytes, not its last', () => {
// Regression guard: before the probe this decoded all 5 MiB before reading 24 bytes of IHDR.
const content = pngBytes(5 * 1024 * 1024, 512, 512).toString('base64')
expect(exceedsRasterImagePreviewLimits(content, 'image/png')).toBe(false)
expect(dimensionReadLengths).toEqual([64])
})
it('widens the probe until a JPEG SOF past its metadata is reachable', () => {
const content = jpegBytes(70_000, 4000, 3000, 3_000_000).toString('base64')
expect(exceedsRasterImagePreviewLimits(content, 'image/jpeg')).toBe(false)
expect(dimensionReadLengths).toEqual([64, 1024, 16_384, 262_144])
// Far below the ~3 MiB the payload decodes to, and below the 8 MiB fallback cap.
expect(dimensionReadLengths.at(-1)!).toBeLessThan(RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES)
})
it('re-reads the whole payload before suppressing an over-limit image', () => {
const content = pngBytes(70_000, 32_769, 1).toString('base64')
expect(exceedsRasterImagePreviewLimits(content, 'image/png')).toBe(true)
// The header answers at 64 bytes, but a suppression verdict is only taken from the same
// full-payload decode the unprobed implementation used, so invalid base64 past the header
// still demotes the answer to "could not measure".
expect(dimensionReadLengths).toEqual([64, 70_000])
})
it('keeps rendering an over-limit header whose payload is not valid base64', () => {
const content = `${pngBytes(70_000, 32_769, 1).toString('base64')}!!!`
expect(exceedsRasterImagePreviewLimits(content, 'image/png')).toBe(false)
})
it('agrees with the unprobed implementation on every fixture and mime type', () => {
const mimeTypes = [
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'image/svg+xml',
undefined
]
const fixtures = [
...DECODE_FIXTURES,
{ label: 'over-limit png', content: pngBytes(24, 32_769, 1).toString('base64') },
{ label: 'over-limit pixels png', content: pngBytes(24, 8192, 8192).toString('base64') },
{ label: 'over-limit gif', content: gifBytes(65_535, 65_535).toString('base64') },
{ label: 'over-limit jpeg', content: jpegBytes(70_000, 40_000, 40_000).toString('base64') },
{ label: 'over-limit webp', content: webpBytes(40_000, 40_000).toString('base64') }
]
for (const { label, content } of fixtures) {
for (const mimeType of mimeTypes) {
expect(exceedsRasterImagePreviewLimits(content, mimeType), `${label} / ${mimeType}`).toBe(
unprobedExceeds(content, mimeType)
)
}
}
})
})
+62 -21
View File
@@ -34,13 +34,20 @@ function isWhitespace(code: number): boolean {
return code === 9 || code === 10 || code === 12 || code === 13 || code === 32
}
/** `quartetLength` under 4 is a final short group; the missing slots decode as `=` padding. */
function writeQuartet(
output: Uint8Array,
offset: number,
quartet: readonly number[]
quartet: readonly number[],
quartetLength: number
): { bytesWritten: number; padded: boolean } | null {
const [a, b, c, d] = quartet
if (a === undefined || b === undefined || a < 0 || b < 0) {
// Index reads, not `const [a, b, c, d] = quartet`: destructuring an array runs the iterator
// protocol (Symbol.iterator plus four `.next()` calls) once per four input characters.
const a = quartet[0]
const b = quartet[1]
const c = quartetLength > 2 ? quartet[2] : BASE64_PADDING
const d = quartetLength > 3 ? quartet[3] : BASE64_PADDING
if (quartetLength < 2 || a === undefined || b === undefined || a < 0 || b < 0) {
return null
}
if (c === BASE64_PADDING) {
@@ -73,10 +80,13 @@ function writeQuartet(
return { bytesWritten: Math.min(3, output.length - offset), padded: false }
}
function decodeBase64Prefix(content: string, maxBytes: number): Uint8Array | null {
/** Exported so the decode can be compared byte-for-byte against a reference implementation. */
export function decodeBase64Prefix(content: string, maxBytes: number): Uint8Array | null {
const capacity = Math.min(maxBytes, Math.ceil(content.length / 4) * 3)
const output = new Uint8Array(capacity)
const quartet: number[] = []
// Fixed four slots plus a counter, never resized: `quartet.length = 0` deoptimizes the array.
const quartet = [0, 0, 0, 0]
let quartetLength = 0
let outputLength = 0
let padded = false
@@ -92,27 +102,27 @@ function decodeBase64Prefix(content: string, maxBytes: number): Uint8Array | nul
if (value === INVALID_BASE64) {
return null
}
quartet.push(value)
if (quartet.length !== 4) {
quartet[quartetLength] = value
quartetLength += 1
if (quartetLength !== 4) {
continue
}
const decoded = writeQuartet(output, outputLength, quartet)
const decoded = writeQuartet(output, outputLength, quartet, 4)
if (!decoded) {
return null
}
outputLength += decoded.bytesWritten
padded = decoded.padded
quartet.length = 0
quartetLength = 0
}
if (!padded && outputLength < capacity && quartet.length > 0) {
if (quartet.length === 1 || quartet.includes(BASE64_PADDING)) {
return null
if (!padded && outputLength < capacity && quartetLength > 0) {
for (let index = 0; index < quartetLength; index += 1) {
if (quartet[index] === BASE64_PADDING) {
return null
}
}
while (quartet.length < 4) {
quartet.push(BASE64_PADDING)
}
const decoded = writeQuartet(output, outputLength, quartet)
const decoded = writeQuartet(output, outputLength, quartet, quartetLength)
if (!decoded) {
return null
}
@@ -121,6 +131,13 @@ function decodeBase64Prefix(content: string, maxBytes: number): Uint8Array | nul
return output.subarray(0, outputLength)
}
// First probe: past every fixed-offset header (PNG 24, GIF 10, WebP 30, BMP 26) and a JFIF-only
// JPEG's SOF, so an icon or screenshot is measured from its first bytes instead of its last.
const RASTER_IMAGE_HEADER_PROBE_BYTES = 64
// Growth per miss. JPEG SOF sits past however much EXIF/ICC/MPF the camera wrote, so the probe
// widens geometrically: total decoded stays within ~1.07x of the bytes the header actually needed.
const RASTER_IMAGE_HEADER_PROBE_GROWTH = 16
/**
* Whether the encoded dimensions are known to exceed the preview limits.
*
@@ -135,10 +152,34 @@ export function exceedsRasterImagePreviewLimits(
if (!isKnownRasterImageMimeType(mimeType)) {
return false
}
const prefix = decodeBase64Prefix(content, RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES)
if (!prefix) {
return false
let probeBytes = RASTER_IMAGE_HEADER_PROBE_BYTES
for (;;) {
const prefix = decodeBase64Prefix(content, probeBytes)
// A short probe only ever fails where the whole payload would: it walks a strict prefix of the
// same characters through the same state machine.
if (!prefix) {
return false
}
// Shorter than asked for means the payload ran out, so a wider probe cannot add bytes.
const exhausted =
prefix.byteLength < probeBytes || probeBytes >= RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
const dimensions = readRasterImageDimensions(prefix)
if (dimensions !== null) {
const withinLimits = isRasterImagePreviewDimensions(dimensions)
if (withinLimits || exhausted) {
return !withinLimits
}
// About to suppress: redo the decode over the whole payload so the verdict stays the one the
// full read gives, including its rejection of base64 that turns invalid past the header.
probeBytes = RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
continue
}
if (exhausted) {
return false
}
probeBytes = Math.min(
probeBytes * RASTER_IMAGE_HEADER_PROBE_GROWTH,
RASTER_IMAGE_PREVIEW_HEADER_MAX_BYTES
)
}
const dimensions = readRasterImageDimensions(prefix)
return dimensions !== null && !isRasterImagePreviewDimensions(dimensions)
}
+98 -1
View File
@@ -1,6 +1,22 @@
import { describe, expect, it } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import type * as ImageDataUriModule from './image-data-uri'
import { githubAvatarIcon, githubAvatarSlug, sanitizeRepoIcon } from './repo-icon'
// Why a module mock: `validateRasterImageDataUri` is the leaf that base64-decodes an inline icon's
// header, so counting its invocations is the direct measure of what re-hydrating a repo costs.
const { dataUriValidations } = vi.hoisted(() => ({ dataUriValidations: { count: 0 } }))
vi.mock('./image-data-uri', async (importOriginal) => {
const actual = await importOriginal<typeof ImageDataUriModule>()
return {
...actual,
validateRasterImageDataUri: (dataUri: string) => {
dataUriValidations.count += 1
return actual.validateRasterImageDataUri(dataUri)
}
}
})
const PNG_1X1_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII='
const WEBP_1X1_BASE64 = 'UklGRhoAAABXRUJQVlA4IA4AAAAwAQCdASoBAAEAAQIlSkwAAA=='
@@ -188,3 +204,84 @@ describe('githubAvatarSlug', () => {
expect(githubAvatarSlug(null, undefined)).toBeNull()
})
})
describe('repo icon source validation memo', () => {
const HYDRATIONS = 25
function uploadIcon(width: number): { type: 'image'; src: string; source: 'upload' } {
return { type: 'image', src: `data:image/png;base64,${pngBase64(width, 1)}`, source: 'upload' }
}
it('validates each distinct icon src once across repeated hydrations', () => {
const icons = [uploadIcon(2), uploadIcon(3), uploadIcon(4)]
// Warm the memo the way the first hydration would, then measure steady state.
for (const icon of icons) {
sanitizeRepoIcon(icon)
}
dataUriValidations.count = 0
for (let hydration = 0; hydration < HYDRATIONS; hydration += 1) {
for (const icon of icons) {
expect(sanitizeRepoIcon(icon)).toEqual(icon)
}
}
// Unmemoized this is HYDRATIONS x icons full base64 header decodes; memoized an unchanged
// persisted icon costs nothing.
expect(dataUriValidations.count).toBe(0)
})
it('re-validates as soon as the src changes', () => {
dataUriValidations.count = 0
expect(sanitizeRepoIcon(uploadIcon(11))).toEqual(uploadIcon(11))
expect(sanitizeRepoIcon(uploadIcon(12))).toEqual(uploadIcon(12))
expect(dataUriValidations.count).toBe(2)
})
it('keeps the verdict specific to the icon source', () => {
const src = `data:image/webp;base64,${WEBP_1X1_BASE64}`
expect(sanitizeRepoIcon({ type: 'image', src, source: 'file' })).toEqual({
type: 'image',
src,
source: 'file'
})
// WebP is a `file` icon only; sharing one cache across sources would accept it as an upload.
expect(sanitizeRepoIcon({ type: 'image', src, source: 'upload' })).toBeUndefined()
})
// Guard for the removed cap: the memo hangs off the persisted icon object, so it holds a verdict
// for every live icon no matter how many there are. A fixed-size map would evict the earliest
// entries here and re-decode them on the next hydration.
it('keeps a verdict for every live icon, however many repos have one', () => {
const LIVE_ICONS = 200
const icons = Array.from({ length: LIVE_ICONS }, (_, index) => uploadIcon(1000 + index))
for (const icon of icons) {
sanitizeRepoIcon(icon)
}
dataUriValidations.count = 0
for (const icon of icons) {
expect(sanitizeRepoIcon(icon)).toEqual(icon)
}
expect(dataUriValidations.count).toBe(0)
})
// Guard for the hazard object keying introduces: the stored src/source are re-checked on a hit,
// so a persisted icon edited in place can never be served its previous verdict.
it('re-validates an icon object whose src or source is mutated in place', () => {
const icon = { type: 'image', src: `data:image/png;base64,${pngBase64(7, 1)}`, source: 'file' }
expect(sanitizeRepoIcon(icon)).toEqual(icon)
icon.src = `data:image/png;base64,${pngBase64(8, 1)}`
dataUriValidations.count = 0
expect(sanitizeRepoIcon(icon)).toEqual(icon)
expect(dataUriValidations.count).toBe(1)
// WebP is a `file` icon but not an `upload` icon, so the same object must flip verdicts.
icon.src = `data:image/webp;base64,${WEBP_1X1_BASE64}`
expect(sanitizeRepoIcon(icon)).toEqual(icon)
icon.source = 'upload'
expect(sanitizeRepoIcon(icon)).toBeUndefined()
})
})
+30 -2
View File
@@ -79,7 +79,7 @@ function normalizeGitHubAvatarHost(rawHost?: string): string {
}
}
function isSupportedImageSrc(src: string, source: RepoIconImageSource): boolean {
function computeIsSupportedImageSrc(src: string, source: RepoIconImageSource): boolean {
if (source === 'upload') {
return (
/^data:image\/png;base64,[A-Za-z0-9+/=\s]+$/i.test(src) &&
@@ -112,6 +112,34 @@ function isSupportedImageSrc(src: string, source: RepoIconImageSource): boolean
return url.hostname === 'www.google.com' && url.pathname === '/s2/favicons'
}
type ImageSrcVerdict = { src: unknown; source: unknown; supported: boolean }
/**
* Why: `getRepos()` re-hydrates every repo on every call, and validating one inline data URI means
* scanning a 400 KB string twice with a regex and base64-decoding its header. `hydrateRepo` is
* handed the *same* persisted `repoIcon` object every time, so the verdict is cached on that object
* and dies with it — no cap, no eviction, and nothing retained once a repo or an icon is replaced.
*
* `src`/`source` are re-checked on a hit, so mutating the persisted icon in place cannot serve a
* stale verdict. Both are the identical string references in the steady state, so the compare is a
* pointer check, not a 400 KB scan.
*/
const imageSrcVerdicts = new WeakMap<object, ImageSrcVerdict>()
function isSupportedImageSrc(
candidate: Record<string, unknown>,
src: string,
source: RepoIconImageSource
): boolean {
const cached = imageSrcVerdicts.get(candidate)
if (cached && cached.src === candidate.src && cached.source === candidate.source) {
return cached.supported
}
const supported = computeIsSupportedImageSrc(src, source)
imageSrcVerdicts.set(candidate, { src: candidate.src, source: candidate.source, supported })
return supported
}
export function sanitizeRepoIcon(value: unknown): RepoIcon | null | undefined {
if (value === undefined) {
return undefined
@@ -146,7 +174,7 @@ export function sanitizeRepoIcon(value: unknown): RepoIcon | null | undefined {
if (!isRepoIconImageSource(source) || src.length > MAX_REPO_ICON_DATA_URL_LENGTH) {
return undefined
}
if (!isSupportedImageSrc(src, source)) {
if (!isSupportedImageSrc(candidate, src, source)) {
return undefined
}
const label = typeof candidate.label === 'string' ? candidate.label.trim().slice(0, 80) : ''