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.
203 lines (202 loc) • 10.3 kB
JavaScript
import CP437Decoder from './cp437Decoder.js';
/**
* The end of central directory in a zip file.
* @see https://en.wikipedia.org/wiki/ZIP_(file_format)#End_of_central_directory_record_(EOCD)
*/
export default class EndOfCentralDirectory {
static END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE = Buffer.from('06054b50', 'hex').reverse();
static ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIGNATURE = Buffer.from('07064b50', 'hex').reverse();
static ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE = Buffer.from('06064b50', 'hex').reverse();
// Size with the signature, and without variable length fields at the end
static END_OF_CENTRAL_DIRECTORY_RECORD_SIZE = 22;
static ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIZE = 20;
static ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIZE = 56;
// Maximum size of the non-zip64 EOCD (~64KiB)
static END_OF_CENTRAL_DIRECTORY_MAX_SIZE = 22 + 0xff_ff;
diskNumber;
centralDirectoryDiskStart;
centralDirectoryDiskRecordsCount;
centralDirectoryTotalRecordsCount;
centralDirectorySizeBytes;
centralDirectoryOffset;
comment;
zip64Locator;
zip64Record;
constructor(props) {
this.diskNumber = props.diskNumber;
this.centralDirectoryDiskStart = props.centralDirectoryDiskStart;
this.centralDirectoryDiskRecordsCount = props.centralDirectoryDiskRecordsCount;
this.centralDirectoryTotalRecordsCount = props.centralDirectoryTotalRecordsCount;
this.centralDirectorySizeBytes = props.centralDirectorySizeBytes;
this.centralDirectoryOffset = props.centralDirectoryOffset;
this.comment = props.comment;
this.zip64Locator = props.zip64Locator;
this.zip64Record = props.zip64Record;
}
/**
* Parse the end of central directory in a file.
*/
static async fromFileHandle(fileHandle) {
const fileSize = (await fileHandle.stat()).size;
const filePosition = Math.max(fileSize - 1 - this.END_OF_CENTRAL_DIRECTORY_MAX_SIZE, 0);
const buffer = Buffer.allocUnsafe(Math.min(this.END_OF_CENTRAL_DIRECTORY_MAX_SIZE, fileSize));
// Find the start position of the EOCD
const readResult = await fileHandle.read({ buffer, position: filePosition });
const eocdPosition = buffer
.subarray(0, readResult.bytesRead)
.lastIndexOf(this.END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE);
if (eocdPosition === -1) {
throw new Error('could not find end of central directory record');
}
// Parse the EOCD
return this.readEndOfCentralDirectoryRecordZip(fileHandle, filePosition + eocdPosition);
}
static async readEndOfCentralDirectoryRecordZip(fileHandle, eocdPosition) {
const buffer = Buffer.allocUnsafe(this.END_OF_CENTRAL_DIRECTORY_RECORD_SIZE);
// Read the EOCD record except for the variable-length comment
await fileHandle.read({ buffer, position: eocdPosition });
if (!buffer.subarray(0, 4).equals(this.END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE)) {
throw new Error('bad end of central directory record position');
}
const diskNumber = buffer.readUInt16LE(4);
const centralDirectoryDiskStart = buffer.readUInt16LE(6);
const centralDirectoryDiskRecordsCount = buffer.readUInt16LE(8);
const centralDirectoryTotalRecordsCount = buffer.readUInt16LE(10);
const centralDirectorySizeBytes = buffer.readUInt32LE(12);
const centralDirectoryOffset = buffer.readUInt32LE(16);
const commentLength = buffer.readUInt16LE(20);
// Read the EOCD comment
let commentBuffer;
if (commentLength === 0) {
// No need to read the comment
commentBuffer = Buffer.alloc(0);
}
else if (commentLength < buffer.length) {
// The comment is small, keep re-using the same buffer
const readResult = await fileHandle.read({
buffer,
position: eocdPosition + this.END_OF_CENTRAL_DIRECTORY_RECORD_SIZE,
length: commentLength,
});
commentBuffer = readResult.buffer.subarray(0, readResult.bytesRead);
}
else {
// The comment is long, allocate a new buffer
const readResult = await fileHandle.read({
buffer: Buffer.alloc(commentLength),
position: eocdPosition + this.END_OF_CENTRAL_DIRECTORY_RECORD_SIZE,
});
commentBuffer = readResult.buffer;
}
const comment = CP437Decoder.decode(commentBuffer);
// Parse the optional zip64 EOCD
let zip64EndOfCentralDirectoryLocator;
let zip64EndOfCentralDirectoryRecord;
if (centralDirectoryDiskStart === 0xff_ff ||
centralDirectoryDiskRecordsCount === 0xff_ff ||
centralDirectoryTotalRecordsCount === 0xff_ff ||
centralDirectorySizeBytes === 0xff_ff_ff_ff ||
centralDirectoryOffset === 0xff_ff_ff_ff) {
zip64EndOfCentralDirectoryLocator = await this.readZip64EndOfCentralDirectoryLocator(fileHandle, eocdPosition - this.ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIZE);
if (zip64EndOfCentralDirectoryLocator.centralDirectoryDiskStart !== 0 ||
zip64EndOfCentralDirectoryLocator.diskCount !== 1) {
throw new Error("multi-disk zip64 zips aren't supported");
}
zip64EndOfCentralDirectoryRecord = await this.readZip64EndOfCentralDirectoryRecord(fileHandle, zip64EndOfCentralDirectoryLocator.centralDirectoryOffset);
}
return new EndOfCentralDirectory({
diskNumber,
centralDirectoryDiskStart,
centralDirectoryDiskRecordsCount,
centralDirectoryTotalRecordsCount,
centralDirectorySizeBytes,
centralDirectoryOffset,
comment,
zip64Locator: zip64EndOfCentralDirectoryLocator,
zip64Record: zip64EndOfCentralDirectoryRecord,
});
}
static async readZip64EndOfCentralDirectoryLocator(fileHandle, locatorPosition) {
const buffer = Buffer.allocUnsafe(this.ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIZE);
await fileHandle.read({ buffer, position: locatorPosition });
if (!buffer.subarray(0, 4).equals(this.ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIGNATURE)) {
throw new Error('bad zip64 end of central directory locator position');
}
return {
centralDirectoryDiskStart: buffer.readUInt32LE(4),
centralDirectoryOffset: Number(buffer.readBigUInt64LE(8)),
diskCount: buffer.readUInt32LE(16),
};
}
static async readZip64EndOfCentralDirectoryRecord(fileHandle, eocdPosition) {
const fixedLengthBuffer = Buffer.allocUnsafe(this.ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIZE);
await fileHandle.read({ buffer: fixedLengthBuffer, position: eocdPosition });
if (!fixedLengthBuffer.subarray(0, 4).equals(this.ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE)) {
throw new Error(`bad zip64 end of central directory record position`);
}
const eocdSize = fixedLengthBuffer.readBigUInt64LE(4) + 12n;
const commentBuffer = Buffer.allocUnsafe(Number(eocdSize - 56n));
if (commentBuffer.length > 0) {
await fileHandle.read({ buffer: commentBuffer, position: eocdPosition + 56 });
}
return {
versionMadeBy: fixedLengthBuffer.readUInt16LE(12),
versionNeeded: fixedLengthBuffer.readUInt16LE(14),
diskNumber: fixedLengthBuffer.readUInt32LE(16),
centralDirectoryDiskStart: fixedLengthBuffer.readUInt32LE(20),
centralDirectoryDiskRecordsCount: Number(fixedLengthBuffer.readBigUInt64LE(24)),
centralDirectoryTotalRecordsCount: Number(fixedLengthBuffer.readBigUInt64LE(32)),
centralDirectorySizeBytes: Number(fixedLengthBuffer.readBigUInt64LE(40)),
centralDirectoryOffset: Number(fixedLengthBuffer.readBigUInt64LE(48)),
comment: CP437Decoder.decode(commentBuffer),
};
}
/**
* Return this EOCD's disk number, taking zip64 information into account.
*/
diskNumberResolved() {
return this.diskNumber === 0xff_ff && this.zip64Record !== undefined
? this.zip64Record.diskNumber
: this.diskNumber;
}
/**
* Return the disk that contains the SOCD, taking zip64 information into account.
*/
centralDirectoryDiskStartResolved() {
return this.centralDirectoryDiskStart === 0xff_ff && this.zip64Record !== undefined
? this.zip64Record.centralDirectoryDiskStart
: this.centralDirectoryDiskStart;
}
/**
* Return the number of central directory records on this disk, taking zip64 information into account.
*/
centralDirectoryDiskRecordsCountResolved() {
return this.centralDirectoryDiskRecordsCount === 0xff_ff && this.zip64Record !== undefined
? this.zip64Record.centralDirectoryDiskRecordsCount
: this.centralDirectoryDiskRecordsCount;
}
/**
* Return the total number of central directory records, taking zip64 information into account.
*/
centralDirectoryTotalRecordsCountResolved() {
return this.centralDirectoryTotalRecordsCount === 0xff_ff && this.zip64Record !== undefined
? this.zip64Record.centralDirectoryTotalRecordsCount
: this.centralDirectoryTotalRecordsCount;
}
/**
* Return the total size of the central directory, taking zip64 information into account.
*/
centralDirectorySizeBytesResolved() {
return this.centralDirectorySizeBytes === 0xff_ff_ff_ff && this.zip64Record !== undefined
? this.zip64Record.centralDirectorySizeBytes
: this.centralDirectorySizeBytes;
}
/**
* Return the relative offset to the SOCD, taking zip64 information into account.
*/
centralDirectoryOffsetResolved() {
return this.centralDirectoryOffset === 0xff_ff_ff_ff && this.zip64Record !== undefined
? this.zip64Record.centralDirectoryOffset
: this.centralDirectoryOffset;
}
}