mirror of
https://github.com/EmulatorJS/EmulatorJS.git
synced 2026-02-06 11:17:36 +00:00
214 lines
9.5 KiB
JavaScript
214 lines
9.5 KiB
JavaScript
/**
|
|
* Handles compression and decompression of various archive formats (ZIP, 7Z, RAR)
|
|
* for the EmulatorJS system.
|
|
*
|
|
* This class provides functionality to detect compressed file formats and extract
|
|
* their contents using web workers for better performance.
|
|
*/
|
|
class EJSCompression {
|
|
/**
|
|
* Creates a new compression handler instance.
|
|
*
|
|
* @param {Object} EJS - The main EmulatorJS instance
|
|
*/
|
|
constructor(EJS) {
|
|
this.EJS = EJS;
|
|
}
|
|
|
|
/**
|
|
* Detects if the given data represents a compressed archive format.
|
|
*
|
|
* @param {Uint8Array|ArrayBuffer} data - The binary data to analyze
|
|
* @returns {string|null} The detected compression format ('zip', '7z', 'rar') or null if not compressed
|
|
*
|
|
* @description
|
|
* Checks the file signature (magic bytes) at the beginning of the data to identify
|
|
* the compression format. Supports ZIP, 7Z, and RAR formats.
|
|
*
|
|
* @see {@link https://www.garykessler.net/library/file_sigs.html|File Signature Database}
|
|
*/
|
|
isCompressed(data) {
|
|
if ((data[0] === 0x50 && data[1] === 0x4B) && ((data[2] === 0x03 && data[3] === 0x04) || (data[2] === 0x05 && data[3] === 0x06) || (data[2] === 0x07 && data[3] === 0x08))) {
|
|
return "zip";
|
|
} else if (data[0] === 0x37 && data[1] === 0x7A && data[2] === 0xBC && data[3] === 0xAF && data[4] === 0x27 && data[5] === 0x1C) {
|
|
return "7z";
|
|
} else if ((data[0] === 0x52 && data[1] === 0x61 && data[2] === 0x72 && data[3] === 0x21 && data[4] === 0x1A && data[5] === 0x07) && ((data[6] === 0x00) || (data[6] === 0x01 && data[7] === 0x00))) {
|
|
return "rar";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Decompresses the given data and extracts all files.
|
|
*
|
|
* @param {Uint8Array|ArrayBuffer} data - The compressed data to extract
|
|
* @param {Function} updateMsg - Callback function for progress updates (message, isProgress)
|
|
* @param {Function} fileCbFunc - Callback function called for each extracted file (filename, fileData)
|
|
* @returns {Promise<Object>} Promise that resolves to an object mapping filenames to file data
|
|
*
|
|
* @description
|
|
* Automatically detects the compression format and delegates to the appropriate
|
|
* decompression method. If the data is not compressed, returns it as-is.
|
|
*/
|
|
decompress(data, updateMsg, fileCbFunc) {
|
|
const compressed = this.isCompressed(data.slice(0, 10));
|
|
if (compressed === null) {
|
|
if (typeof fileCbFunc === "function") {
|
|
fileCbFunc("!!notCompressedData", data);
|
|
}
|
|
return new Promise(resolve => resolve({ "!!notCompressedData": data }));
|
|
}
|
|
return this.decompressFile(compressed, data, updateMsg, fileCbFunc);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the appropriate worker script for the specified compression method.
|
|
*
|
|
* @param {string} method - The compression method ('7z', 'zip', or 'rar')
|
|
* @returns {Promise<Blob>} Promise that resolves to a Blob containing the worker script
|
|
*
|
|
* @description
|
|
* Downloads the necessary worker script and WASM files for the specified compression
|
|
* method. For RAR files, also downloads the libunrar.wasm file and creates a custom
|
|
* worker script with the WASM binary embedded.
|
|
*
|
|
* @throws {Error} When network errors occur during file downloads
|
|
*/
|
|
getWorkerFile(method) {
|
|
return new Promise(async (resolve, reject) => {
|
|
let path, obj;
|
|
if (method === "7z") {
|
|
path = "compression/extract7z.js";
|
|
obj = "sevenZip";
|
|
} else if (method === "zip") {
|
|
path = "compression/extractzip.js";
|
|
obj = "zip";
|
|
} else if (method === "rar") {
|
|
path = "compression/libunrar.js";
|
|
obj = "rar";
|
|
}
|
|
const res = await this.EJS.downloadFile(path, this.EJS.downloadType.support.name, null, false, { responseType: "text", method: "GET" }, false, this.EJS.downloadType.support.dontCache);
|
|
if (res === -1) {
|
|
this.EJS.startGameError(this.EJS.localization("Network Error"));
|
|
return;
|
|
}
|
|
if (method === "rar") {
|
|
const res2 = await this.EJS.downloadFile("compression/libunrar.wasm", this.EJS.downloadType.support.name, null, false, { responseType: "arraybuffer", method: "GET" }, false, this.EJS.downloadType.support.dontCache);
|
|
if (res2 === -1) {
|
|
this.EJS.startGameError(this.EJS.localization("Network Error"));
|
|
return;
|
|
}
|
|
const path = URL.createObjectURL(new Blob([res2.data], { type: "application/wasm" }));
|
|
let script = `
|
|
let dataToPass = [];
|
|
Module = {
|
|
monitorRunDependencies: function(left) {
|
|
if (left == 0) {
|
|
setTimeout(function() {
|
|
unrar(dataToPass, null);
|
|
}, 100);
|
|
}
|
|
},
|
|
onRuntimeInitialized: function() {},
|
|
locateFile: function(file) {
|
|
console.log("locateFile");
|
|
return "` + path + `";
|
|
}
|
|
};
|
|
` + res.data + `
|
|
let unrar = function(data, password) {
|
|
let cb = function(fileName, fileSize, progress) {
|
|
postMessage({ "t": 4, "current": progress, "total": fileSize, "name": fileName });
|
|
};
|
|
let rarContent = readRARContent(data.map(function(d) {
|
|
return {
|
|
name: d.name,
|
|
content: new Uint8Array(d.content)
|
|
}
|
|
}), password, cb)
|
|
let rec = function(entry) {
|
|
if (!entry) return;
|
|
if (entry.type === "file") {
|
|
postMessage({ "t": 2, "file": entry.fullFileName, "size": entry.fileSize, "data": entry.fileContent });
|
|
} else if (entry.type === "dir") {
|
|
Object.keys(entry.ls).forEach(function(k) {
|
|
rec(entry.ls[k]);
|
|
});
|
|
} else {
|
|
throw "Unknown type";
|
|
}
|
|
}
|
|
rec(rarContent);
|
|
postMessage({ "t": 1 });
|
|
return rarContent;
|
|
};
|
|
onmessage = function(data) {
|
|
dataToPass.push({ name: "test.rar", content: data.data });
|
|
};
|
|
`;
|
|
const blob = new Blob([script], {
|
|
type: "application/javascript"
|
|
})
|
|
resolve(blob);
|
|
} else {
|
|
const blob = new Blob([res.data.files[0].bytes], {
|
|
type: "application/javascript"
|
|
})
|
|
resolve(blob);
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Decompresses a file using the specified compression method.
|
|
*
|
|
* @param {string} method - The compression method ('7z', 'zip', or 'rar')
|
|
* @param {Uint8Array|ArrayBuffer} data - The compressed data to extract
|
|
* @param {Function} updateMsg - Callback function for progress updates (message, isProgress)
|
|
* @param {Function} fileCbFunc - Callback function called for each extracted file (filename, fileData)
|
|
* @returns {Promise<Object>} Promise that resolves to an object mapping filenames to file data
|
|
*
|
|
* @description
|
|
* Creates a web worker to handle the decompression process asynchronously.
|
|
* The worker communicates progress updates and extracted files back to the main thread.
|
|
*
|
|
* @example
|
|
* // Message types from worker:
|
|
* // t: 4 - Progress update (current, total, name)
|
|
* // t: 2 - File extracted (file, size, data)
|
|
* // t: 1 - Extraction complete
|
|
*/
|
|
decompressFile(method, data, updateMsg, fileCbFunc) {
|
|
return new Promise(async callback => {
|
|
const file = await this.getWorkerFile(method);
|
|
const worker = new Worker(URL.createObjectURL(file));
|
|
const files = {};
|
|
worker.onmessage = (data) => {
|
|
if (!data.data) return;
|
|
//data.data.t/ 4=progress, 2 is file, 1 is zip done
|
|
if (data.data.t === 4) {
|
|
const pg = data.data;
|
|
const num = Math.floor(pg.current / pg.total * 100);
|
|
if (isNaN(num)) return;
|
|
const progress = " " + num.toString() + "%";
|
|
updateMsg(progress, true);
|
|
}
|
|
if (data.data.t === 2) {
|
|
if (typeof fileCbFunc === "function") {
|
|
fileCbFunc(data.data.file, data.data.data);
|
|
files[data.data.file] = true;
|
|
} else {
|
|
files[data.data.file] = data.data.data;
|
|
}
|
|
}
|
|
if (data.data.t === 1) {
|
|
callback(files);
|
|
}
|
|
}
|
|
worker.postMessage(data);
|
|
});
|
|
}
|
|
}
|
|
|
|
window.EJS_COMPRESSION = EJSCompression;
|