UNPKG

igir

Version:

🕹 A zero-setup ROM collection manager that sorts, filters, extracts or archives, patches, and reports on collections of any size on any OS.

172 lines (171 loc) • 8.21 kB
import fs from 'node:fs'; import CP437Decoder from './cp437Decoder.js'; import FileRecord from './fileRecord.js'; import FileRecordUtil from './fileRecordUtil.js'; import LocalFileHeader from './localFileHeader.js'; /** * A central directory file header in a zip file. * @see https://en.wikipedia.org/wiki/ZIP_(file_format)#Central_directory_file_header_(CDFH) */ export default class CentralDirectoryFileHeader extends FileRecord { static CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE = Buffer.from('02014b50', 'hex').reverse(); // Size with the signature, and without variable length fields at the end static CENTRAL_DIRECTORY_FILE_HEADER_SIZE = 46; zipFilePath; versionMadeBy; fileCommentLength; fileDiskStart; internalFileAttributes; externalFileAttributes; localFileHeaderRelativeOffset; fileComment; _localFileHeader; constructor(zipFilePath, props) { super(props); this.zipFilePath = zipFilePath; this.versionMadeBy = props.versionMadeBy; this.fileCommentLength = props.fileCommentLength; this.fileDiskStart = props.fileDiskStart; this.internalFileAttributes = props.internalFileAttributes; this.externalFileAttributes = props.externalFileAttributes; this.localFileHeaderRelativeOffset = props.localFileHeaderRelativeOffset; this.fileComment = props.fileComment; } /** * Parse all central directory file headers given an EOCD. */ static async centralDirectoryFileFromFileHandle(zipFilePath, fileHandle, endOfCentralDirectoryRecord) { if (endOfCentralDirectoryRecord.diskNumberResolved() !== 0 || endOfCentralDirectoryRecord.centralDirectoryDiskStartResolved() !== 0) { throw new Error(`multi-disk zips aren't supported`); } const fileHeaders = []; const fixedLengthBuffer = Buffer.allocUnsafe(this.CENTRAL_DIRECTORY_FILE_HEADER_SIZE); let position = endOfCentralDirectoryRecord.centralDirectoryOffsetResolved(); for (let i = 0; i < endOfCentralDirectoryRecord.centralDirectoryTotalRecordsCountResolved(); i += 1) { // const fileRecord = await FileRecord.fileRecordFromFileHandle( // zipFilePath, // fileHandle, // position, // this.CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE, // this.CENTRAL_DIRECTORY_FILE_HEADER_SIZE, // this.FIELD_OFFSETS, // ); await fileHandle.read({ buffer: fixedLengthBuffer, position }); const signature = fixedLengthBuffer.subarray(0, this.CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE.length); if (!signature.equals(this.CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE)) { throw new Error(`invalid zip central directory file header signature: 0x${signature.toString('hex')}`); } const versionMadeBy = fixedLengthBuffer.readUInt16LE(4); const versionNeeded = fixedLengthBuffer.readUInt16LE(6); const generalPurposeBitFlag = fixedLengthBuffer.readUInt16LE(8); const compressionMethod = fixedLengthBuffer.readUInt16LE(10); const fileModificationTime = fixedLengthBuffer.readUInt16LE(12); const fileModificationDate = fixedLengthBuffer.readUInt16LE(14); const uncompressedCrc32 = Buffer.from(fixedLengthBuffer.subarray(16, 20)); const compressedSize = fixedLengthBuffer.readUInt32LE(20); const uncompressedSize = fixedLengthBuffer.readUInt32LE(24); const fileNameLength = fixedLengthBuffer.readUInt16LE(28); const extraFieldLength = fixedLengthBuffer.readUInt16LE(30); const fileCommentLength = fixedLengthBuffer.readUInt16LE(32); const fileDiskStart = fixedLengthBuffer.readUInt16LE(34); const internalFileAttributes = fixedLengthBuffer.readUInt16LE(36); const externalFileAttributes = fixedLengthBuffer.readUInt32LE(38); const localFileHeaderRelativeOffset = fixedLengthBuffer.readUInt32LE(42); const variableLengthBufferSize = fileNameLength + extraFieldLength + fileCommentLength; let variableLengthBuffer; if (variableLengthBufferSize > 0) { // Only read from the file if there's something to read variableLengthBuffer = Buffer.allocUnsafe(variableLengthBufferSize); await fileHandle.read({ buffer: variableLengthBuffer, position: position + fixedLengthBuffer.length, }); } else { variableLengthBuffer = Buffer.alloc(0); } const fileName = Buffer.from(variableLengthBuffer.subarray(0, fileNameLength)); const extraFields = FileRecordUtil.parseExtraFields(variableLengthBuffer.subarray(fileNameLength, fileNameLength + extraFieldLength)); const fileComment = Buffer.from(variableLengthBuffer.subarray(fileNameLength + extraFieldLength)); const zip64ExtendedInformation = FileRecordUtil.parseZip64ExtendedInformation(extraFields.get(0x00_01), uncompressedSize, compressedSize, localFileHeaderRelativeOffset, fileDiskStart); fileHeaders.push(new CentralDirectoryFileHeader(zipFilePath, { raw: Buffer.concat([fixedLengthBuffer, variableLengthBuffer]), versionMadeBy, versionNeeded, generalPurposeBitFlag, compressionMethod, fileModificationTime, fileModificationDate, uncompressedCrc32, compressedSize, uncompressedSize, fileNameLength, extraFieldLength, fileCommentLength, fileDiskStart, internalFileAttributes, externalFileAttributes, localFileHeaderRelativeOffset, fileName, extraFields, fileComment, zip64ExtendedInformation, })); position += fixedLengthBuffer.length + variableLengthBuffer.length; } return fileHeaders; } /** * Return the disk number that contains the start of the file, taking zip64 information into account. */ fileDiskStartResolved() { return this.zip64ExtendedInformation?.fileDiskStart ?? this.fileDiskStart; } /** * Return the relative offset to the start of the file, taking zip64 information into account. */ localFileHeaderRelativeOffsetResolved() { return (this.zip64ExtendedInformation?.localFileHeaderRelativeOffset ?? this.localFileHeaderRelativeOffset); } /** * Return the file's comment, taking extra fields and encodings into account. */ fileCommentResolved() { return (this.extraFields.get(0x63_75)?.subarray(5).toString('utf8') ?? (this.generalPurposeBitFlag & 0x8_00 ? this.fileComment.toString('utf8') : CP437Decoder.decode(this.fileComment))); } /** * Return the local file header associated with this central directory file header. */ async localFileHeader() { if (this._localFileHeader !== undefined) { return this._localFileHeader; } const fileHandle = await fs.promises.open(this.zipFilePath, 'r'); try { this._localFileHeader = await LocalFileHeader.localFileHeaderFromFileHandle(this, fileHandle); return this._localFileHeader; } finally { await fileHandle.close(); } } /** * Return this file's compressed/raw stream. */ async compressedStream(highWaterMark) { const localFileHeader = await this.localFileHeader(); return localFileHeader.compressedStream(highWaterMark); } /** * Return this file's uncompressed/decompressed stream. */ async uncompressedStream(highWaterMark) { const localFileHeader = await this.localFileHeader(); return localFileHeader.uncompressedStream(highWaterMark); } }