import { crc32 } from 'node:zlib' /** * 7-Zip archives, independent of AdonisJS. A 7z archive starts with a * 32-byte signature header that points to the archive's header (the file * list) at its very end, often LZMA-compressed ("encoded") with its packed * data just before it. So the list needs the first 32 bytes and the tail: * for a split archive (.7z.001, .002, …), the start of the first volume and * the end of the last. Listing itself is left to libarchive (bsdtar). */ const SIGNATURE = Buffer.from([0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c]) const SIGNATURE_HEADER_SIZE = 32 /** Header property ids (7zFormat.txt). */ const K_ENCODED_HEADER = 0x17 const K_PACK_INFO = 0x06 export interface SevenZipStart { /** Where the archive header starts, from the start of the archive. */ headerStart: number headerSize: number headerCrc: number /** The archive's total size (the header is its last part). */ archiveSize: number } /** Reads the signature header; null when `data` isn't a 7z start or its CRC fails. */ export function parseSevenZipStart(data: Buffer): SevenZipStart | null { if (data.length < SIGNATURE_HEADER_SIZE || !data.subarray(0, 6).equals(SIGNATURE)) return null if ((crc32(data.subarray(12, 32)) >>> 0) !== data.readUInt32LE(8)) return null const offset = Number(data.readBigUInt64LE(12)) const size = Number(data.readBigUInt64LE(20)) const headerStart = SIGNATURE_HEADER_SIZE + offset return { headerStart, headerSize: size, headerCrc: data.readUInt32LE(28), archiveSize: headerStart + size } } /** Whether `header` (the bytes at headerStart) is the archive header its signature header describes. */ export function checkHeader(start: SevenZipStart, header: Buffer): boolean { return header.length === start.headerSize && (crc32(header) >>> 0) === start.headerCrc } /** * Where the bytes needed for listing start: the header itself, or for an * encoded header the packed (compressed) header stream it points to. */ export function listingStart(start: SevenZipStart, header: Buffer): number { if (header[0] !== K_ENCODED_HEADER || header[1] !== K_PACK_INFO) return start.headerStart const [packPos] = readNumber(header, 2) return SIGNATURE_HEADER_SIZE + packPos } /** * 7z's variable-length number: the leading 1-bits of the first byte count * the little-endian bytes that follow; its remaining bits are the high part. */ export function readNumber(data: Buffer, pos: number): [value: number, next: number] { const first = data[pos] let mask = 0x80 let value = 0 for (let i = 0; i < 8; i++) { if ((first & mask) === 0) { value += (first & (mask - 1)) * 2 ** (8 * i) return [value, pos + 1 + i] } value += data[pos + 1 + i] * 2 ** (8 * i) mask >>= 1 } return [value, pos + 9] } export interface SevenZipEntry { name: string size: number directory: boolean } /** * Entries from `bsdtar -tv` output, e.g. * `-rw-rw-r-- 0 0 0 3000000 Sep 30 19:13 Some.Release/movie.mkv`. */ export function parseBsdtarListing(stdout: string): SevenZipEntry[] { const entries: SevenZipEntry[] = [] for (const line of stdout.split('\n')) { const match = /^([-dlbcps])\S*\s+\d+\s+\S+\s+\S+\s+(\d+)\s+\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{2}|\d{4})\s(.+)$/.exec(line) if (!match) continue const directory = match[1] === 'd' entries.push({ name: directory ? match[3].replace(/\/$/, '') : match[3], size: Number(match[2]), directory }) } return entries } /** 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 }