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
JavaScript
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);
}
}