UNPKG

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