Keep repo icon href probes inside repo

This commit is contained in:
Neil
2026-05-30 04:20:05 -07:00
parent 7091f5b2b9
commit 750698cf45
2 changed files with 46 additions and 1 deletions
+19
View File
@@ -69,6 +69,25 @@ describe('detectRepoIcon', () => {
})
})
it('does not resolve declared icon hrefs outside the repo', async () => {
const rootPath = await makeTempRepoDir()
const repoPath = join(rootPath, 'repo')
await mkdir(repoPath)
await writeFile(join(repoPath, 'index.html'), '<link rel="icon" href="../outside.png">')
await writeFile(join(rootPath, 'outside.png'), Buffer.from(PNG_1X1_BASE64, 'base64'))
await writeFile(
join(repoPath, 'package.json'),
JSON.stringify({ homepage: 'https://app.example.com/docs' })
)
await expect(detectRepoIcon({ repoPath, kind: 'folder' })).resolves.toEqual({
type: 'image',
src: 'https://www.google.com/s2/favicons?domain=app.example.com&sz=64',
source: 'favicon',
label: 'Website favicon'
})
})
it('falls back to the GitHub owner avatar for GitHub repos', async () => {
const repoPath = await makeTempRepoDir()
await gitExecFileAsync(['init'], { cwd: repoPath })
+27 -1
View File
@@ -75,9 +75,35 @@ function extractIconHref(source: string): string | null {
return source.match(LINK_ICON_HTML_RE)?.[1] ?? source.match(LINK_ICON_OBJECT_RE)?.[1] ?? null
}
function normalizeIconRelativePath(relativePath: string): string | null {
const normalized = relativePath.replace(/\\/g, '/').trim()
if (
!normalized ||
normalized.startsWith('/') ||
normalized.includes('\0') ||
/^[A-Za-z][A-Za-z0-9+.-]*:/.test(normalized)
) {
return null
}
const segments = normalized.split('/').filter((segment) => segment && segment !== '.')
if (segments.length === 0 || segments.some((segment) => segment === '..')) {
return null
}
return segments.join('/')
}
function iconHrefCandidates(href: string): string[] {
const clean = href.replace(/^\/+/, '')
return [`public/${clean}`, clean]
const candidates: string[] = []
for (const candidate of [`public/${clean}`, clean]) {
// Why: icon hrefs are repo-controlled input; never let detection probe above
// the selected repo root while looking for a best-effort visual.
const normalized = normalizeIconRelativePath(candidate)
if (normalized && !candidates.includes(normalized)) {
candidates.push(normalized)
}
}
return candidates
}
async function readLocalPngIcon(repoPath: string, relativePath: string): Promise<RepoIcon | null> {