/** * Files whose first segment says what an archive holds: named first RAR * volumes (.part01.rar, .rar), 7z (.7z, .7z.001: its type; the listing is at * its end, the 7z-tail), ZIP (its first entries) and `.bin`, an extension * uploaders use for archive volumes too (sniffed by content). */ export function isArchiveStart(name: string): boolean { if (/\.(bin|zip)$/i.test(name)) return true const volume = sevenZipVolume(name) if (volume === 0 || volume === 1) return true return /\.part0*1\.rar$/i.test(name) || (/\.rar$/i.test(name) && !/\.part\d+\.rar$/i.test(name)) } /** * A later RAR volume (.part02.rar, .r00) or a par2 recovery volume * (.vol03+04.par2): the volume number is readable even under a random stem. * The set is listed from its first volume and named from its index .par2, * so the first segment of these says nothing new (LOG.md 2026-10-04). */ export function isLaterVolume(name: string): boolean { const part = /\.part(\d+)\.rar$/i.exec(name) if (part) return Number(part[1]) > 1 return /\.r\d{2,3}$/i.test(name) || /\.vol\d+[+-]\d+\.par2$/i.test(name) } /** * The set and 1-based number of a RAR volume: `x.part03.rar` is volume 3 of * `x`, and in the old naming `x.rar` is 1, `x.r00` 2, `x.r01` 3. Null for * other names. */ export function rarVolume(name: string): { base: string; number: number } | null { const part = /^(.*)\.part(\d+)\.rar$/i.exec(name) if (part) return { base: part[1].toLowerCase(), number: Number(part[2]) } const old = /^(.*)\.r(\d{2,3})$/i.exec(name) if (old) return { base: old[1].toLowerCase(), number: Number(old[2]) + 2 } const first = /^(.*)\.rar$/i.exec(name) if (first) return { base: first[1].toLowerCase(), number: 1 } return null } /** Volume number of a split 7z name (`x.7z.001` → 1), 0 for an unsplit `.7z`, null otherwise. */ export function sevenZipVolume(name: string): number | null { const split = /\.7z\.(\d{1,4})$/i.exec(name) if (split) return Number(split[1]) return /\.7z$/i.test(name) ? 0 : null }