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.
159 lines (158 loc) • 7.34 kB
JavaScript
import fs from 'node:fs';
import stream from 'node:stream';
import zlib from 'node:zlib';
import zstd from 'zstd-napi';
import StreamPoly from '../../../src/polyfill/streamPoly.js';
import FileRecord, { CompressionMethod, CompressionMethodInverted } from './fileRecord.js';
import FileRecordUtil from './fileRecordUtil.js';
import ZipBombProtector from './zipBombProtector.js';
/**
* A local file header in a zip file.
* @see https://en.wikipedia.org/wiki/ZIP_(file_format)#Local_file_header
*/
export default class LocalFileHeader extends FileRecord {
static LOCAL_FILE_HEADER_SIGNATURE = Buffer.from('04034b50', 'hex').reverse();
static DATA_DESCRIPTOR_SIGNATURE = Buffer.from('08074b50', 'hex').reverse();
// 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) {
// Only read from the file if there's something to read
variableLengthBuffer = Buffer.allocUnsafe(variableLengthBufferSize);
await fileHandle.read({
buffer: variableLengthBuffer,
position: centralDirectoryFileHeader.localFileHeaderRelativeOffsetResolved() +
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 zip64ExtendedInformation = FileRecordUtil.parseZip64ExtendedInformation(extraFields.get(0x00_01), 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 & 0x01 ? 12 : 0),
versionNeeded,
generalPurposeBitFlag,
compressionMethod,
fileModificationTime,
fileModificationDate,
uncompressedCrc32,
compressedSize,
uncompressedSize,
fileNameLength,
extraFieldLength,
fileName,
extraFields,
zip64ExtendedInformation,
});
}
getLocalFileDataRelativeOffset() {
return this.dataRelativeOffset;
}
/**
* 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) {
// There's no need to open the file, it will be an empty stream
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 StreamPoly.withTransforms(this.compressedStream(highWaterMark), zlib.createInflateRaw(), new ZipBombProtector(this.uncompressedSizeResolved()));
}
case CompressionMethod.ZSTD_DEPRECATED:
case CompressionMethod.ZSTD: {
return StreamPoly.withTransforms(this.compressedStream(),
// TODO(cemmer): replace with zlib in Node.js 24
new zstd.DecompressStream(), new ZipBombProtector(this.uncompressedSizeResolved()));
}
default: {
throw new Error(`unsupported compression method: ${CompressionMethodInverted[this.compressionMethod]}`);
}
}
}
}