2f59a6ce63
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>
100 lines
3.4 KiB
TypeScript
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()
|
|
}
|