Apply all CODE-AUDIT.md findings (S1-S5, P1-P3, M1-M3)
Security: - S1 (backup.ts): require explicit confirmation before enabling custom-command templates from a backup file; import templates in a disabled state if declined - S2 (cookies.ts): deny non-http/https popups in cookie login window, matching the main window's setWindowOpenHandler defence - S3 (download.ts): main-process concurrency cap — startDownload now rejects spawns when active.size >= maxConcurrent (defence-in-depth; renderer already enforces this but should not be the only gate) - S4 (settings.ts, validation.ts): reject filenameTemplate values containing path traversal (.. segments or absolute paths); validate outputDir is absolute - S5 (history.ts, errorlog.ts, templates.ts, validation.ts): per-field validation on JSON reads — invalid entries dropped rather than blindly trusted Performance: - P1 (settings.ts): getSettings() now only writes to disk when sanitization actually changed a value; removes synchronous file I/O on every hot-path read - P2 (ipc.ts, download.ts, downloads.ts): renderer forwards its pre-probed metadata (title/channel/duration) via StartDownloadOptions.meta; main uses it directly instead of spawning a redundant second yt-dlp probe Maintainability: - M1 (log.ts, download.ts, probe.ts): extract duplicated cleanError() to src/main/log.ts; both callers import from the shared module - M2 (buildArgs.ts): document parseExtraArgs escape-sequence limitations - M3 (electron-builder.yml): clarify icon TODO as a pre-release action - P3 (downloads.ts): document that lowering maxConcurrent mid-flight does not pause active downloads (intended behaviour, now written down) Tests: - Add test/validation.test.ts covering isSafeFilenameTemplate, isSafeOutputDir, isValidHistoryEntry, isValidErrorLogEntry, isTemplateLike (76 tests pass) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+23
-17
@@ -8,6 +8,7 @@ import { getCookiesFilePath } from './cookies'
|
||||
import { listTemplates } from './templates'
|
||||
import { assertHttpUrl } from './url'
|
||||
import { buildArgs, parseExtraArgs, formatCommandLine } from './buildArgs'
|
||||
import { cleanError } from './log'
|
||||
import { addErrorLog } from './errorlog'
|
||||
import {
|
||||
IpcChannels,
|
||||
@@ -77,16 +78,6 @@ function parseProgress(rest: string): DownloadProgress | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** Trim yt-dlp's noisy stderr down to the most useful trailing error line. */
|
||||
function cleanError(stderr: string): string {
|
||||
const lines = stderr
|
||||
.split('\n')
|
||||
.map((l) => l.trim())
|
||||
.filter(Boolean)
|
||||
const errLine = [...lines].reverse().find((l) => /^error/i.test(l))
|
||||
return (errLine ?? lines[lines.length - 1] ?? '').replace(/^error:\s*/i, '').trim()
|
||||
}
|
||||
|
||||
function send(wc: WebContents, ev: DownloadEvent): void {
|
||||
if (!wc.isDestroyed()) wc.send(IpcChannels.downloadEvent, ev)
|
||||
}
|
||||
@@ -231,6 +222,13 @@ export function startDownload(
|
||||
if (active.has(opts.id)) {
|
||||
return { ok: false, error: 'A download with this id is already running.' }
|
||||
}
|
||||
// Defence-in-depth: the renderer's pump() already caps concurrency, but the main
|
||||
// process shouldn't trust it — a buggy or compromised renderer could otherwise
|
||||
// spawn unbounded yt-dlp processes. Enforce the same cap here on active spawns.
|
||||
const maxConcurrent = getSettings().maxConcurrent
|
||||
if (active.size >= maxConcurrent) {
|
||||
return { ok: false, error: 'Max concurrent downloads reached. Wait for a slot to free up.' }
|
||||
}
|
||||
|
||||
let child: ChildProcess
|
||||
try {
|
||||
@@ -242,14 +240,22 @@ export function startDownload(
|
||||
const rec: ActiveDownload = { child, canceled: false }
|
||||
active.set(opts.id, rec)
|
||||
|
||||
// Fetch title/channel/duration in parallel so the card fills in quickly.
|
||||
// Also kept locally so the completion notification/error log can show a
|
||||
// real title instead of just the raw URL.
|
||||
// Title/channel/duration are kept locally so the completion notification and
|
||||
// error log can show a real title instead of just the raw URL.
|
||||
let resolvedTitle: string | undefined
|
||||
probeMeta(ytdlp, opts.url).then((meta) => {
|
||||
if (meta?.title) resolvedTitle = meta.title
|
||||
if (meta && active.has(opts.id)) send(wc, { type: 'meta', id: opts.id, meta })
|
||||
})
|
||||
if (opts.meta && (opts.meta.title || opts.meta.channel || opts.meta.durationLabel)) {
|
||||
// The renderer already probed this URL and passed the metadata along — reuse
|
||||
// it instead of spawning a redundant second yt-dlp probe (audit P2).
|
||||
resolvedTitle = opts.meta.title
|
||||
send(wc, { type: 'meta', id: opts.id, meta: opts.meta })
|
||||
} else {
|
||||
// No pre-probed metadata (e.g. a direct paste that skipped the probe) — fetch
|
||||
// it in parallel so the card fills in quickly.
|
||||
probeMeta(ytdlp, opts.url).then((meta) => {
|
||||
if (meta?.title) resolvedTitle = meta.title
|
||||
if (meta && active.has(opts.id)) send(wc, { type: 'meta', id: opts.id, meta })
|
||||
})
|
||||
}
|
||||
|
||||
let stdoutBuf = ''
|
||||
let stderrTail = ''
|
||||
|
||||
Reference in New Issue
Block a user