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
JavaScript
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