export function fileExtension(pathOrName: string): string { const clean = pathOrName.split(/[?#]/, 1)[0] ?? '' const name = clean.split('/').pop() ?? clean const index = name.lastIndexOf('.') return index > 0 ? name.slice(index + 1).toLowerCase() : '' } export function sanitizeFileName(name: string, fallback = 'file'): string { const sanitized = name .trim() .replace(/[\\/:*?"<>|\u0000-\u001f]/g, '_') .replace(/\s+/g, ' ') return sanitized || fallback } /** Resolve an RFC 5987/6266 filename from a Content-Disposition header. */ export function parseContentDispositionFileName(header: string): string | null { const encoded = header.match(/filename\*=(?:[^']*'[^']*')?([^;\s]+)/i)?.[1] if (encoded) { try { return sanitizeFileName(decodeURIComponent(encoded.replace(/['"]/g, ''))) } catch { // Fall through to the non-encoded forms. } } const quoted = header.match(/filename="([^"]+)"/i)?.[1] if (quoted?.trim()) return sanitizeFileName(quoted) const plain = header.match(/filename=([^;\s"]+)/i)?.[1] return plain?.trim() ? sanitizeFileName(plain) : null } export function fileNameFromUrl(url: string, fallback = 'download'): string { try { const pathname = new URL(url).pathname const candidate = pathname.split('/').filter(Boolean).at(-1) return candidate ? sanitizeFileName(decodeURIComponent(candidate), fallback) : fallback } catch { const candidate = url.split(/[?#]/, 1)[0]?.split('/').filter(Boolean).at(-1) return candidate ? sanitizeFileName(candidate, fallback) : fallback } }