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.

175 lines (174 loc) • 7.11 kB
import fs from "node:fs"; import stream from "node:stream"; import zlib from "node:zlib"; import StreamUtil from "../../../src/utils/streamUtil.js"; import DataDescriptor from "./dataDescriptor.js"; import FileRecord, { CompressionMethod, CompressionMethodInverted } from "./fileRecord.js"; import FileRecordUtil from "./fileRecordUtil.js"; import ZipBombProtector from "./zipBombProtector.js"; class LocalFileHeader extends FileRecord { static LOCAL_FILE_HEADER_SIGNATURE = Buffer.from("04034b50", "hex").toReversed(); // Size with the signature, and without variable length fields at the end static LOCAL_FILE_HEADER_SIZE = 30; zipFilePath; centralDirectoryFileHeader; headerRelativeOffset; dataRelativeOffset; constructor(zipFilePath, centralDirectoryFileHeader, props) { super(props); this.zipFilePath = zipFilePath; this.centralDirectoryFileHeader = centralDirectoryFileHeader; this.headerRelativeOffset = props.headerRelativeOffset; this.dataRelativeOffset = props.dataRelativeOffset; } /** * Parse a local file header given a central directory file header. */ static async localFileHeaderFromFileHandle(centralDirectoryFileHeader, fileHandle) { const fixedLengthBuffer = Buffer.allocUnsafe(this.LOCAL_FILE_HEADER_SIZE); await fileHandle.read({ buffer: fixedLengthBuffer, position: centralDirectoryFileHeader.localFileHeaderRelativeOffsetResolved() }); const versionNeeded = fixedLengthBuffer.readUInt16LE(4); const generalPurposeBitFlag = fixedLengthBuffer.readUInt16LE(6); const compressionMethod = fixedLengthBuffer.readUInt16LE(8); const fileModificationTime = fixedLengthBuffer.readUInt16LE(10); const fileModificationDate = fixedLengthBuffer.readUInt16LE(12); const uncompressedCrc32 = Buffer.from(fixedLengthBuffer.subarray(14, 18)); const compressedSize = fixedLengthBuffer.readUInt32LE(18); const uncompressedSize = fixedLengthBuffer.readUInt32LE(22); const fileNameLength = fixedLengthBuffer.readUInt16LE(26); const extraFieldLength = fixedLengthBuffer.readUInt16LE(28); const variableLengthBufferSize = fileNameLength + extraFieldLength; let variableLengthBuffer; if (variableLengthBufferSize > 0) { variableLengthBuffer = Buffer.allocUnsafe(variableLengthBufferSize); await fileHandle.read({ buffer: variableLengthBuffer, position: centralDirectoryFileHeader.localFileHeaderRelativeOffsetResolved() + 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 zip64ExtendedInformation = FileRecordUtil.parseZip64ExtendedInformation( extraFields.get(1), uncompressedSize, compressedSize, centralDirectoryFileHeader.localFileHeaderRelativeOffsetResolved(), centralDirectoryFileHeader.fileDiskStartResolved() ); return new LocalFileHeader(centralDirectoryFileHeader.zipFilePath, centralDirectoryFileHeader, { raw: Buffer.concat([fixedLengthBuffer, variableLengthBuffer]), headerRelativeOffset: centralDirectoryFileHeader.localFileHeaderRelativeOffsetResolved(), dataRelativeOffset: centralDirectoryFileHeader.localFileHeaderRelativeOffsetResolved() + fixedLengthBuffer.length + variableLengthBuffer.length + (generalPurposeBitFlag & 1 ? 12 : 0), versionNeeded, generalPurposeBitFlag, compressionMethod, fileModificationTime, fileModificationDate, uncompressedCrc32, compressedSize, uncompressedSize, fileNameLength, extraFieldLength, fileName, extraFields, zip64ExtendedInformation }); } getLocalFileDataRelativeOffset() { return this.dataRelativeOffset; } /** * Return the data descriptor that follows this file's data when general purpose bit 3 is set, * or undefined when there is no data descriptor. */ async dataDescriptor() { if (!this.hasDataDescriptor()) { return void 0; } const position = this.getLocalFileDataRelativeOffset() + this.compressedSizeResolved(); const fileHandle = await fs.promises.open(this.zipFilePath, "r"); try { return await DataDescriptor.fromFileHandle(fileHandle, position, this.versionNeeded >= 45); } finally { await fileHandle.close(); } } /** * Return the numerical CRC32, taking the existence of a data descriptor into account. */ uncompressedCrc32Number() { return this.hasDataDescriptor() ? this.centralDirectoryFileHeader.uncompressedCrc32Number() : super.uncompressedCrc32Number(); } /** * Return the CRC32 string, taking the existence of a data descriptor into account. */ uncompressedCrc32String() { return this.hasDataDescriptor() ? this.centralDirectoryFileHeader.uncompressedCrc32String() : super.uncompressedCrc32String(); } /** * Return the compressed size, taking the existence of a data descriptor into account. */ compressedSizeResolved() { return this.hasDataDescriptor() ? this.centralDirectoryFileHeader.compressedSizeResolved() : super.compressedSizeResolved(); } /** * Return the uncompressed size, taking the existence of a data descriptor into account. */ uncompressedSizeResolved() { return this.hasDataDescriptor() ? this.centralDirectoryFileHeader.uncompressedSizeResolved() : super.uncompressedSizeResolved(); } /** * Return this file's compressed/raw stream. */ compressedStream(highWaterMark) { if (this.compressedSizeResolved() === 0) { return stream.Readable.from([]); } return fs.createReadStream(this.zipFilePath, { start: this.getLocalFileDataRelativeOffset(), end: this.getLocalFileDataRelativeOffset() + this.compressedSizeResolved() - 1, highWaterMark }); } /** * Return this file's uncompressed/decompressed stream. */ uncompressedStream(highWaterMark) { switch (this.compressionMethod) { case CompressionMethod.STORE: { return this.compressedStream(highWaterMark); } case CompressionMethod.DEFLATE: { return StreamUtil.withTransforms( this.compressedStream(highWaterMark), zlib.createInflateRaw({ chunkSize: highWaterMark }), new ZipBombProtector(this.uncompressedSizeResolved()) ); } case CompressionMethod.ZSTD_DEPRECATED: case CompressionMethod.ZSTD: { return StreamUtil.withTransforms( this.compressedStream(highWaterMark), zlib.createZstdDecompress({ chunkSize: highWaterMark }), new ZipBombProtector(this.uncompressedSizeResolved()) ); } default: { throw new Error( `unsupported compression method: ${CompressionMethodInverted[this.compressionMethod]}` ); } } } } export { LocalFileHeader as default }; //# sourceMappingURL=localFileHeader.js.map