minecraft-core-master
Version:
Núcleo avanzado para launchers de Minecraft. Descarga, instala y ejecuta versiones de Minecraft, assets, librerías, Java y loaders de forma automática y eficiente.
212 lines (211 loc) • 7.83 kB
JavaScript
import fs from 'fs';
import path from 'path';
import zlib from 'zlib';
import { EventEmitter } from 'events';
export class Unzipper extends EventEmitter {
entries = [];
constructor() {
super();
}
isValidZip(buffer) {
if (buffer.length < 4)
return false;
// Verificar firma ZIP al inicio
if (buffer.readUInt32LE(0) === 0x04034b50)
return true;
// Buscar firma en los primeros bytes
for (let i = 0; i < Math.min(buffer.length - 4, 1024); i++) {
if (buffer.readUInt32LE(i) === 0x04034b50)
return true;
}
return false;
}
safeInflate(data) {
try {
return zlib.inflateRawSync(data);
}
catch {
try {
return zlib.inflateSync(data);
}
catch (error) {
throw new Error(`Failed to decompress: ${error}`);
}
}
}
async extract(options) {
const { src, dest, validExts = [], flattenNatives = true, cleanAfter = true, ignoreFolders = ['META-INF'] } = options;
// Verificaciones iniciales
if (!fs.existsSync(src)) {
throw new Error(`File not found: ${src}`);
}
const stats = fs.statSync(src);
if (stats.size === 0) {
throw new Error(`Empty file: ${src}`);
}
const buffer = fs.readFileSync(src);
if (!this.isValidZip(buffer)) {
throw new Error(`Invalid ZIP file: ${src}`);
}
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
let offset = 0;
let processed = 0;
const maxSize = 100 * 1024 * 1024; // 100MB límite
try {
while (offset + 30 <= buffer.length && offset < maxSize) {
// Buscar signature
while (offset + 4 <= buffer.length && buffer.readUInt32LE(offset) !== 0x04034b50) {
offset++;
if (offset >= maxSize)
break;
}
if (offset + 30 > buffer.length)
break;
// Leer header
const compression = buffer.readUInt16LE(offset + 8);
const compressedSize = buffer.readUInt32LE(offset + 18);
const fileNameLen = buffer.readUInt16LE(offset + 26);
const extraLen = buffer.readUInt16LE(offset + 28);
// Verificaciones de seguridad
if (fileNameLen > 1000 || extraLen > 10000 || compressedSize > maxSize) {
offset += 30 + fileNameLen + extraLen + compressedSize;
continue;
}
const dataStart = offset + 30 + fileNameLen + extraLen;
const dataEnd = dataStart + compressedSize;
if (dataEnd > buffer.length) {
this.emit('warning', `Truncated entry in: ${path.basename(src)}`);
break;
}
const fileName = buffer.toString('utf8', offset + 30, offset + 30 + fileNameLen);
offset = dataEnd;
// Filtrar entradas no deseadas
if (this.shouldSkipEntry(fileName, ignoreFolders)) {
continue;
}
// Procesar entrada
try {
await this.processEntry({
fileName,
compression,
data: buffer.slice(dataStart, dataEnd),
dest,
validExts,
flattenNatives
});
processed++;
}
catch (error) {
this.emit('warning', `Skipping ${fileName}: ${error}`);
}
}
if (processed === 0) {
this.emit('warning', `No files extracted from: ${path.basename(src)}`);
}
else {
this.emit('status', `Extracted ${processed} files from ${path.basename(src)}`);
}
}
catch (error) {
this.emit('error', error);
throw error;
}
if (cleanAfter) {
this.cleanup(dest, validExts, ignoreFolders);
}
this.emit('done', this.entries);
}
shouldSkipEntry(fileName, ignoreFolders) {
if (!fileName)
return true;
if (fileName.includes('..'))
return true; // Path traversal
if (fileName.match(/licen[cs]e/i))
return true; // Licencias
return ignoreFolders.some(folder => fileName.startsWith(folder + '/'));
}
async processEntry(entry) {
const { fileName, compression, data, dest, validExts, flattenNatives } = entry;
if (fileName.endsWith('/')) {
// Directorio
const dirPath = path.join(dest, fileName);
if (dirPath.startsWith(dest)) {
fs.mkdirSync(dirPath, { recursive: true });
this.entries.push({ name: fileName, isDirectory: true, size: 0 });
}
return;
}
const ext = path.extname(fileName).toLowerCase();
if (validExts.length > 0 && !validExts.includes(ext)) {
return;
}
// Descomprimir datos
let fileData;
if (compression === 0) {
fileData = data; // Sin compresión
}
else if (compression === 8) {
fileData = this.safeInflate(data); // DEFLATE
}
else {
throw new Error(`Unsupported compression method: ${compression}`);
}
// Determinar path de salida
let outputPath = path.join(dest, fileName);
if (flattenNatives && ['.dll', '.so', '.dylib', '.jnilib'].includes(ext)) {
outputPath = path.join(dest, path.basename(fileName));
}
// Verificar seguridad del path
if (!outputPath.startsWith(dest)) {
throw new Error('Invalid output path');
}
// Escribir archivo
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, fileData);
this.entries.push({
name: fileName,
isDirectory: false,
size: fileData.length
});
this.emit('file', {
name: fileName,
size: fileData.length,
path: outputPath
});
}
cleanup(baseDir, validExts, ignoreFolders) {
if (!fs.existsSync(baseDir))
return;
const cleanDirectory = (dir) => {
const items = fs.readdirSync(dir);
for (const item of items) {
const fullPath = path.join(dir, item);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
if (ignoreFolders.includes(item)) {
fs.rmSync(fullPath, { recursive: true, force: true });
}
else {
cleanDirectory(fullPath);
// Eliminar directorio vacío
if (fs.existsSync(fullPath) && fs.readdirSync(fullPath).length === 0) {
fs.rmSync(fullPath, { recursive: true });
}
}
}
else if (validExts.length > 0) {
const ext = path.extname(item).toLowerCase();
if (!validExts.includes(ext)) {
fs.unlinkSync(fullPath);
}
}
}
};
cleanDirectory(baseDir);
}
getEntries() {
return this.entries;
}
}