Files
AeroFetch/src/main/deeplink.ts
T
debont80 2f59a6ce63 Fix M10: consolidate .url Internet Shortcut parsing into shared utility
Both DownloadBar (renderer, drag-drop) and deeplink.ts (main, argv) had their
own regex to extract URL= from a Windows Internet Shortcut file -- with subtly
different patterns (/^\s*URL\s*=\s*(\S+)/ vs /^URL=(.+)$/).

Added parseUrlShortcutContent() to @shared/ipc. Both callers now delegate to
it and apply their own http/https guard (looksLikeUrl in renderer, asHttpUrl
in main). The local parseUrlFile wrapper in DownloadBar is kept as the
validation compositing point; it's now a one-liner.

typecheck + 242 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:35:00 -04:00

100 lines
3.4 KiB
TypeScript

import { app, shell, type BrowserWindow } from 'electron'
import { existsSync, openSync, readSync, closeSync } from 'fs'
import { join } from 'path'
import { assertHttpUrl } from './url'
import { parseUrlShortcutContent } from '@shared/ipc'
/** Only ever forward http(s) targets into the app — same restriction the
* external-link window-open handler in index.ts applies to in-page links.
* Delegates to the download path's guard so an incoming deep-link target is
* validated AND parser-normalised (no embedded tabs/newlines/control chars
* reach the renderer); returns null instead of throwing for the argv scan.
* (audit T3 / F5) */
function asHttpUrl(candidate: string): string | null {
try {
return assertHttpUrl(candidate)
} catch {
return null
}
}
/** Reads a Windows Internet Shortcut (.url) file's target — what Explorer's
* "Send to" menu hands us when the user sends a saved link to AeroFetch.
* Only the first 64 KB is read: a real shortcut is a few hundred bytes, so this
* caps memory and limits exposure if argv points at a pathological or oversized
* file that merely ends in `.url`. (audit T5) */
const MAX_URL_FILE_BYTES = 64 * 1024
function readUrlShortcut(path: string): string | null {
let fd: number | null = null
try {
fd = openSync(path, 'r')
const buf = Buffer.alloc(MAX_URL_FILE_BYTES)
const bytes = readSync(fd, buf, 0, buf.length, 0)
const url = parseUrlShortcutContent(buf.toString('utf8', 0, bytes))
return url ? asHttpUrl(url) : null
} catch {
return null
} finally {
if (fd !== null) closeSync(fd)
}
}
/**
* Pulls a download target out of process argv — either the `aerofetch://`
* custom protocol (`aerofetch://download?url=<encoded>`) or a `.url` Internet
* Shortcut path Explorer's "Send to" menu passes us (see registerSendToShortcut).
*/
export function extractIncomingUrl(argv: string[]): string | null {
for (const arg of argv) {
if (/^aerofetch:\/\//i.test(arg)) {
try {
const target = new URL(arg).searchParams.get('url')
const valid = target && asHttpUrl(target)
if (valid) return valid
} catch {
/* malformed protocol invocation — ignore */
}
} else if (/\.url$/i.test(arg) && existsSync(arg)) {
const target = readUrlShortcut(arg)
if (target) return target
}
}
return null
}
/**
* Adds/refreshes a "Send to AeroFetch" entry in Explorer's SendTo menu — the
* closest Windows analog to Android's share sheet a Win32 (non-MSIX) app can
* offer; registering as an actual Share Target requires an MSIX package.
* Best-effort and idempotent (re-running just overwrites the same shortcut),
* so a failure here should never block startup.
*/
export function registerSendToShortcut(): void {
if (process.platform !== 'win32') return
try {
const shortcutPath = join(
app.getPath('appData'),
'Microsoft',
'Windows',
'SendTo',
'AeroFetch.lnk'
)
shell.writeShortcutLink(shortcutPath, {
target: process.execPath,
description: 'Send to AeroFetch',
icon: process.execPath,
iconIndex: 0
})
} catch {
/* SendTo integration is a nice-to-have */
}
}
/** Focuses (and un-minimizes) an existing window — used when a second launch
* (protocol or SendTo) should hand its URL to the already-running instance. */
export function focusWindow(win: BrowserWindow): void {
if (win.isMinimized()) win.restore()
win.show()
win.focus()
}