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.
326 lines (325 loc) • 13.6 kB
JavaScript
import fs from "node:fs";
import os from "node:os";
import stream from "node:stream";
import zlib from "node:zlib";
import CompressedTransform from "./compressedTransform.js";
import CP437Encoder from "./cp437Encoder.js";
import ProgressTransform from "./progressTransform.js";
import UncompressedTransform from "./uncompressedTransform.js";
import ZlibDeflateTransform from "./zlibDeflateTransform.js";
import ZstdThreadedCompressTransform from "./zstdThreadedCompressTransform.js";
const CompressionMethod = {
DEFLATE: "DEFLATE",
ZSTD: "ZSTD"
};
class TZWriter {
static LOCAL_FILE_HEADER_MIN_LENGTH = 30;
static LOCAL_FILE_HEADER_SIGNATURE = Buffer.from("504B0304", "hex");
static CENTRAL_DIRECTORY_FILE_HEADER_MIN_LENGTH = 46;
static CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE = Buffer.from("504B0102", "hex");
static ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_LENGTH = 56;
static ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE = Buffer.from(
"504B0606",
"hex"
);
static ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LENGTH = 20;
static ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIGNATURE = Buffer.from(
"504B0607",
"hex"
);
static END_OF_CENTRAL_DIRECTORY_HEADER_MIN_LENGTH = 22;
static END_OF_CENTRAL_DIRECTORY_HEADER_SIGNATURE = Buffer.from(
"504B0506",
"hex"
);
fileHandle;
compressionMethod;
filePosition = 0;
localFileHeaders = [];
constructor(fileHandle, compressionMethod) {
this.fileHandle = fileHandle;
this.compressionMethod = compressionMethod;
}
/**
* Open a file path for writing.
*/
static async open(filePath, compressionMethod) {
const fileHandle = await fs.promises.open(filePath, "w");
return new TZWriter(fileHandle, compressionMethod);
}
/**
* Add a stream to the TorrentZip.
*/
async addStream(readable, filename, uncompressedSize, compressorThreads, progressCallback) {
let localFileHeaderPlaceholder;
if (uncompressedSize >= 4294967295) {
localFileHeaderPlaceholder = this.zip64LocalFileHeader(filename, this.filePosition);
} else {
localFileHeaderPlaceholder = this.localFileHeader(filename);
}
await this.fileHandle.write(
localFileHeaderPlaceholder,
void 0,
void 0,
this.filePosition
);
const uncompressedTransform = new UncompressedTransform();
let compressorTransform;
if (this.compressionMethod === CompressionMethod.DEFLATE) {
compressorTransform = new ZlibDeflateTransform();
} else if (uncompressedSize === 0) {
compressorTransform = new stream.PassThrough();
} else {
compressorTransform = new ZstdThreadedCompressTransform(compressorThreads);
}
const compressedTransform = new CompressedTransform();
await stream.promises.pipeline(
readable,
uncompressedTransform,
new ProgressTransform(progressCallback),
compressorTransform,
compressedTransform,
fs.createWriteStream(os.devNull, {
fd: this.fileHandle.fd,
autoClose: false,
start: this.filePosition + localFileHeaderPlaceholder.length
})
);
let localFileHeader;
if (uncompressedSize >= 4294967295) {
localFileHeader = this.zip64LocalFileHeader(
filename,
this.filePosition,
uncompressedTransform.getCrc32(),
compressedTransform.getSize(),
uncompressedTransform.getSize()
);
} else {
localFileHeader = this.localFileHeader(
filename,
uncompressedTransform.getCrc32(),
compressedTransform.getSize(),
uncompressedTransform.getSize()
);
}
await this.fileHandle.write(localFileHeader, void 0, void 0, this.filePosition);
this.localFileHeaders.push({
position: this.filePosition,
uncompressedCrc32: uncompressedTransform.getCrc32(),
compressedSize: compressedTransform.getSize(),
uncompressedSize: uncompressedTransform.getSize(),
rawBytes: localFileHeader
});
this.filePosition += localFileHeader.length + compressedTransform.getSize();
}
localFileHeader(filename, uncompressedCrc32, compressedSize, uncompressedSize) {
const isCp437 = CP437Encoder.canEncode(filename);
const encodedFilename = isCp437 ? CP437Encoder.encode(filename) : Buffer.from(filename, "utf8");
const buffer = Buffer.allocUnsafe(
TZWriter.LOCAL_FILE_HEADER_MIN_LENGTH + encodedFilename.length
);
TZWriter.LOCAL_FILE_HEADER_SIGNATURE.copy(buffer);
if (this.compressionMethod === CompressionMethod.DEFLATE || uncompressedSize === 0) {
buffer.writeUInt16LE(20, 4);
} else {
buffer.writeUInt16LE(63, 4);
}
buffer.writeUInt16LE(2 | (isCp437 ? 0 : 2048), 6);
if (this.compressionMethod === CompressionMethod.DEFLATE) {
buffer.writeUInt16LE(8, 8);
} else if (uncompressedSize === 0) {
buffer.writeUInt16LE(0, 8);
} else {
buffer.writeUInt16LE(93, 8);
}
if (this.compressionMethod === CompressionMethod.DEFLATE) {
buffer.writeUInt16LE(48128, 10);
buffer.writeUInt16LE(8600, 12);
} else {
buffer.writeUInt16LE(0, 10);
buffer.writeUInt16LE(0, 12);
}
buffer.writeUInt32LE(uncompressedCrc32 ?? 0, 14);
if ((compressedSize ?? 0) >= 4294967295 || (uncompressedSize ?? 0) >= 4294967295) {
buffer.writeUInt32LE(4294967295, 18);
buffer.writeUInt32LE(4294967295, 22);
} else {
buffer.writeUInt32LE(compressedSize ?? 0, 18);
buffer.writeUInt32LE(uncompressedSize ?? 0, 22);
}
buffer.writeUint16LE(encodedFilename.length, 26);
buffer.writeUint16LE(0, 28);
encodedFilename.copy(buffer, TZWriter.LOCAL_FILE_HEADER_MIN_LENGTH);
return buffer;
}
zip64LocalFileHeader(filename, localFileHeaderRelativeOffset, uncompressedCrc32, compressedSize, uncompressedSize) {
const localFileHeader = this.localFileHeader(
filename,
uncompressedCrc32,
compressedSize,
uncompressedSize
);
const extraFieldLength = 20 + (localFileHeaderRelativeOffset >= 4294967295 ? 8 : 0);
const buffer = Buffer.allocUnsafe(localFileHeader.length + extraFieldLength);
localFileHeader.copy(buffer, 0);
if (buffer.readUInt16LE(4) < 45) {
buffer.writeUInt16LE(45, 4);
}
buffer.writeUint16LE(extraFieldLength, 28);
buffer.writeUInt16LE(1, localFileHeader.length);
buffer.writeUInt16LE(extraFieldLength - 4, localFileHeader.length + 2);
buffer.writeBigUInt64LE(BigInt(uncompressedSize ?? 0), localFileHeader.length + 4);
buffer.writeBigUInt64LE(BigInt(compressedSize ?? 0), localFileHeader.length + 12);
if (localFileHeaderRelativeOffset >= 4294967295) {
buffer.writeBigUInt64LE(BigInt(localFileHeaderRelativeOffset), localFileHeader.length + 20);
}
return buffer;
}
/**
* Write the central directory and close the file.
*/
async finalize() {
try {
const centralDirectoryFileHeaders = this.localFileHeaders.map(
(lfh) => TZWriter.centralDirectoryFileHeader(lfh)
);
const startOfCentralDirectoryOffset = this.filePosition;
const centralDirectoryFileHeadersConcat = Buffer.concat(centralDirectoryFileHeaders);
await this.fileHandle.write(
centralDirectoryFileHeadersConcat,
void 0,
void 0,
this.filePosition
);
this.filePosition += centralDirectoryFileHeadersConcat.length;
const isZip64 = centralDirectoryFileHeadersConcat.length >= 4294967295 || startOfCentralDirectoryOffset >= 4294967295 || this.localFileHeaders.length >= 65535;
if (isZip64) {
const zip64EndOfCentralDirectoryOffset = this.filePosition;
const zip64EndOfCentralDirectoryRecord = TZWriter.zip64EndOfCentralDirectoryRecord(
centralDirectoryFileHeadersConcat,
this.localFileHeaders.length,
startOfCentralDirectoryOffset
);
const zip64EndOfCentralDirectoryLocator = TZWriter.zip64EndOfCentralDirectoryLocator(
zip64EndOfCentralDirectoryOffset
);
await this.fileHandle.write(
Buffer.concat([zip64EndOfCentralDirectoryRecord, zip64EndOfCentralDirectoryLocator]),
void 0,
void 0,
this.filePosition
);
this.filePosition += zip64EndOfCentralDirectoryRecord.length + zip64EndOfCentralDirectoryLocator.length;
}
const eocd = this.endOfCentralDirectoryHeader(
centralDirectoryFileHeadersConcat,
startOfCentralDirectoryOffset
);
await this.fileHandle.write(eocd, void 0, void 0, this.filePosition);
this.filePosition += eocd.length;
} finally {
await this.close();
}
}
/**
* Close the file handle.
*/
async close() {
await this.fileHandle.close();
}
static centralDirectoryFileHeader(localFileHeader) {
const fileNameLength = localFileHeader.rawBytes.readUInt16LE(26);
const extraFieldLength = localFileHeader.uncompressedSize >= 4294967295 || localFileHeader.compressedSize >= 4294967295 || localFileHeader.position >= 4294967295 ? 4 + (localFileHeader.uncompressedSize >= 4294967295 ? 8 : 0) + (localFileHeader.compressedSize >= 4294967295 ? 8 : 0) + (localFileHeader.position >= 4294967295 ? 8 : 0) : 0;
const buffer = Buffer.allocUnsafe(
this.CENTRAL_DIRECTORY_FILE_HEADER_MIN_LENGTH + fileNameLength + extraFieldLength
);
this.CENTRAL_DIRECTORY_FILE_HEADER_SIGNATURE.copy(buffer);
buffer.writeUInt16LE(0, 4);
localFileHeader.rawBytes.copy(buffer, 6, 4, 4 + 2);
if (extraFieldLength > 0 && buffer.readUInt16LE(6) < 45) {
buffer.writeUInt16LE(45, 6);
}
localFileHeader.rawBytes.copy(buffer, 8, 6, 6 + 2);
localFileHeader.rawBytes.copy(buffer, 10, 8, 8 + 2);
localFileHeader.rawBytes.copy(buffer, 12, 10, 10 + 2);
localFileHeader.rawBytes.copy(buffer, 14, 12, 12 + 2);
buffer.writeUInt32LE(localFileHeader.uncompressedCrc32, 16);
buffer.writeUInt32LE(Math.min(localFileHeader.compressedSize, 4294967295), 20);
buffer.writeUInt32LE(Math.min(localFileHeader.uncompressedSize, 4294967295), 24);
localFileHeader.rawBytes.copy(buffer, 28, 26, 26 + 2);
buffer.writeUInt16LE(extraFieldLength, 30);
buffer.writeUInt16LE(0, 32);
buffer.writeUInt16LE(0, 34);
buffer.writeUInt16LE(0, 36);
buffer.writeUInt32LE(0, 38);
buffer.writeUInt32LE(Math.min(localFileHeader.position, 4294967295), 42);
localFileHeader.rawBytes.copy(buffer, 46, 30, 30 + fileNameLength);
if (extraFieldLength > 0) {
buffer.writeUInt16LE(1, 46 + fileNameLength);
buffer.writeUInt16LE(extraFieldLength - 4, 46 + fileNameLength + 2);
let extraFieldPosition = 46 + fileNameLength + 4;
if (localFileHeader.uncompressedSize >= 4294967295) {
buffer.writeBigUInt64LE(BigInt(localFileHeader.uncompressedSize), extraFieldPosition);
extraFieldPosition += 8;
}
if (localFileHeader.compressedSize >= 4294967295) {
buffer.writeBigUInt64LE(BigInt(localFileHeader.compressedSize), extraFieldPosition);
extraFieldPosition += 8;
}
if (localFileHeader.position >= 4294967295) {
buffer.writeBigUInt64LE(BigInt(localFileHeader.position), extraFieldPosition);
}
}
return buffer;
}
static zip64EndOfCentralDirectoryRecord(centralDirectoryConcat, centralDirectoryCount, startOfCentralDirectoryOffset) {
const buffer = Buffer.allocUnsafe(this.ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_LENGTH);
this.ZIP64_END_OF_CENTRAL_DIRECTORY_RECORD_SIGNATURE.copy(buffer);
buffer.writeBigUInt64LE(BigInt(buffer.length - 12), 4);
buffer.writeUInt16LE(45, 12);
buffer.writeUInt16LE(45, 14);
buffer.writeUInt32LE(0, 16);
buffer.writeUInt32LE(0, 20);
buffer.writeBigUInt64LE(BigInt(centralDirectoryCount), 24);
buffer.writeBigUInt64LE(BigInt(centralDirectoryCount), 32);
buffer.writeBigUInt64LE(BigInt(centralDirectoryConcat.length), 40);
buffer.writeBigUInt64LE(BigInt(startOfCentralDirectoryOffset), 48);
return buffer;
}
static zip64EndOfCentralDirectoryLocator(zip64EndOfCentralDirectoryOffset) {
const buffer = Buffer.allocUnsafe(this.ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_LENGTH);
this.ZIP64_END_OF_CENTRAL_DIRECTORY_LOCATOR_SIGNATURE.copy(buffer);
buffer.writeUInt32LE(0, 4);
buffer.writeBigUInt64LE(BigInt(zip64EndOfCentralDirectoryOffset), 8);
buffer.writeUInt32LE(1, 16);
return buffer;
}
endOfCentralDirectoryHeader(centralDirectoryConcat, startOfCentralDirectoryOffset) {
const commentLength = this.compressionMethod === CompressionMethod.DEFLATE ? 22 : 15;
const buffer = Buffer.allocUnsafe(
TZWriter.END_OF_CENTRAL_DIRECTORY_HEADER_MIN_LENGTH + commentLength
);
TZWriter.END_OF_CENTRAL_DIRECTORY_HEADER_SIGNATURE.copy(buffer);
buffer.writeUInt16LE(0, 4);
buffer.writeUInt16LE(0, 6);
buffer.writeUInt16LE(Math.min(this.localFileHeaders.length, 65535), 8);
buffer.writeUInt16LE(Math.min(this.localFileHeaders.length, 65535), 10);
buffer.writeUInt32LE(Math.min(centralDirectoryConcat.length, 4294967295), 12);
buffer.writeUInt32LE(Math.min(startOfCentralDirectoryOffset, 4294967295), 16);
const cdfhCrc32 = (zlib.crc32(centralDirectoryConcat) >>> 0).toString(16).padStart(8, "0").toUpperCase();
let comment;
if (this.compressionMethod === CompressionMethod.DEFLATE) {
comment = `TORRENTZIPPED-${cdfhCrc32}`;
} else {
comment = `RVZSTD-${cdfhCrc32}`;
}
buffer.writeUInt32LE(comment.length, 20);
buffer.write(comment, 22);
return buffer;
}
}
export {
CompressionMethod,
TZWriter as default
};
//# sourceMappingURL=tzWriter.js.map