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.

176 lines (175 loc) • 7.17 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"; class CentralDirectoryFileHeader extends FileRecord { static CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE = Buffer.from( "02014b50", "hex" ).toReversed(); // 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) { 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) { variableLengthBuffer = Buffer.allocUnsafe(variableLengthBufferSize); await fileHandle.read({ buffer: variableLengthBuffer, position: position + fixedLengthBuffer.length }); } else { variableLengthBuffer = Buffer.allocUnsafe(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(1), 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(25461)?.subarray(5).toString("utf8") ?? (this.generalPurposeBitFlag & 2048 ? 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 !== void 0) { 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); } } export { CentralDirectoryFileHeader as default }; //# sourceMappingURL=centralDirectoryFileHeader.js.map