-
-
-
Language
-
-
- :
-
-
-
-
-
-
-
Paths
-
-
- :
-
-
-
-
-
-
-
-
-
- :
-
-
-
-
-
-
-
-
-
Game List
-
-
+
+
Launcher Settings
+
+
+
+
+
+
- :
-
-
-
-
-
-
- :
-
-
- %
-
-
-
-
-
- :
-
- %
-
-
-
-
+
+
Language
-
:
+
+ :
+
+ ()
+
+
+
+
+
Paths
+
+
+ :
+
+
+
+
+
+
-
-
%
+
+
+ :
+
+
+
+
+
+
+
+
+
Game List
+
+
+
+ :
+
+
+
+
+
+
+ :
+
+
+ %
+
+
+
+
+
+ :
+
+ %
+
+
+
+
+
+ :
+
+
+ %
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Emu Running
+
+
+ :
+
+ %
+
+
+
+ :
+
+ %
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Log Options
+
+
+ :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
fpPS4 Updater
+
+
+
+
+ Enable fpPS4 updater
+
+
+
+
+
+ Search updates from branch
+ :
+
+
+
+
+
+
+
+
+
+
Misc.
+
+
+
+
+ Enable PARAM.SFO
support (Click on "Delete all game settings" and restart launcher to take effect)
+
+
+
+
+
+
+ Remove Project.gp4
from every game on game list load
+
+
+
+
+
+
+
+
-
-
-
-
-
- Enable case-sentitive search on game list
-
-
-
-
-
-
- Show background image for every game entry on list
-
-
-
-
-
-
- Show app / game metadata (or executable path) for every title in game list
-
-
-
-
-
-
Emu Running
-
-
- Background Blur:
-
- ???%
-
-
-
- Background Opacity:
-
- ???%
-
-
-
-
-
- Display icon / name GUI while fpPS4 is running
-
-
-
-
-
-
- Display app / game metadata (or executable path) below game title
-
-
-
-
-
-
Log Options
-
-
- Start console window:
-
-
-
-
-
-
- Prompt user to press any key when fpPS4 closes
-
-
-
-
-
-
- EXPERIMENTAL Log fpPS4 process output (stdout
and stderr
) on intenal log (Press F12 --> Console)
-
-
-
-
-
-
Misc.
-
-
-
-
- Enable PARAM.SFO
support (Click on "Delete all game settings" and restart launcher to take effect)
-
-
-
-
-
-
- Remove Project.gp4
from every game on game list load
-
-
-
-
-
-
-
-
-
-
-
+
@@ -233,6 +277,7 @@
+
@@ -358,10 +403,11 @@
-
+
+
-
+
diff --git a/App/js/design.js b/App/js/design.js
index 39357ca..18ec4b3 100644
--- a/App/js/design.js
+++ b/App/js/design.js
@@ -347,10 +347,7 @@ temp_DESIGN = {
// Check if emu is present before allowing to run
if (APP.fs.existsSync(APP.settings.data.emuPath) === !0 && APP.gameList.selectedGame !== ''){
- var btnRun = '',
- btnLog = '',
- btnRefresh = '',
- btnSettings = '',
+ var btnDisabled = '',
btnKill = 'disabled',
emuRunPath = 'block',
bgBlur = APP.settings.data.bgListBlur,
@@ -364,10 +361,7 @@ temp_DESIGN = {
if (APP.emuManager.emuRunning === !0){
btnKill = '';
- btnLog = 'disabled';
- btnRun = 'disabled';
- btnRefresh = 'disabled';
- btnSettings = 'disabled';
+ btnDisabled = 'disabled';
bgBlur = APP.settings.data.bgEmuBlur;
showGuiMetadata = {'display': 'flex'};
bgOpacity = APP.settings.data.bgEmuOpacity;
@@ -391,12 +385,13 @@ temp_DESIGN = {
TMS.css('DIV_GAMELIST_BG', {'filter': 'blur(' + bgBlur + 'px) opacity(' + bgOpacity + ')'});
// Update Buttons
- document.getElementById('BTN_RUN').disabled = btnRun;
document.getElementById('BTN_KILL').disabled = btnKill;
- document.getElementById('BTN_CLEAR_LOG').disabled = btnLog;
- document.getElementById('BTN_REFRESH').disabled = btnRefresh;
- document.getElementById('BTN_SETTINGS').disabled = btnSettings;
- document.getElementById('INPUT_gameListSearch').disabled = btnRun;
+ document.getElementById('BTN_RUN').disabled = btnDisabled;
+ document.getElementById('BTN_REFRESH').disabled = btnDisabled;
+ document.getElementById('BTN_SETTINGS').disabled = btnDisabled;
+ document.getElementById('BTN_CLEAR_LOG').disabled = btnDisabled;
+ document.getElementById('BTN_UPDATE_FPPS4').disabled = btnDisabled;
+ document.getElementById('INPUT_gameListSearch').disabled = btnDisabled;
} else {
@@ -661,6 +656,7 @@ temp_DESIGN = {
document.getElementById('CHECKBOX_settingsShowBgOnGameEntry').checked = JSON.parse(cSettings.showBgOnEntry);
document.getElementById('CHECKBOX_settingsShowGameMetadata').checked = JSON.parse(cSettings.showGuiMetadata);
document.getElementById('CHECKBOX_settingsRemoveProjectGp4').checked = JSON.parse(cSettings.removeProjectGp4);
+ document.getElementById('CHECKBOX_settingsEnableFpps4Updates').checked = JSON.parse(cSettings.enableEmuUpdates);
document.getElementById('CHECKBOX_settingsGameSearchCaseSensitive').checked = JSON.parse(cSettings.searchCaseSensitive);
document.getElementById('CHECKBOX_settingsExternalWindowPrompt').checked = JSON.parse(cSettings.logExternalWindowPrompt);
@@ -675,6 +671,9 @@ temp_DESIGN = {
document.getElementById('RANGE_settingsEmuRunningBgOpacity').value = cSettings.bgEmuOpacity;
document.getElementById('RANGE_settingsGridIconBorderRadius').value = cSettings.gridBorderRadius;
+ // Text
+ document.getElementById('INPUT_settingsUpdateFpps4Branch').value = cSettings.fpps4BranchName;
+
// Fix for grid size / border-radius
if (cSettings.gridIconSize > 512){
cSettings.gridIconSize = 512;
@@ -735,6 +734,7 @@ temp_DESIGN = {
APP.settings.data.showPathRunning = JSON.parse(document.getElementById('CHECKBOX_settingsShowExecRunning').checked);
APP.settings.data.showGuiMetadata = JSON.parse(document.getElementById('CHECKBOX_settingsShowGameMetadata').checked);
APP.settings.data.removeProjectGp4 = JSON.parse(document.getElementById('CHECKBOX_settingsRemoveProjectGp4').checked);
+ APP.settings.data.enableEmuUpdates = JSON.parse(document.getElementById('CHECKBOX_settingsEnableFpps4Updates').checked);
APP.settings.data.searchCaseSensitive = JSON.parse(document.getElementById('CHECKBOX_settingsGameSearchCaseSensitive').checked);
APP.settings.data.logExternalWindowPrompt = JSON.parse(document.getElementById('CHECKBOX_settingsExternalWindowPrompt').checked);
@@ -749,6 +749,9 @@ temp_DESIGN = {
APP.settings.data.bgEmuOpacity = parseFloat(document.getElementById('RANGE_settingsEmuRunningBgOpacity').value);
APP.settings.data.gridBorderRadius = parseFloat(document.getElementById('RANGE_settingsGridIconBorderRadius').value);
+ // Text
+ APP.settings.data.fpps4BranchName = document.getElementById('INPUT_settingsUpdateFpps4Branch').value;
+
/*
End
*/
@@ -761,6 +764,44 @@ temp_DESIGN = {
APP.design.toggleSettings(!0);
}
+ },
+
+ /*
+ Updater
+ */
+
+ // Display / Hide GUI
+ toggleEmuUpdateGUI: function(mode){
+
+ var cssData;
+ switch (mode) {
+
+ case 'show':
+ cssData = {'display': 'flex'};
+ break;
+
+ case 'hide':
+ cssData = {'display': 'none'};
+ break;
+
+ default:
+ cssData = {'display': 'none'};
+ break;
+
+ }
+
+ // Reset progressbar status
+ TMS.css('DIV_PROGRESSBAR_UPDATE_FPPS4', {'width': '0%'});
+
+ // Update display mode
+ TMS.css('DIV_FPPS4_UPDATER', cssData);
+
+ },
+
+ // Update status
+ updateProgressbarStatus: function(percentage, status){
+ TMS.css('DIV_PROGRESSBAR_UPDATE_FPPS4', {'width': percentage + '%'});
+ document.getElementById('LABEL_FPPS4_UPDATER_STATUS').innerHTML = status;
}
}
\ No newline at end of file
diff --git a/App/js/emumanager.js b/App/js/emumanager.js
index 06e29a3..6ed0d08 100644
--- a/App/js/emumanager.js
+++ b/App/js/emumanager.js
@@ -4,7 +4,7 @@
emumanager.js
This file contains all functions / variables about running main project
- executable and game module checks.
+ executable, game module checks and updating fpPS4 executable.
******************************************************************************
*/
@@ -13,6 +13,9 @@ temp_EMUMANAGER = {
// Emulator is running
emuRunning: !1,
+ // Update functions
+ update: temp_EMU_UPDATE,
+
// Run emu
runGame: function(){
@@ -85,7 +88,7 @@ temp_EMUMANAGER = {
}
// Kill process and set emu running var to false
- APP.getProcessInfo('fpPS4.exe', function(pData){
+ APP.getProcessInfo(APP.path.parse(APP.settings.data.emuPath).base, function(pData){
process.kill(pData.th32ProcessID);
this.emuRunning = !1;
});
diff --git a/App/js/language.js b/App/js/language.js
index 92662b2..bd5854b 100644
--- a/App/js/language.js
+++ b/App/js/language.js
@@ -56,7 +56,7 @@ temp_LANGUAGE = {
"logWindowTitle": "Running fpPS4",
"killEmuStatus": "Main process closed - close fpPS4 log window to go back",
"logCleared": "INFO - Previous log was cleared!\n ",
- "about": "fpPS4 Temmie\'s Launcher - Version: %VARIABLE_0%\nCreated by TemmieHeartz\n(https://twitter.com/themitosan)\n\nfpPS4 is created by red-prig\n(https://github.com/red-prig/fpPS4)\n\nPlugin memoryjs is created by Rob--\n(https://github.com/rob--/memoryjs)\n\nSVG icons were obtained from https://www.svgrepo.com/",
+ "about": "fpPS4 Temmie\'s Launcher - Version: %VARIABLE_0%\nCreated by TemmieHeartz\n(https://twitter.com/themitosan)\n\nfpPS4 is created by red-prig\n(https://github.com/red-prig/fpPS4)\n\nPlugin memoryjs is created by Rob--\n(https://github.com/rob--/memoryjs)\n\nPlugin node-stream-zip is created by antelle\n(https://github.com/antelle/node-stream-zip)\n\nSVG icons were obtained from https://www.svgrepo.com/",
"mainLog": 'fpPS4 Temmie\'s Launcher - Version: %VARIABLE_0%\nRunning on nw.js (node-webkit) version %VARIABLE_1% [%VARIABLE_2%]',
"settingsErrorCreatePath": "ERROR - Unable to create path!\n(%VARIABLE_0%)\n%VARIABLE_1%",
"settingsErrorfpPS4NotFound": "ERROR - Unable to locate main fpPS4 executable!\nMake sure to select it in Settings or insert it in \"Emu\" folder and click OK.",
@@ -95,7 +95,19 @@ temp_LANGUAGE = {
"gameListVersion": "Version",
"selectGameLoadPatchErrorParamSfo": "ERROR - Unable to read PARAM.SFO from this patch!\n%VARIABLE_0%",
"path": "Path",
- "gamelistGamePath404": "ERROR - Unable to find selected app / game path!\n%VARIABLE_0%"
+ "gamelistGamePath404": "ERROR - Unable to find selected app / game path!\n%VARIABLE_0%",
+
+ "updateEmuFetchActionsError": "ERROR - Unable to fetch GitHub actions data!",
+ "updateEmuIsLatestVersion": "INFO - You are using the latest fpPS4 version available!\nCommit ID (SHA): %VARIABLE_0%",
+ "updateEmuShaAvailable": "INFO - A new update is available!\n\nLocal version: %VARIABLE_0%\nNew version: %VARIABLE_1%\n\nDo you want to update?",
+ "updateEmuShaUnavailable": "INFO - This Launcher detected that you didn\'t updated fpPS4 yet (or fpPS4 executable was not found!)\n\nYou can fix this by running fpPS4 updater process.\nDo you want to proceed?",
+ "updateEmuDownloadFailed": "ERROR - Unable to download fpPS4 update!\nResponse status: %VARIABLE_0% - OK: %VARIABLE_1%",
+ "updateEmuProcessComplete": "INFO - Update complete!\nNew fpPS4 version (commit id / sha): %VARIABLE_0%",
+ "updateEmu-1-4": "Downloading fpPS4 update (
%VARIABLE_0%)",
+ "updateEmu-2-4": "Extracting update",
+ "updateEmu-3-4": "Removing leftover files",
+ "updateEmu-4-4": "Update complete!",
+ "settingsLogEmuSha": "INFO - fpPS4 version: (%VARIABLE_0%)"
},
@@ -108,4 +120,4 @@ temp_LANGUAGE = {
// Selected lang
selected: {}
-}
+}
\ No newline at end of file
diff --git a/App/js/main.js b/App/js/main.js
index b3f21db..3ee640b 100644
--- a/App/js/main.js
+++ b/App/js/main.js
@@ -14,9 +14,11 @@ var APP = {
fs: require('fs'),
win: nw.Window.get(),
path: require('path'),
+ https: require('https'),
childProcess: require('child_process'),
packageJson: require('../package.json'),
memoryjs: require('App/node_modules/memoryjs'),
+ streamZip: require('App/node_modules/node-stream-zip'),
// App version
title: '',
@@ -257,6 +259,7 @@ delete temp_SETTINGS;
delete temp_GAMELIST;
delete temp_LANGUAGE;
delete temp_EMUMANAGER;
+delete temp_EMU_UPDATE;
delete temp_FILEMANAGER;
delete temp_PARAMSFO_PARSER;
@@ -297,6 +300,9 @@ window.onload = function(){
// Remove all previous imported modules
APP.gameList.removeAllModules();
+ // Check if fpPS4 have any update (silenty)
+ APP.emuManager.update.check({silent: !0});
+
} catch (err) {
// Log error
diff --git a/App/js/settings.js b/App/js/settings.js
index 4fdfb5d..afeec39 100644
--- a/App/js/settings.js
+++ b/App/js/settings.js
@@ -45,7 +45,7 @@ temp_SETTINGS = {
// Game list
showBgOnEntry: !0,
showPathEntry: !0,
- gameListMode: 'normal',
+ gameListMode: 'compact',
// Emu running
showPathRunning: !0,
@@ -66,7 +66,14 @@ temp_SETTINGS = {
// (Grid)
gridIconSize: 116,
gridBorderRadius: 8,
-
+
+ /*
+ fpPS4 Update
+ */
+ enableEmuUpdates: !0,
+ latestCommitSha: '',
+ fpps4BranchName: 'trunk',
+
/*
Debug
*/
@@ -210,11 +217,16 @@ temp_SETTINGS = {
if (this.data.emuPath === '' || APP.fs.existsSync(this.data.emuPath) === !1){
APP.settings.data.emuPath = mainPath + '/Emu/fpPS4.exe';
}
+
+ // If fpPS4 is not found, reset latest commit sha and request update
if (APP.fs.existsSync(this.data.emuPath) !== !0){
+ this.data.latestCommitSha = '';
+ APP.emuManager.update.check();
+ }
- logMessage = APP.lang.getVariable('settingsErrorfpPS4NotFound');
- window.alert(logMessage);
-
+ // If latestCommitSha isn't empty, log it
+ if (this.data.latestCommitSha !== ''){
+ APP.log(APP.lang.getVariable('settingsLogEmuSha', [APP.settings.data.latestCommitSha.slice(0, 7)]));
}
// Log message
diff --git a/App/js/updateEmu.js b/App/js/updateEmu.js
new file mode 100644
index 0000000..a032764
--- /dev/null
+++ b/App/js/updateEmu.js
@@ -0,0 +1,260 @@
+/*
+ ******************************************************************************
+ fpPS4 Temmie's Launcher
+ updateEmu.js
+
+ This file is responsible for feching latest data from red-prig fpPS4 actions
+ and update.
+ ******************************************************************************
+*/
+
+temp_EMU_UPDATE = {
+
+ // GitHub actions link
+ githubLink: 'https://api.github.com/repos/red-prig/fpPS4/actions/artifacts',
+
+ /*
+ Fetch latest github actions
+
+ options: Object
+ jsonData: [Object] GitHub actions list (json)
+ forceUpdate: [Boolean] Skip checks and download latest version available
+ silent: [Boolean] Don't show message if user already have latest version
+ */
+ check: function(options){
+
+ if (options === void 0){
+ options = {
+ forceUpdate: !1,
+ silent: !1
+ };
+ }
+
+ // If Emu updates is available, has internet and fpPS4 isn't running
+ if (APP.settings.data.enableEmuUpdates === !0 && navigator.onLine === !0 && APP.emuManager.emuRunning === !1){
+
+ // Disable check for updates emu
+ document.getElementById('BTN_UPDATE_FPPS4').disabled = 'disabled';
+
+ // Get error message
+ const errMsg = APP.lang.getVariable('updateEmuFetchActionsError');
+
+ // Fetch data
+ fetch(this.githubLink).then(function(resp){
+
+ // Check if fetch status is ok
+ if (resp.ok === !0){
+
+ resp.json().then(function(jsonData){
+ options['jsonData'] = jsonData;
+ APP.emuManager.update.processActions(options);
+ });
+
+ } else {
+
+
+ // If launcher can't get data, log error and reset button
+ APP.log(errMsg);
+ console.error(errMsg);
+ document.getElementById('BTN_UPDATE_FPPS4').disabled = '';
+
+ }
+
+ });
+
+ }
+
+ },
+
+ // Process github actions data
+ processActions: function(options){
+
+ const data = options.jsonData;
+
+ if (data !== void 0){
+
+ var conf, updateData, updateId,
+ latestSha = APP.settings.data.latestCommitSha,
+ accpetableBranch = APP.settings.data.fpps4BranchName;
+
+ // Read latest actions
+ for (var i = 0; i < Object.keys(data.artifacts).length; i++){
+
+ // Shortcut
+ const workflow = data.artifacts[i].workflow_run;
+
+ // If user already updated and have latest changes
+ if (workflow.head_sha === latestSha){
+ updateId = i;
+ break;
+ }
+
+ // Check if branch is the same selected on settings, repo is from red-prig and if lash head_sha is different
+ if (workflow.head_branch === accpetableBranch && workflow.head_sha !== latestSha){
+ updateId = i;
+ break;
+ }
+
+ }
+
+ // Enable fpPS4 updates button again
+ document.getElementById('BTN_UPDATE_FPPS4').disabled = '';
+
+ // Check if there's matching updates
+ if (updateId !== void 0){
+
+ // Set latest valid commit as new update
+ updateData = data.artifacts[updateId];
+
+ // Set user message
+ var canPrompt = !0,
+ msgMode = 'confirm',
+ msgData = APP.lang.getVariable('updateEmuShaAvailable', [latestSha.slice(0, 7), updateData.workflow_run.head_sha.slice(0, 7)]);
+
+ // If user didn't updated yet using launcher
+ if (latestSha === ''){
+ msgData = APP.lang.getVariable('updateEmuShaUnavailable');
+ }
+
+ // If local version is the latest
+ if (latestSha === updateData.workflow_run.head_sha){
+
+ // Update prompt
+ msgMode = 'alert';
+ msgData = APP.lang.getVariable('updateEmuIsLatestVersion', [latestSha.slice(0, 7)]);
+
+ // If silent is active
+ if (options.silent === !0){
+ canPrompt = !1;
+ }
+
+ }
+
+ // Call popup
+ if (canPrompt === !0 && options.forceUpdate === !1){
+ conf = window[msgMode](msgData);
+ }
+
+ // If anren't latest version and user confirms
+ if (msgMode === 'confirm' && conf === !0 || options.forceUpdate === !0){
+ this.getZipFile(updateData);
+ }
+
+ }
+
+ }
+
+ },
+
+ /*
+ Get zip from specific github action run
+
+ Since fpPS4 actions require being logged to download, nightly.links service will be used instead.
+ https://nightly.link
+ */
+ getZipFile: function(actionsData){
+
+ // If (by some reason) fpPS4 is running - close it!
+ APP.emuManager.killEmu();
+
+ // Display GUI
+ APP.design.toggleEmuUpdateGUI('show');
+ APP.design.updateProgressbarStatus(25, APP.lang.getVariable('updateEmu-1-4', [actionsData.workflow_run.head_sha.slice(0, 7)]));
+
+ // Start download
+ fetch('https://nightly.link/red-prig/fpPS4/actions/runs/' + actionsData.workflow_run.id + '/fpPS4.zip').then(function(resp){
+
+ if (resp.ok === !0){
+
+ APP.https.get(resp.url, function(data){
+
+ const fPath = APP.settings.data.nwPath + '/Emu/fpPS4.zip',
+ writeStream = APP.fs.createWriteStream(fPath);
+
+ data.pipe(writeStream);
+
+ writeStream.on('finish', function(){
+
+ // Close writestream
+ writeStream.close();
+
+ // Extract emu executable
+ APP.emuManager.update.extractZip({
+ newExecName: 'fpPS4_' + actionsData.workflow_run.head_sha.slice(0, 7) + '.exe',
+ actions: actionsData,
+ path: fPath
+ });
+
+ });
+
+ });
+
+ } else {
+
+ console.error(resp);
+ APP.log(APP.lang.getVariable('updateEmuDownloadFailed', [resp.status, resp.ok]));
+
+ }
+
+ });
+
+ },
+
+ // Extract zip
+ extractZip: function(data){
+
+ // Update status
+ APP.design.updateProgressbarStatus(50, APP.lang.getVariable('updateEmu-2-4'));
+
+ // Open and extract zip file
+ const updateFile = new APP.streamZip.async({ file: data.path });
+ updateFile.extract('fpPS4.exe', APP.path.parse(data.path).dir + '/' + data.newExecName, function(err){
+ if (err){
+ console.error(err);
+ }
+ }).then(function(){
+
+ // Close zip
+ updateFile.close();
+
+ // Finish process
+ APP.emuManager.update.finish(data);
+
+ });
+
+ },
+
+ // Finish process
+ finish: function(data){
+
+ // Update status
+ APP.design.updateProgressbarStatus(75, APP.lang.getVariable('updateEmu-3-4'));
+
+ // Remove download file
+ APP.fs.unlinkSync(data.path);
+
+ // Update settings
+ APP.settings.data.latestCommitSha = data.actions.workflow_run.head_sha;
+ APP.settings.data.emuPath = APP.path.parse(data.path).dir + '/' + data.newExecName;
+
+ // Save settings
+ APP.settings.save();
+
+ // Display success message
+ const processCompleteMsg = APP.lang.getVariable('updateEmuProcessComplete', [data.actions.workflow_run.head_sha.slice(0, 7)]);
+ APP.design.updateProgressbarStatus(100, APP.lang.getVariable('updateEmu-4-4'));
+
+ // Timing out just to update GUI
+ setTimeout(function(){
+
+ APP.log(processCompleteMsg);
+ window.alert(processCompleteMsg);
+
+ // Hide update gui
+ APP.design.toggleEmuUpdateGUI('hide');
+
+ }, 410);
+
+ }
+
+}
\ No newline at end of file
diff --git a/App/node_modules/node-stream-zip/LICENSE b/App/node_modules/node-stream-zip/LICENSE
new file mode 100644
index 0000000..37ac867
--- /dev/null
+++ b/App/node_modules/node-stream-zip/LICENSE
@@ -0,0 +1,44 @@
+Copyright (c) 2021 Antelle https://github.com/antelle
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+== dependency license: adm-zip ==
+
+Copyright (c) 2012 Another-D-Mention Software and other contributors,
+http://www.another-d-mention.ro/
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/App/node_modules/node-stream-zip/README.md b/App/node_modules/node-stream-zip/README.md
new file mode 100644
index 0000000..98b5a56
--- /dev/null
+++ b/App/node_modules/node-stream-zip/README.md
@@ -0,0 +1,224 @@
+# node-stream-zip ![CI Checks](https://github.com/antelle/node-stream-zip/workflows/CI%20Checks/badge.svg)
+
+node.js library for reading and extraction of ZIP archives.
+Features:
+
+- it never loads entire archive into memory, everything is read by chunks
+- large archives support
+- all operations are non-blocking, no sync i/o
+- fast initialization
+- no dependencies, no binary addons
+- decompression with built-in zlib module
+- deflate, sfx, macosx/windows built-in archives
+- ZIP64 support
+
+## Installation
+
+```sh
+npm i node-stream-zip
+```
+
+## Usage
+
+There are two APIs provided:
+1. [promise-based / async](#async-api)
+2. [callbacks](#callback-api)
+
+It's recommended to use the new, promise API, however the legacy callback API
+may be more flexible for certain operations.
+
+### Async API
+
+Open a zip file
+```javascript
+const StreamZip = require('node-stream-zip');
+const zip = new StreamZip.async({ file: 'archive.zip' });
+```
+
+Stream one entry to stdout
+```javascript
+const stm = await zip.stream('path/inside/zip.txt');
+stm.pipe(process.stdout);
+stm.on('end', () => zip.close());
+```
+
+Read a file as buffer
+```javascript
+const data = await zip.entryData('path/inside/zip.txt');
+await zip.close();
+```
+
+Extract one file to disk
+```javascript
+await zip.extract('path/inside/zip.txt', './extracted.txt');
+await zip.close();
+```
+
+List entries
+```javascript
+const entriesCount = await zip.entriesCount;
+console.log(`Entries read: ${entriesCount}`);
+
+const entries = await zip.entries();
+for (const entry of Object.values(entries)) {
+ const desc = entry.isDirectory ? 'directory' : `${entry.size} bytes`;
+ console.log(`Entry ${entry.name}: ${desc}`);
+}
+
+// Do not forget to close the file once you're done
+await zip.close();
+```
+
+Extract a folder from archive to disk
+```javascript
+fs.mkdirSync('extracted');
+await zip.extract('path/inside/zip/', './extracted');
+await zip.close();
+```
+
+Extract everything
+```javascript
+fs.mkdirSync('extracted');
+const count = await zip.extract(null, './extracted');
+console.log(`Extracted ${count} entries`);
+await zip.close();
+```
+
+When extracting a folder, you can listen to `extract` event
+```javascript
+zip.on('extract', (entry, file) => {
+ console.log(`Extracted ${entry.name} to ${file}`);
+});
+```
+
+`entry` event is generated for every entry during loading
+```javascript
+zip.on('entry', entry => {
+ // you can already stream this entry,
+ // without waiting until all entry descriptions are read (suitable for very large archives)
+ console.log(`Read entry ${entry.name}`);
+});
+```
+
+### Callback API
+
+Open a zip file
+```javascript
+const StreamZip = require('node-stream-zip');
+const zip = new StreamZip({ file: 'archive.zip' });
+
+// Handle errors
+zip.on('error', err => { /*...*/ });
+```
+
+List entries
+```javascript
+zip.on('ready', () => {
+ console.log('Entries read: ' + zip.entriesCount);
+ for (const entry of Object.values(zip.entries())) {
+ const desc = entry.isDirectory ? 'directory' : `${entry.size} bytes`;
+ console.log(`Entry ${entry.name}: ${desc}`);
+ }
+ // Do not forget to close the file once you're done
+ zip.close();
+});
+```
+
+Stream one entry to stdout
+```javascript
+zip.on('ready', () => {
+ zip.stream('path/inside/zip.txt', (err, stm) => {
+ stm.pipe(process.stdout);
+ stm.on('end', () => zip.close());
+ });
+});
+```
+
+Extract one file to disk
+```javascript
+zip.on('ready', () => {
+ zip.extract('path/inside/zip.txt', './extracted.txt', err => {
+ console.log(err ? 'Extract error' : 'Extracted');
+ zip.close();
+ });
+});
+```
+
+Extract a folder from archive to disk
+```javascript
+zip.on('ready', () => {
+ fs.mkdirSync('extracted');
+ zip.extract('path/inside/zip/', './extracted', err => {
+ console.log(err ? 'Extract error' : 'Extracted');
+ zip.close();
+ });
+});
+```
+
+Extract everything
+```javascript
+zip.on('ready', () => {
+ fs.mkdirSync('extracted');
+ zip.extract(null, './extracted', (err, count) => {
+ console.log(err ? 'Extract error' : `Extracted ${count} entries`);
+ zip.close();
+ });
+});
+```
+
+Read a file as buffer in sync way
+```javascript
+zip.on('ready', () => {
+ const data = zip.entryDataSync('path/inside/zip.txt');
+ zip.close();
+});
+```
+
+When extracting a folder, you can listen to `extract` event
+```javascript
+zip.on('extract', (entry, file) => {
+ console.log(`Extracted ${entry.name} to ${file}`);
+});
+```
+
+`entry` event is generated for every entry during loading
+```javascript
+zip.on('entry', entry => {
+ // you can already stream this entry,
+ // without waiting until all entry descriptions are read (suitable for very large archives)
+ console.log(`Read entry ${entry.name}`);
+});
+```
+
+## Options
+
+You can pass these options to the constructor
+- `storeEntries: true` - you will be able to work with entries inside zip archive, otherwise the only way to access them is `entry` event
+- `skipEntryNameValidation: true` - by default, entry name is checked for malicious characters, like `../` or `c:\123`, pass this flag to disable validation errors
+
+## Methods
+
+- `zip.entries()` - get all entries description
+- `zip.entry(name)` - get entry description by name
+- `zip.stream(entry, function(err, stm) { })` - get entry data reader stream
+- `zip.entryDataSync(entry)` - get entry data in sync way
+- `zip.close()` - cleanup after all entries have been read, streamed, extracted, and you don't need the archive
+
+## Building
+
+The project doesn't require building. To run unit tests with [nodeunit](https://github.com/caolan/nodeunit):
+```sh
+npm test
+```
+
+## Known issues
+
+- [utf8](https://github.com/rubyzip/rubyzip/wiki/Files-with-non-ascii-filenames) file names
+
+## Out of scope
+
+- AES encrypted files: the library will throw an error if you try to open it
+
+## Contributors
+
+ZIP parsing code has been partially forked from [cthackers/adm-zip](https://github.com/cthackers/adm-zip) (MIT license).
diff --git a/App/node_modules/node-stream-zip/node_stream_zip.d.ts b/App/node_modules/node-stream-zip/node_stream_zip.d.ts
new file mode 100644
index 0000000..f076c72
--- /dev/null
+++ b/App/node_modules/node-stream-zip/node_stream_zip.d.ts
@@ -0,0 +1,199 @@
+///
+
+declare namespace StreamZip {
+ interface StreamZipOptions {
+ /**
+ * File to read
+ * @default undefined
+ */
+ file?: string;
+
+ /**
+ * Alternatively, you can pass fd here
+ * @default undefined
+ */
+ fd?: number;
+
+ /**
+ * You will be able to work with entries inside zip archive,
+ * otherwise the only way to access them is entry event
+ * @default true
+ */
+ storeEntries?: boolean;
+
+ /**
+ * By default, entry name is checked for malicious characters, like ../ or c:\123,
+ * pass this flag to disable validation error
+ * @default false
+ */
+ skipEntryNameValidation?: boolean;
+
+ /**
+ * Filesystem read chunk size
+ * @default automatic based on file size
+ */
+ chunkSize?: number;
+
+ /**
+ * Encoding used to decode file names
+ * @default UTF8
+ */
+ nameEncoding?: string;
+ }
+
+ interface ZipEntry {
+ /**
+ * file name
+ */
+ name: string;
+
+ /**
+ * true if it's a directory entry
+ */
+ isDirectory: boolean;
+
+ /**
+ * true if it's a file entry, see also isDirectory
+ */
+ isFile: boolean;
+
+ /**
+ * file comment
+ */
+ comment: string;
+
+ /**
+ * if the file is encrypted
+ */
+ encrypted: boolean;
+
+ /**
+ * version made by
+ */
+ verMade: number;
+
+ /**
+ * version needed to extract
+ */
+ version: number;
+
+ /**
+ * encrypt, decrypt flags
+ */
+ flags: number;
+
+ /**
+ * compression method
+ */
+ method: number;
+
+ /**
+ * modification time
+ */
+ time: number;
+
+ /**
+ * uncompressed file crc-32 value
+ */
+ crc: number;
+
+ /**
+ * compressed size
+ */
+ compressedSize: number;
+
+ /**
+ * uncompressed size
+ */
+ size: number;
+
+ /**
+ * volume number start
+ */
+ diskStart: number;
+
+ /**
+ * internal file attributes
+ */
+ inattr: number;
+
+ /**
+ * external file attributes
+ */
+ attr: number;
+
+ /**
+ * LOC header offset
+ */
+ offset: number;
+ }
+
+ class StreamZipAsync {
+ constructor(config: StreamZipOptions);
+
+ entriesCount: Promise
;
+ comment: Promise;
+
+ entry(name: string): Promise;
+ entries(): Promise<{ [name: string]: ZipEntry }>;
+ entryData(entry: string | ZipEntry): Promise;
+ stream(entry: string | ZipEntry): Promise;
+ extract(entry: string | ZipEntry | null, outPath: string): Promise;
+
+ on(event: 'entry', handler: (entry: ZipEntry) => void): void;
+ on(event: 'extract', handler: (entry: ZipEntry, outPath: string) => void): void;
+
+ close(): Promise;
+ }
+}
+
+type StreamZipOptions = StreamZip.StreamZipOptions;
+type ZipEntry = StreamZip.ZipEntry;
+
+declare class StreamZip {
+ constructor(config: StreamZipOptions);
+
+ /**
+ * number of entries in the archive
+ */
+ entriesCount: number;
+
+ /**
+ * archive comment
+ */
+ comment: string;
+
+ on(event: 'error', handler: (error: any) => void): void;
+ on(event: 'entry', handler: (entry: ZipEntry) => void): void;
+ on(event: 'ready', handler: () => void): void;
+ on(event: 'extract', handler: (entry: ZipEntry, outPath: string) => void): void;
+
+ entry(name: string): ZipEntry | undefined;
+
+ entries(): { [name: string]: ZipEntry };
+
+ stream(
+ entry: string | ZipEntry,
+ callback: (err: any | null, stream?: NodeJS.ReadableStream) => void
+ ): void;
+
+ entryDataSync(entry: string | ZipEntry): Buffer;
+
+ openEntry(
+ entry: string | ZipEntry,
+ callback: (err: any | null, entry?: ZipEntry) => void,
+ sync: boolean
+ ): void;
+
+ extract(
+ entry: string | ZipEntry | null,
+ outPath: string,
+ callback: (err?: any, res?: number) => void
+ ): void;
+
+ close(callback?: (err?: any) => void): void;
+
+ static async: typeof StreamZip.StreamZipAsync;
+}
+
+export = StreamZip;
diff --git a/App/node_modules/node-stream-zip/node_stream_zip.js b/App/node_modules/node-stream-zip/node_stream_zip.js
new file mode 100644
index 0000000..d95bbef
--- /dev/null
+++ b/App/node_modules/node-stream-zip/node_stream_zip.js
@@ -0,0 +1,1210 @@
+/**
+ * @license node-stream-zip | (c) 2020 Antelle | https://github.com/antelle/node-stream-zip/blob/master/LICENSE
+ * Portions copyright https://github.com/cthackers/adm-zip | https://raw.githubusercontent.com/cthackers/adm-zip/master/LICENSE
+ */
+
+let fs = require('fs');
+const util = require('util');
+const path = require('path');
+const events = require('events');
+const zlib = require('zlib');
+const stream = require('stream');
+
+const consts = {
+ /* The local file header */
+ LOCHDR: 30, // LOC header size
+ LOCSIG: 0x04034b50, // "PK\003\004"
+ LOCVER: 4, // version needed to extract
+ LOCFLG: 6, // general purpose bit flag
+ LOCHOW: 8, // compression method
+ LOCTIM: 10, // modification time (2 bytes time, 2 bytes date)
+ LOCCRC: 14, // uncompressed file crc-32 value
+ LOCSIZ: 18, // compressed size
+ LOCLEN: 22, // uncompressed size
+ LOCNAM: 26, // filename length
+ LOCEXT: 28, // extra field length
+
+ /* The Data descriptor */
+ EXTSIG: 0x08074b50, // "PK\007\008"
+ EXTHDR: 16, // EXT header size
+ EXTCRC: 4, // uncompressed file crc-32 value
+ EXTSIZ: 8, // compressed size
+ EXTLEN: 12, // uncompressed size
+
+ /* The central directory file header */
+ CENHDR: 46, // CEN header size
+ CENSIG: 0x02014b50, // "PK\001\002"
+ CENVEM: 4, // version made by
+ CENVER: 6, // version needed to extract
+ CENFLG: 8, // encrypt, decrypt flags
+ CENHOW: 10, // compression method
+ CENTIM: 12, // modification time (2 bytes time, 2 bytes date)
+ CENCRC: 16, // uncompressed file crc-32 value
+ CENSIZ: 20, // compressed size
+ CENLEN: 24, // uncompressed size
+ CENNAM: 28, // filename length
+ CENEXT: 30, // extra field length
+ CENCOM: 32, // file comment length
+ CENDSK: 34, // volume number start
+ CENATT: 36, // internal file attributes
+ CENATX: 38, // external file attributes (host system dependent)
+ CENOFF: 42, // LOC header offset
+
+ /* The entries in the end of central directory */
+ ENDHDR: 22, // END header size
+ ENDSIG: 0x06054b50, // "PK\005\006"
+ ENDSIGFIRST: 0x50,
+ ENDSUB: 8, // number of entries on this disk
+ ENDTOT: 10, // total number of entries
+ ENDSIZ: 12, // central directory size in bytes
+ ENDOFF: 16, // offset of first CEN header
+ ENDCOM: 20, // zip file comment length
+ MAXFILECOMMENT: 0xffff,
+
+ /* The entries in the end of ZIP64 central directory locator */
+ ENDL64HDR: 20, // ZIP64 end of central directory locator header size
+ ENDL64SIG: 0x07064b50, // ZIP64 end of central directory locator signature
+ ENDL64SIGFIRST: 0x50,
+ ENDL64OFS: 8, // ZIP64 end of central directory offset
+
+ /* The entries in the end of ZIP64 central directory */
+ END64HDR: 56, // ZIP64 end of central directory header size
+ END64SIG: 0x06064b50, // ZIP64 end of central directory signature
+ END64SIGFIRST: 0x50,
+ END64SUB: 24, // number of entries on this disk
+ END64TOT: 32, // total number of entries
+ END64SIZ: 40,
+ END64OFF: 48,
+
+ /* Compression methods */
+ STORED: 0, // no compression
+ SHRUNK: 1, // shrunk
+ REDUCED1: 2, // reduced with compression factor 1
+ REDUCED2: 3, // reduced with compression factor 2
+ REDUCED3: 4, // reduced with compression factor 3
+ REDUCED4: 5, // reduced with compression factor 4
+ IMPLODED: 6, // imploded
+ // 7 reserved
+ DEFLATED: 8, // deflated
+ ENHANCED_DEFLATED: 9, // deflate64
+ PKWARE: 10, // PKWare DCL imploded
+ // 11 reserved
+ BZIP2: 12, // compressed using BZIP2
+ // 13 reserved
+ LZMA: 14, // LZMA
+ // 15-17 reserved
+ IBM_TERSE: 18, // compressed using IBM TERSE
+ IBM_LZ77: 19, //IBM LZ77 z
+
+ /* General purpose bit flag */
+ FLG_ENC: 0, // encrypted file
+ FLG_COMP1: 1, // compression option
+ FLG_COMP2: 2, // compression option
+ FLG_DESC: 4, // data descriptor
+ FLG_ENH: 8, // enhanced deflation
+ FLG_STR: 16, // strong encryption
+ FLG_LNG: 1024, // language encoding
+ FLG_MSK: 4096, // mask header values
+ FLG_ENTRY_ENC: 1,
+
+ /* 4.5 Extensible data fields */
+ EF_ID: 0,
+ EF_SIZE: 2,
+
+ /* Header IDs */
+ ID_ZIP64: 0x0001,
+ ID_AVINFO: 0x0007,
+ ID_PFS: 0x0008,
+ ID_OS2: 0x0009,
+ ID_NTFS: 0x000a,
+ ID_OPENVMS: 0x000c,
+ ID_UNIX: 0x000d,
+ ID_FORK: 0x000e,
+ ID_PATCH: 0x000f,
+ ID_X509_PKCS7: 0x0014,
+ ID_X509_CERTID_F: 0x0015,
+ ID_X509_CERTID_C: 0x0016,
+ ID_STRONGENC: 0x0017,
+ ID_RECORD_MGT: 0x0018,
+ ID_X509_PKCS7_RL: 0x0019,
+ ID_IBM1: 0x0065,
+ ID_IBM2: 0x0066,
+ ID_POSZIP: 0x4690,
+
+ EF_ZIP64_OR_32: 0xffffffff,
+ EF_ZIP64_OR_16: 0xffff,
+};
+
+const StreamZip = function (config) {
+ let fd, fileSize, chunkSize, op, centralDirectory, closed;
+ const ready = false,
+ that = this,
+ entries = config.storeEntries !== false ? {} : null,
+ fileName = config.file,
+ textDecoder = config.nameEncoding ? new TextDecoder(config.nameEncoding) : null;
+
+ open();
+
+ function open() {
+ if (config.fd) {
+ fd = config.fd;
+ readFile();
+ } else {
+ fs.open(fileName, 'r', (err, f) => {
+ if (err) {
+ return that.emit('error', err);
+ }
+ fd = f;
+ readFile();
+ });
+ }
+ }
+
+ function readFile() {
+ fs.fstat(fd, (err, stat) => {
+ if (err) {
+ return that.emit('error', err);
+ }
+ fileSize = stat.size;
+ chunkSize = config.chunkSize || Math.round(fileSize / 1000);
+ chunkSize = Math.max(
+ Math.min(chunkSize, Math.min(128 * 1024, fileSize)),
+ Math.min(1024, fileSize)
+ );
+ readCentralDirectory();
+ });
+ }
+
+ function readUntilFoundCallback(err, bytesRead) {
+ if (err || !bytesRead) {
+ return that.emit('error', err || new Error('Archive read error'));
+ }
+ let pos = op.lastPos;
+ let bufferPosition = pos - op.win.position;
+ const buffer = op.win.buffer;
+ const minPos = op.minPos;
+ while (--pos >= minPos && --bufferPosition >= 0) {
+ if (buffer.length - bufferPosition >= 4 && buffer[bufferPosition] === op.firstByte) {
+ // quick check first signature byte
+ if (buffer.readUInt32LE(bufferPosition) === op.sig) {
+ op.lastBufferPosition = bufferPosition;
+ op.lastBytesRead = bytesRead;
+ op.complete();
+ return;
+ }
+ }
+ }
+ if (pos === minPos) {
+ return that.emit('error', new Error('Bad archive'));
+ }
+ op.lastPos = pos + 1;
+ op.chunkSize *= 2;
+ if (pos <= minPos) {
+ return that.emit('error', new Error('Bad archive'));
+ }
+ const expandLength = Math.min(op.chunkSize, pos - minPos);
+ op.win.expandLeft(expandLength, readUntilFoundCallback);
+ }
+
+ function readCentralDirectory() {
+ const totalReadLength = Math.min(consts.ENDHDR + consts.MAXFILECOMMENT, fileSize);
+ op = {
+ win: new FileWindowBuffer(fd),
+ totalReadLength,
+ minPos: fileSize - totalReadLength,
+ lastPos: fileSize,
+ chunkSize: Math.min(1024, chunkSize),
+ firstByte: consts.ENDSIGFIRST,
+ sig: consts.ENDSIG,
+ complete: readCentralDirectoryComplete,
+ };
+ op.win.read(fileSize - op.chunkSize, op.chunkSize, readUntilFoundCallback);
+ }
+
+ function readCentralDirectoryComplete() {
+ const buffer = op.win.buffer;
+ const pos = op.lastBufferPosition;
+ try {
+ centralDirectory = new CentralDirectoryHeader();
+ centralDirectory.read(buffer.slice(pos, pos + consts.ENDHDR));
+ centralDirectory.headerOffset = op.win.position + pos;
+ if (centralDirectory.commentLength) {
+ that.comment = buffer
+ .slice(
+ pos + consts.ENDHDR,
+ pos + consts.ENDHDR + centralDirectory.commentLength
+ )
+ .toString();
+ } else {
+ that.comment = null;
+ }
+ that.entriesCount = centralDirectory.volumeEntries;
+ that.centralDirectory = centralDirectory;
+ if (
+ (centralDirectory.volumeEntries === consts.EF_ZIP64_OR_16 &&
+ centralDirectory.totalEntries === consts.EF_ZIP64_OR_16) ||
+ centralDirectory.size === consts.EF_ZIP64_OR_32 ||
+ centralDirectory.offset === consts.EF_ZIP64_OR_32
+ ) {
+ readZip64CentralDirectoryLocator();
+ } else {
+ op = {};
+ readEntries();
+ }
+ } catch (err) {
+ that.emit('error', err);
+ }
+ }
+
+ function readZip64CentralDirectoryLocator() {
+ const length = consts.ENDL64HDR;
+ if (op.lastBufferPosition > length) {
+ op.lastBufferPosition -= length;
+ readZip64CentralDirectoryLocatorComplete();
+ } else {
+ op = {
+ win: op.win,
+ totalReadLength: length,
+ minPos: op.win.position - length,
+ lastPos: op.win.position,
+ chunkSize: op.chunkSize,
+ firstByte: consts.ENDL64SIGFIRST,
+ sig: consts.ENDL64SIG,
+ complete: readZip64CentralDirectoryLocatorComplete,
+ };
+ op.win.read(op.lastPos - op.chunkSize, op.chunkSize, readUntilFoundCallback);
+ }
+ }
+
+ function readZip64CentralDirectoryLocatorComplete() {
+ const buffer = op.win.buffer;
+ const locHeader = new CentralDirectoryLoc64Header();
+ locHeader.read(
+ buffer.slice(op.lastBufferPosition, op.lastBufferPosition + consts.ENDL64HDR)
+ );
+ const readLength = fileSize - locHeader.headerOffset;
+ op = {
+ win: op.win,
+ totalReadLength: readLength,
+ minPos: locHeader.headerOffset,
+ lastPos: op.lastPos,
+ chunkSize: op.chunkSize,
+ firstByte: consts.END64SIGFIRST,
+ sig: consts.END64SIG,
+ complete: readZip64CentralDirectoryComplete,
+ };
+ op.win.read(fileSize - op.chunkSize, op.chunkSize, readUntilFoundCallback);
+ }
+
+ function readZip64CentralDirectoryComplete() {
+ const buffer = op.win.buffer;
+ const zip64cd = new CentralDirectoryZip64Header();
+ zip64cd.read(buffer.slice(op.lastBufferPosition, op.lastBufferPosition + consts.END64HDR));
+ that.centralDirectory.volumeEntries = zip64cd.volumeEntries;
+ that.centralDirectory.totalEntries = zip64cd.totalEntries;
+ that.centralDirectory.size = zip64cd.size;
+ that.centralDirectory.offset = zip64cd.offset;
+ that.entriesCount = zip64cd.volumeEntries;
+ op = {};
+ readEntries();
+ }
+
+ function readEntries() {
+ op = {
+ win: new FileWindowBuffer(fd),
+ pos: centralDirectory.offset,
+ chunkSize,
+ entriesLeft: centralDirectory.volumeEntries,
+ };
+ op.win.read(op.pos, Math.min(chunkSize, fileSize - op.pos), readEntriesCallback);
+ }
+
+ function readEntriesCallback(err, bytesRead) {
+ if (err || !bytesRead) {
+ return that.emit('error', err || new Error('Entries read error'));
+ }
+ let bufferPos = op.pos - op.win.position;
+ let entry = op.entry;
+ const buffer = op.win.buffer;
+ const bufferLength = buffer.length;
+ try {
+ while (op.entriesLeft > 0) {
+ if (!entry) {
+ entry = new ZipEntry();
+ entry.readHeader(buffer, bufferPos);
+ entry.headerOffset = op.win.position + bufferPos;
+ op.entry = entry;
+ op.pos += consts.CENHDR;
+ bufferPos += consts.CENHDR;
+ }
+ const entryHeaderSize = entry.fnameLen + entry.extraLen + entry.comLen;
+ const advanceBytes = entryHeaderSize + (op.entriesLeft > 1 ? consts.CENHDR : 0);
+ if (bufferLength - bufferPos < advanceBytes) {
+ op.win.moveRight(chunkSize, readEntriesCallback, bufferPos);
+ op.move = true;
+ return;
+ }
+ entry.read(buffer, bufferPos, textDecoder);
+ if (!config.skipEntryNameValidation) {
+ entry.validateName();
+ }
+ if (entries) {
+ entries[entry.name] = entry;
+ }
+ that.emit('entry', entry);
+ op.entry = entry = null;
+ op.entriesLeft--;
+ op.pos += entryHeaderSize;
+ bufferPos += entryHeaderSize;
+ }
+ that.emit('ready');
+ } catch (err) {
+ that.emit('error', err);
+ }
+ }
+
+ function checkEntriesExist() {
+ if (!entries) {
+ throw new Error('storeEntries disabled');
+ }
+ }
+
+ Object.defineProperty(this, 'ready', {
+ get() {
+ return ready;
+ },
+ });
+
+ this.entry = function (name) {
+ checkEntriesExist();
+ return entries[name];
+ };
+
+ this.entries = function () {
+ checkEntriesExist();
+ return entries;
+ };
+
+ this.stream = function (entry, callback) {
+ return this.openEntry(
+ entry,
+ (err, entry) => {
+ if (err) {
+ return callback(err);
+ }
+ const offset = dataOffset(entry);
+ let entryStream = new EntryDataReaderStream(fd, offset, entry.compressedSize);
+ if (entry.method === consts.STORED) {
+ // nothing to do
+ } else if (entry.method === consts.DEFLATED) {
+ entryStream = entryStream.pipe(zlib.createInflateRaw());
+ } else {
+ return callback(new Error('Unknown compression method: ' + entry.method));
+ }
+ if (canVerifyCrc(entry)) {
+ entryStream = entryStream.pipe(
+ new EntryVerifyStream(entryStream, entry.crc, entry.size)
+ );
+ }
+ callback(null, entryStream);
+ },
+ false
+ );
+ };
+
+ this.entryDataSync = function (entry) {
+ let err = null;
+ this.openEntry(
+ entry,
+ (e, en) => {
+ err = e;
+ entry = en;
+ },
+ true
+ );
+ if (err) {
+ throw err;
+ }
+ let data = Buffer.alloc(entry.compressedSize);
+ new FsRead(fd, data, 0, entry.compressedSize, dataOffset(entry), (e) => {
+ err = e;
+ }).read(true);
+ if (err) {
+ throw err;
+ }
+ if (entry.method === consts.STORED) {
+ // nothing to do
+ } else if (entry.method === consts.DEFLATED || entry.method === consts.ENHANCED_DEFLATED) {
+ data = zlib.inflateRawSync(data);
+ } else {
+ throw new Error('Unknown compression method: ' + entry.method);
+ }
+ if (data.length !== entry.size) {
+ throw new Error('Invalid size');
+ }
+ if (canVerifyCrc(entry)) {
+ const verify = new CrcVerify(entry.crc, entry.size);
+ verify.data(data);
+ }
+ return data;
+ };
+
+ this.openEntry = function (entry, callback, sync) {
+ if (typeof entry === 'string') {
+ checkEntriesExist();
+ entry = entries[entry];
+ if (!entry) {
+ return callback(new Error('Entry not found'));
+ }
+ }
+ if (!entry.isFile) {
+ return callback(new Error('Entry is not file'));
+ }
+ if (!fd) {
+ return callback(new Error('Archive closed'));
+ }
+ const buffer = Buffer.alloc(consts.LOCHDR);
+ new FsRead(fd, buffer, 0, buffer.length, entry.offset, (err) => {
+ if (err) {
+ return callback(err);
+ }
+ let readEx;
+ try {
+ entry.readDataHeader(buffer);
+ if (entry.encrypted) {
+ readEx = new Error('Entry encrypted');
+ }
+ } catch (ex) {
+ readEx = ex;
+ }
+ callback(readEx, entry);
+ }).read(sync);
+ };
+
+ function dataOffset(entry) {
+ return entry.offset + consts.LOCHDR + entry.fnameLen + entry.extraLen;
+ }
+
+ function canVerifyCrc(entry) {
+ // if bit 3 (0x08) of the general-purpose flags field is set, then the CRC-32 and file sizes are not known when the header is written
+ return (entry.flags & 0x8) !== 0x8;
+ }
+
+ function extract(entry, outPath, callback) {
+ that.stream(entry, (err, stm) => {
+ if (err) {
+ callback(err);
+ } else {
+ let fsStm, errThrown;
+ stm.on('error', (err) => {
+ errThrown = err;
+ if (fsStm) {
+ stm.unpipe(fsStm);
+ fsStm.close(() => {
+ callback(err);
+ });
+ }
+ });
+ fs.open(outPath, 'w', (err, fdFile) => {
+ if (err) {
+ return callback(err);
+ }
+ if (errThrown) {
+ fs.close(fd, () => {
+ callback(errThrown);
+ });
+ return;
+ }
+ fsStm = fs.createWriteStream(outPath, { fd: fdFile });
+ fsStm.on('finish', () => {
+ that.emit('extract', entry, outPath);
+ if (!errThrown) {
+ callback();
+ }
+ });
+ stm.pipe(fsStm);
+ });
+ }
+ });
+ }
+
+ function createDirectories(baseDir, dirs, callback) {
+ if (!dirs.length) {
+ return callback();
+ }
+ let dir = dirs.shift();
+ dir = path.join(baseDir, path.join(...dir));
+ fs.mkdir(dir, { recursive: true }, (err) => {
+ if (err && err.code !== 'EEXIST') {
+ return callback(err);
+ }
+ createDirectories(baseDir, dirs, callback);
+ });
+ }
+
+ function extractFiles(baseDir, baseRelPath, files, callback, extractedCount) {
+ if (!files.length) {
+ return callback(null, extractedCount);
+ }
+ const file = files.shift();
+ const targetPath = path.join(baseDir, file.name.replace(baseRelPath, ''));
+ extract(file, targetPath, (err) => {
+ if (err) {
+ return callback(err, extractedCount);
+ }
+ extractFiles(baseDir, baseRelPath, files, callback, extractedCount + 1);
+ });
+ }
+
+ this.extract = function (entry, outPath, callback) {
+ let entryName = entry || '';
+ if (typeof entry === 'string') {
+ entry = this.entry(entry);
+ if (entry) {
+ entryName = entry.name;
+ } else {
+ if (entryName.length && entryName[entryName.length - 1] !== '/') {
+ entryName += '/';
+ }
+ }
+ }
+ if (!entry || entry.isDirectory) {
+ const files = [],
+ dirs = [],
+ allDirs = {};
+ for (const e in entries) {
+ if (
+ Object.prototype.hasOwnProperty.call(entries, e) &&
+ e.lastIndexOf(entryName, 0) === 0
+ ) {
+ let relPath = e.replace(entryName, '');
+ const childEntry = entries[e];
+ if (childEntry.isFile) {
+ files.push(childEntry);
+ relPath = path.dirname(relPath);
+ }
+ if (relPath && !allDirs[relPath] && relPath !== '.') {
+ allDirs[relPath] = true;
+ let parts = relPath.split('/').filter((f) => {
+ return f;
+ });
+ if (parts.length) {
+ dirs.push(parts);
+ }
+ while (parts.length > 1) {
+ parts = parts.slice(0, parts.length - 1);
+ const partsPath = parts.join('/');
+ if (allDirs[partsPath] || partsPath === '.') {
+ break;
+ }
+ allDirs[partsPath] = true;
+ dirs.push(parts);
+ }
+ }
+ }
+ }
+ dirs.sort((x, y) => {
+ return x.length - y.length;
+ });
+ if (dirs.length) {
+ createDirectories(outPath, dirs, (err) => {
+ if (err) {
+ callback(err);
+ } else {
+ extractFiles(outPath, entryName, files, callback, 0);
+ }
+ });
+ } else {
+ extractFiles(outPath, entryName, files, callback, 0);
+ }
+ } else {
+ fs.stat(outPath, (err, stat) => {
+ if (stat && stat.isDirectory()) {
+ extract(entry, path.join(outPath, path.basename(entry.name)), callback);
+ } else {
+ extract(entry, outPath, callback);
+ }
+ });
+ }
+ };
+
+ this.close = function (callback) {
+ if (closed || !fd) {
+ closed = true;
+ if (callback) {
+ callback();
+ }
+ } else {
+ closed = true;
+ fs.close(fd, (err) => {
+ fd = null;
+ if (callback) {
+ callback(err);
+ }
+ });
+ }
+ };
+
+ const originalEmit = events.EventEmitter.prototype.emit;
+ this.emit = function (...args) {
+ if (!closed) {
+ return originalEmit.call(this, ...args);
+ }
+ };
+};
+
+StreamZip.setFs = function (customFs) {
+ fs = customFs;
+};
+
+StreamZip.debugLog = (...args) => {
+ if (StreamZip.debug) {
+ // eslint-disable-next-line no-console
+ console.log(...args);
+ }
+};
+
+util.inherits(StreamZip, events.EventEmitter);
+
+const propZip = Symbol('zip');
+
+StreamZip.async = class StreamZipAsync extends events.EventEmitter {
+ constructor(config) {
+ super();
+
+ const zip = new StreamZip(config);
+
+ zip.on('entry', (entry) => this.emit('entry', entry));
+ zip.on('extract', (entry, outPath) => this.emit('extract', entry, outPath));
+
+ this[propZip] = new Promise((resolve, reject) => {
+ zip.on('ready', () => {
+ zip.removeListener('error', reject);
+ resolve(zip);
+ });
+ zip.on('error', reject);
+ });
+ }
+
+ get entriesCount() {
+ return this[propZip].then((zip) => zip.entriesCount);
+ }
+
+ get comment() {
+ return this[propZip].then((zip) => zip.comment);
+ }
+
+ async entry(name) {
+ const zip = await this[propZip];
+ return zip.entry(name);
+ }
+
+ async entries() {
+ const zip = await this[propZip];
+ return zip.entries();
+ }
+
+ async stream(entry) {
+ const zip = await this[propZip];
+ return new Promise((resolve, reject) => {
+ zip.stream(entry, (err, stm) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(stm);
+ }
+ });
+ });
+ }
+
+ async entryData(entry) {
+ const stm = await this.stream(entry);
+ return new Promise((resolve, reject) => {
+ const data = [];
+ stm.on('data', (chunk) => data.push(chunk));
+ stm.on('end', () => {
+ resolve(Buffer.concat(data));
+ });
+ stm.on('error', (err) => {
+ stm.removeAllListeners('end');
+ reject(err);
+ });
+ });
+ }
+
+ async extract(entry, outPath) {
+ const zip = await this[propZip];
+ return new Promise((resolve, reject) => {
+ zip.extract(entry, outPath, (err, res) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve(res);
+ }
+ });
+ });
+ }
+
+ async close() {
+ const zip = await this[propZip];
+ return new Promise((resolve, reject) => {
+ zip.close((err) => {
+ if (err) {
+ reject(err);
+ } else {
+ resolve();
+ }
+ });
+ });
+ }
+};
+
+class CentralDirectoryHeader {
+ read(data) {
+ if (data.length !== consts.ENDHDR || data.readUInt32LE(0) !== consts.ENDSIG) {
+ throw new Error('Invalid central directory');
+ }
+ // number of entries on this volume
+ this.volumeEntries = data.readUInt16LE(consts.ENDSUB);
+ // total number of entries
+ this.totalEntries = data.readUInt16LE(consts.ENDTOT);
+ // central directory size in bytes
+ this.size = data.readUInt32LE(consts.ENDSIZ);
+ // offset of first CEN header
+ this.offset = data.readUInt32LE(consts.ENDOFF);
+ // zip file comment length
+ this.commentLength = data.readUInt16LE(consts.ENDCOM);
+ }
+}
+
+class CentralDirectoryLoc64Header {
+ read(data) {
+ if (data.length !== consts.ENDL64HDR || data.readUInt32LE(0) !== consts.ENDL64SIG) {
+ throw new Error('Invalid zip64 central directory locator');
+ }
+ // ZIP64 EOCD header offset
+ this.headerOffset = readUInt64LE(data, consts.ENDSUB);
+ }
+}
+
+class CentralDirectoryZip64Header {
+ read(data) {
+ if (data.length !== consts.END64HDR || data.readUInt32LE(0) !== consts.END64SIG) {
+ throw new Error('Invalid central directory');
+ }
+ // number of entries on this volume
+ this.volumeEntries = readUInt64LE(data, consts.END64SUB);
+ // total number of entries
+ this.totalEntries = readUInt64LE(data, consts.END64TOT);
+ // central directory size in bytes
+ this.size = readUInt64LE(data, consts.END64SIZ);
+ // offset of first CEN header
+ this.offset = readUInt64LE(data, consts.END64OFF);
+ }
+}
+
+class ZipEntry {
+ readHeader(data, offset) {
+ // data should be 46 bytes and start with "PK 01 02"
+ if (data.length < offset + consts.CENHDR || data.readUInt32LE(offset) !== consts.CENSIG) {
+ throw new Error('Invalid entry header');
+ }
+ // version made by
+ this.verMade = data.readUInt16LE(offset + consts.CENVEM);
+ // version needed to extract
+ this.version = data.readUInt16LE(offset + consts.CENVER);
+ // encrypt, decrypt flags
+ this.flags = data.readUInt16LE(offset + consts.CENFLG);
+ // compression method
+ this.method = data.readUInt16LE(offset + consts.CENHOW);
+ // modification time (2 bytes time, 2 bytes date)
+ const timebytes = data.readUInt16LE(offset + consts.CENTIM);
+ const datebytes = data.readUInt16LE(offset + consts.CENTIM + 2);
+ this.time = parseZipTime(timebytes, datebytes);
+
+ // uncompressed file crc-32 value
+ this.crc = data.readUInt32LE(offset + consts.CENCRC);
+ // compressed size
+ this.compressedSize = data.readUInt32LE(offset + consts.CENSIZ);
+ // uncompressed size
+ this.size = data.readUInt32LE(offset + consts.CENLEN);
+ // filename length
+ this.fnameLen = data.readUInt16LE(offset + consts.CENNAM);
+ // extra field length
+ this.extraLen = data.readUInt16LE(offset + consts.CENEXT);
+ // file comment length
+ this.comLen = data.readUInt16LE(offset + consts.CENCOM);
+ // volume number start
+ this.diskStart = data.readUInt16LE(offset + consts.CENDSK);
+ // internal file attributes
+ this.inattr = data.readUInt16LE(offset + consts.CENATT);
+ // external file attributes
+ this.attr = data.readUInt32LE(offset + consts.CENATX);
+ // LOC header offset
+ this.offset = data.readUInt32LE(offset + consts.CENOFF);
+ }
+
+ readDataHeader(data) {
+ // 30 bytes and should start with "PK\003\004"
+ if (data.readUInt32LE(0) !== consts.LOCSIG) {
+ throw new Error('Invalid local header');
+ }
+ // version needed to extract
+ this.version = data.readUInt16LE(consts.LOCVER);
+ // general purpose bit flag
+ this.flags = data.readUInt16LE(consts.LOCFLG);
+ // compression method
+ this.method = data.readUInt16LE(consts.LOCHOW);
+ // modification time (2 bytes time ; 2 bytes date)
+ const timebytes = data.readUInt16LE(consts.LOCTIM);
+ const datebytes = data.readUInt16LE(consts.LOCTIM + 2);
+ this.time = parseZipTime(timebytes, datebytes);
+
+ // uncompressed file crc-32 value
+ this.crc = data.readUInt32LE(consts.LOCCRC) || this.crc;
+ // compressed size
+ const compressedSize = data.readUInt32LE(consts.LOCSIZ);
+ if (compressedSize && compressedSize !== consts.EF_ZIP64_OR_32) {
+ this.compressedSize = compressedSize;
+ }
+ // uncompressed size
+ const size = data.readUInt32LE(consts.LOCLEN);
+ if (size && size !== consts.EF_ZIP64_OR_32) {
+ this.size = size;
+ }
+ // filename length
+ this.fnameLen = data.readUInt16LE(consts.LOCNAM);
+ // extra field length
+ this.extraLen = data.readUInt16LE(consts.LOCEXT);
+ }
+
+ read(data, offset, textDecoder) {
+ const nameData = data.slice(offset, (offset += this.fnameLen));
+ this.name = textDecoder
+ ? textDecoder.decode(new Uint8Array(nameData))
+ : nameData.toString('utf8');
+ const lastChar = data[offset - 1];
+ this.isDirectory = lastChar === 47 || lastChar === 92;
+
+ if (this.extraLen) {
+ this.readExtra(data, offset);
+ offset += this.extraLen;
+ }
+ this.comment = this.comLen ? data.slice(offset, offset + this.comLen).toString() : null;
+ }
+
+ validateName() {
+ if (/\\|^\w+:|^\/|(^|\/)\.\.(\/|$)/.test(this.name)) {
+ throw new Error('Malicious entry: ' + this.name);
+ }
+ }
+
+ readExtra(data, offset) {
+ let signature, size;
+ const maxPos = offset + this.extraLen;
+ while (offset < maxPos) {
+ signature = data.readUInt16LE(offset);
+ offset += 2;
+ size = data.readUInt16LE(offset);
+ offset += 2;
+ if (consts.ID_ZIP64 === signature) {
+ this.parseZip64Extra(data, offset, size);
+ }
+ offset += size;
+ }
+ }
+
+ parseZip64Extra(data, offset, length) {
+ if (length >= 8 && this.size === consts.EF_ZIP64_OR_32) {
+ this.size = readUInt64LE(data, offset);
+ offset += 8;
+ length -= 8;
+ }
+ if (length >= 8 && this.compressedSize === consts.EF_ZIP64_OR_32) {
+ this.compressedSize = readUInt64LE(data, offset);
+ offset += 8;
+ length -= 8;
+ }
+ if (length >= 8 && this.offset === consts.EF_ZIP64_OR_32) {
+ this.offset = readUInt64LE(data, offset);
+ offset += 8;
+ length -= 8;
+ }
+ if (length >= 4 && this.diskStart === consts.EF_ZIP64_OR_16) {
+ this.diskStart = data.readUInt32LE(offset);
+ // offset += 4; length -= 4;
+ }
+ }
+
+ get encrypted() {
+ return (this.flags & consts.FLG_ENTRY_ENC) === consts.FLG_ENTRY_ENC;
+ }
+
+ get isFile() {
+ return !this.isDirectory;
+ }
+}
+
+class FsRead {
+ constructor(fd, buffer, offset, length, position, callback) {
+ this.fd = fd;
+ this.buffer = buffer;
+ this.offset = offset;
+ this.length = length;
+ this.position = position;
+ this.callback = callback;
+ this.bytesRead = 0;
+ this.waiting = false;
+ }
+
+ read(sync) {
+ StreamZip.debugLog('read', this.position, this.bytesRead, this.length, this.offset);
+ this.waiting = true;
+ let err;
+ if (sync) {
+ let bytesRead = 0;
+ try {
+ bytesRead = fs.readSync(
+ this.fd,
+ this.buffer,
+ this.offset + this.bytesRead,
+ this.length - this.bytesRead,
+ this.position + this.bytesRead
+ );
+ } catch (e) {
+ err = e;
+ }
+ this.readCallback(sync, err, err ? bytesRead : null);
+ } else {
+ fs.read(
+ this.fd,
+ this.buffer,
+ this.offset + this.bytesRead,
+ this.length - this.bytesRead,
+ this.position + this.bytesRead,
+ this.readCallback.bind(this, sync)
+ );
+ }
+ }
+
+ readCallback(sync, err, bytesRead) {
+ if (typeof bytesRead === 'number') {
+ this.bytesRead += bytesRead;
+ }
+ if (err || !bytesRead || this.bytesRead === this.length) {
+ this.waiting = false;
+ return this.callback(err, this.bytesRead);
+ } else {
+ this.read(sync);
+ }
+ }
+}
+
+class FileWindowBuffer {
+ constructor(fd) {
+ this.position = 0;
+ this.buffer = Buffer.alloc(0);
+ this.fd = fd;
+ this.fsOp = null;
+ }
+
+ checkOp() {
+ if (this.fsOp && this.fsOp.waiting) {
+ throw new Error('Operation in progress');
+ }
+ }
+
+ read(pos, length, callback) {
+ this.checkOp();
+ if (this.buffer.length < length) {
+ this.buffer = Buffer.alloc(length);
+ }
+ this.position = pos;
+ this.fsOp = new FsRead(this.fd, this.buffer, 0, length, this.position, callback).read();
+ }
+
+ expandLeft(length, callback) {
+ this.checkOp();
+ this.buffer = Buffer.concat([Buffer.alloc(length), this.buffer]);
+ this.position -= length;
+ if (this.position < 0) {
+ this.position = 0;
+ }
+ this.fsOp = new FsRead(this.fd, this.buffer, 0, length, this.position, callback).read();
+ }
+
+ expandRight(length, callback) {
+ this.checkOp();
+ const offset = this.buffer.length;
+ this.buffer = Buffer.concat([this.buffer, Buffer.alloc(length)]);
+ this.fsOp = new FsRead(
+ this.fd,
+ this.buffer,
+ offset,
+ length,
+ this.position + offset,
+ callback
+ ).read();
+ }
+
+ moveRight(length, callback, shift) {
+ this.checkOp();
+ if (shift) {
+ this.buffer.copy(this.buffer, 0, shift);
+ } else {
+ shift = 0;
+ }
+ this.position += shift;
+ this.fsOp = new FsRead(
+ this.fd,
+ this.buffer,
+ this.buffer.length - shift,
+ shift,
+ this.position + this.buffer.length - shift,
+ callback
+ ).read();
+ }
+}
+
+class EntryDataReaderStream extends stream.Readable {
+ constructor(fd, offset, length) {
+ super();
+ this.fd = fd;
+ this.offset = offset;
+ this.length = length;
+ this.pos = 0;
+ this.readCallback = this.readCallback.bind(this);
+ }
+
+ _read(n) {
+ const buffer = Buffer.alloc(Math.min(n, this.length - this.pos));
+ if (buffer.length) {
+ fs.read(this.fd, buffer, 0, buffer.length, this.offset + this.pos, this.readCallback);
+ } else {
+ this.push(null);
+ }
+ }
+
+ readCallback(err, bytesRead, buffer) {
+ this.pos += bytesRead;
+ if (err) {
+ this.emit('error', err);
+ this.push(null);
+ } else if (!bytesRead) {
+ this.push(null);
+ } else {
+ if (bytesRead !== buffer.length) {
+ buffer = buffer.slice(0, bytesRead);
+ }
+ this.push(buffer);
+ }
+ }
+}
+
+class EntryVerifyStream extends stream.Transform {
+ constructor(baseStm, crc, size) {
+ super();
+ this.verify = new CrcVerify(crc, size);
+ baseStm.on('error', (e) => {
+ this.emit('error', e);
+ });
+ }
+
+ _transform(data, encoding, callback) {
+ let err;
+ try {
+ this.verify.data(data);
+ } catch (e) {
+ err = e;
+ }
+ callback(err, data);
+ }
+}
+
+class CrcVerify {
+ constructor(crc, size) {
+ this.crc = crc;
+ this.size = size;
+ this.state = {
+ crc: ~0,
+ size: 0,
+ };
+ }
+
+ data(data) {
+ const crcTable = CrcVerify.getCrcTable();
+ let crc = this.state.crc;
+ let off = 0;
+ let len = data.length;
+ while (--len >= 0) {
+ crc = crcTable[(crc ^ data[off++]) & 0xff] ^ (crc >>> 8);
+ }
+ this.state.crc = crc;
+ this.state.size += data.length;
+ if (this.state.size >= this.size) {
+ const buf = Buffer.alloc(4);
+ buf.writeInt32LE(~this.state.crc & 0xffffffff, 0);
+ crc = buf.readUInt32LE(0);
+ if (crc !== this.crc) {
+ throw new Error('Invalid CRC');
+ }
+ if (this.state.size !== this.size) {
+ throw new Error('Invalid size');
+ }
+ }
+ }
+
+ static getCrcTable() {
+ let crcTable = CrcVerify.crcTable;
+ if (!crcTable) {
+ CrcVerify.crcTable = crcTable = [];
+ const b = Buffer.alloc(4);
+ for (let n = 0; n < 256; n++) {
+ let c = n;
+ for (let k = 8; --k >= 0; ) {
+ if ((c & 1) !== 0) {
+ c = 0xedb88320 ^ (c >>> 1);
+ } else {
+ c = c >>> 1;
+ }
+ }
+ if (c < 0) {
+ b.writeInt32LE(c, 0);
+ c = b.readUInt32LE(0);
+ }
+ crcTable[n] = c;
+ }
+ }
+ return crcTable;
+ }
+}
+
+function parseZipTime(timebytes, datebytes) {
+ const timebits = toBits(timebytes, 16);
+ const datebits = toBits(datebytes, 16);
+
+ const mt = {
+ h: parseInt(timebits.slice(0, 5).join(''), 2),
+ m: parseInt(timebits.slice(5, 11).join(''), 2),
+ s: parseInt(timebits.slice(11, 16).join(''), 2) * 2,
+ Y: parseInt(datebits.slice(0, 7).join(''), 2) + 1980,
+ M: parseInt(datebits.slice(7, 11).join(''), 2),
+ D: parseInt(datebits.slice(11, 16).join(''), 2),
+ };
+ const dt_str = [mt.Y, mt.M, mt.D].join('-') + ' ' + [mt.h, mt.m, mt.s].join(':') + ' GMT+0';
+ return new Date(dt_str).getTime();
+}
+
+function toBits(dec, size) {
+ let b = (dec >>> 0).toString(2);
+ while (b.length < size) {
+ b = '0' + b;
+ }
+ return b.split('');
+}
+
+function readUInt64LE(buffer, offset) {
+ return buffer.readUInt32LE(offset + 4) * 0x0000000100000000 + buffer.readUInt32LE(offset);
+}
+
+module.exports = StreamZip;
diff --git a/App/node_modules/node-stream-zip/package.json b/App/node_modules/node-stream-zip/package.json
new file mode 100644
index 0000000..5fd74e0
--- /dev/null
+++ b/App/node_modules/node-stream-zip/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "node-stream-zip",
+ "version": "1.15.0",
+ "description": "node.js library for reading and extraction of ZIP archives",
+ "keywords": [
+ "zip",
+ "archive",
+ "unzip",
+ "stream"
+ ],
+ "homepage": "https://github.com/antelle/node-stream-zip",
+ "author": "Antelle (https://github.com/antelle)",
+ "bugs": {
+ "email": "antelle.net@gmail.com",
+ "url": "https://github.com/antelle/node-stream-zip/issues"
+ },
+ "license": "MIT",
+ "files": [
+ "LICENSE",
+ "node_stream_zip.js",
+ "node_stream_zip.d.ts"
+ ],
+ "scripts": {
+ "lint": "eslint node_stream_zip.js test/tests.js",
+ "check-types": "tsc node_stream_zip.d.ts",
+ "test": "nodeunit test/tests.js"
+ },
+ "main": "node_stream_zip.js",
+ "types": "node_stream_zip.d.ts",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/antelle/node-stream-zip.git"
+ },
+ "engines": {
+ "node": ">=0.12.0"
+ },
+ "devDependencies": {
+ "@types/node": "^14.14.6",
+ "eslint": "^7.19.0",
+ "nodeunit": "^0.11.3",
+ "prettier": "^2.2.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/antelle"
+ }
+}
diff --git a/Lang/about-translations.md b/Lang/about-translations.md
index b53ac2b..a6a9eab 100644
--- a/Lang/about-translations.md
+++ b/Lang/about-translations.md
@@ -6,4 +6,4 @@ I would like to thank everyone who contributed to the translation of this projec
- French: [Mizmalik](https://github.com/Mizmalik)
- Chinese (Simplified): [nini22P](https://github.com/nini22P)
- Russian: ThatSameGuy _(Revisions by [gandalfthewhite](https://github.com/gandalfthewhite19890404))_
-- Italian: [Dan Adrian Radut (Aka. B8nee)](https://github.com/B8nee)
+- Italian: [Dan Adrian Radut (Aka. B8nee)](https://github.com/B8nee)
\ No newline at end of file
diff --git a/Lang/fr-fr.json b/Lang/fr-fr.json
index bc39b02..ebf4402 100644
--- a/Lang/fr-fr.json
+++ b/Lang/fr-fr.json
@@ -8,7 +8,7 @@
"logWindowTitle": "Exécution de fpPS4",
"killEmuStatus": "Le processus principal a été fermé - fermez la fenêtre des logs pour continuer",
"logCleared": "INFO - La liste des journaux a été effacée!\n ",
- "about": "fpPS4 Lanceur de Temmie - Version: %VARIABLE_0%\nCréé par TemmieHeartz\n(https://twitter.com/themitosan)\n\nfpPS4 a été créé/développé par red-prig\n(https://github.com/red -prig/fpPS4)\n\nLe plugin Memoryjs a été créé/développé par Rob--\n(https://github.com/rob--/memoryjs)\n\nLes icônes SVG ont été obtenues à partir de\nhttps://www.svgrepo.com/",
+ "about": "",
"mainLog": "Lanceur de fpPS4 Temmie - Version: %VARIABLE_0%\nUtilisation de nw.js (node-webkit) version %VARIABLE_1% [%VARIABLE_2%]",
"settingsErrorCreatePath": "ERREUR - Impossible de créer le dossier!\n(%VARIABLE_0%)\n%VARIABLE_1%",
"settingsErrorfpPS4NotFound": "ERREUR - Impossible de trouver l'exécutable fpPS4!\nSélectionnez l'exécutable dans les paramètres ou placez-le dans le dossier \"Emu\" et cliquez sur OK.",
@@ -47,7 +47,18 @@
"gameListVersion": "Version",
"selectGameLoadPatchErrorParamSfo": "ERREUR - Impossible de charger PARAM.SFO de ce correctif!\n%VARIABLE_0%",
"path": "Chemin",
- "gamelistGamePath404": ""
+ "gamelistGamePath404": "",
+ "updateEmuFetchActionsError": "",
+ "updateEmuIsLatestVersion": "",
+ "updateEmuShaAvailable": "",
+ "updateEmuShaUnavailable": "",
+ "updateEmuDownloadFailed": "",
+ "updateEmuProcessComplete": "",
+ "updateEmu-1-4": "",
+ "updateEmu-2-4": "",
+ "updateEmu-3-4": "",
+ "updateEmu-4-4": "",
+ "settingsLogEmuSha": ""
},
"input_text": {
@@ -95,7 +106,10 @@
"LABEL_FPPS4_OPTIONS_LAUNCHER_OPTIONS": "Options du lanceur",
"LABEL_FPPS4_OPTIONS_HACKS": "Hacks",
"LABEL_SETTINGS_SHOW_METADATA_GUI": "",
- "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": ""
+ "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": "",
+ "DIV_SETTINGS_FPPS4_UPDATER": "",
+ "LABEL_SETTINGS_ENABLE_LAUNCHER_FPPS4_UPDATES": "",
+ "LABEL_SETTINGS_FPPS4_UPDATE_BRANCH": ""
},
@@ -131,7 +145,9 @@
"BTN_FPPS4_OPTIONS_RESET_SETTINGS": "Réinitialiser les options",
"BTN_launcherOptionsExportMetadata": "Exporter les métadonnées",
"BTN_RUN": "Démarrer fpPS4",
- "BTN_SETTINGS_RESTART_LAUNCHER": ""
+ "BTN_SETTINGS_RESTART_LAUNCHER": "",
+ "BTN_UPDATE_FPPS4": "",
+ "BTN_SETTINGS_FORCE_FPPS4_UPDATE": ""
}
}
\ No newline at end of file
diff --git a/Lang/it-it.json b/Lang/it-it.json
index fe21ed9..11fd4ad 100644
--- a/Lang/it-it.json
+++ b/Lang/it-it.json
@@ -8,7 +8,7 @@
"logWindowTitle": "Eseguendo fpPS4",
"killEmuStatus": "Il processo principale è stato chiuso: chiudi la finestra del log per continuare",
"logCleared": "INFO - L'elenco dei log è stato cancellato!\n",
- "about": "fpPS4 Temmie's Launcher - Versione: %VARIABLE_0%\nCreato da TemmieHeartz\n(https://twitter.com/themitosan)\n\nfpPS4 è stato creato/sviluppato da red-prig\n(https://github.com/red-prig/fpPS4)\n\nIl plugin Memoryjs è stato creato/sviluppato da Rob--\n(https://github.com/rob--/memoryjs)\n\nLe icone SVG sono state ottenute da https://www.svgrepo.com/",
+ "about": "",
"mainLog": "fpPS4 Temmie's Launcher - Versione: %VARIABLE_0%\nUsando nw.js (node-webkit) versione %VARIABLE_1% [%VARIABLE_2%]",
"settingsErrorCreatePath": "ERRORE - Impossibile creare la cartella!\n(%VARIABLE_0%)\n%VARIABLE_1%",
"settingsErrorfpPS4NotFound": "ERRORE - Impossibile trovare l'eseguibile fpPS4!\nSelezionare l'eseguibile nelle impostazioni o posizionarlo all'interno della cartella \"Emu\" e fare clic su ok.",
@@ -47,7 +47,18 @@
"gameListVersion": "Versione",
"selectGameLoadPatchErrorParamSfo": "ERRORE - Impossibile caricare PARAM.SFO di questa patch!\n%VARIABLE_0%",
"path": "Percorso",
- "gamelistGamePath404": "INFO - La cartella app/giochi selezionata non esiste!\n%VARIABLE_0%"
+ "gamelistGamePath404": "INFO - La cartella app/giochi selezionata non esiste!\n%VARIABLE_0%",
+ "updateEmuFetchActionsError": "",
+ "updateEmuIsLatestVersion": "",
+ "updateEmuShaAvailable": "",
+ "updateEmuShaUnavailable": "",
+ "updateEmuDownloadFailed": "",
+ "updateEmuProcessComplete": "",
+ "updateEmu-1-4": "",
+ "updateEmu-2-4": "",
+ "updateEmu-3-4": "",
+ "updateEmu-4-4": "",
+ "settingsLogEmuSha": ""
},
"input_text": {
@@ -95,7 +106,10 @@
"LABEL_FPPS4_OPTIONS_LAUNCHER_OPTIONS": "Opzioni del launcher",
"LABEL_FPPS4_OPTIONS_HACKS": "Hacks",
"LABEL_SETTINGS_SHOW_METADATA_GUI": "Mostra l'icona e il nome dell'app/gioco durante l'emulazione",
- "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": "SPERIMENTALE Visualizza l'output dell'emulatore (stdout
e stderr
) nel log interno (premi F12 --> Console)"
+ "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": "SPERIMENTALE Visualizza l'output dell'emulatore (stdout
e stderr
) nel log interno (premi F12 --> Console)",
+ "DIV_SETTINGS_FPPS4_UPDATER": "",
+ "LABEL_SETTINGS_ENABLE_LAUNCHER_FPPS4_UPDATES": "",
+ "LABEL_SETTINGS_FPPS4_UPDATE_BRANCH": ""
},
@@ -131,7 +145,9 @@
"BTN_FPPS4_OPTIONS_RESET_SETTINGS": "Ripristina le impostazioni",
"BTN_launcherOptionsExportMetadata": "Esportare i metadati",
"BTN_RUN": "Avvia fpPS4",
- "BTN_SETTINGS_RESTART_LAUNCHER": "Riavvia il launcher"
+ "BTN_SETTINGS_RESTART_LAUNCHER": "Riavvia il launcher",
+ "BTN_UPDATE_FPPS4": "",
+ "BTN_SETTINGS_FORCE_FPPS4_UPDATE": ""
}
}
diff --git a/Lang/pt-br.json b/Lang/pt-br.json
index b773264..80acf8b 100644
--- a/Lang/pt-br.json
+++ b/Lang/pt-br.json
@@ -8,7 +8,7 @@
"logWindowTitle": "Executando fpPS4",
"killEmuStatus": "Processo principal foi fechado - feche a janela do log para continuar",
"logCleared": "INFO - A lista de log foi limpa!\n ",
- "about": "fpPS4 Temmie's Launcher - Versão: %VARIABLE_0%\nCriado por TemmieHeartz\n(https://twitter.com/themitosan)\n\nfpPS4 foi criado / desenvolvido por red-prig\n(https://github.com/red-prig/fpPS4)\n\nPlugin memoryjs foi criado / desenvolvido por Rob--\n(https://github.com/rob--/memoryjs)\n\nIcones SVG foram obtidos através do site https://www.svgrepo.com/",
+ "about": "fpPS4 Temmie's Launcher - Versão: %VARIABLE_0%\nCriado por TemmieHeartz\n(https://twitter.com/themitosan)\n\nfpPS4 foi criado / desenvolvido por red-prig\n(https://github.com/red-prig/fpPS4)\n\nPlugin memoryjs foi criado / desenvolvido por Rob--\n(https://github.com/rob--/memoryjs)\n\nPlugin node-stream-zip foi criado / desenvolvido por antelle\n(https://github.com/antelle/node-stream-zip)\n\nÍcones SVG foram obtidos através do site https://www.svgrepo.com/",
"mainLog": "fpPS4 Temmie's Launcher - Versão: %VARIABLE_0%\nUsando nw.js (node-webkit) versão %VARIABLE_1% [%VARIABLE_2%]",
"settingsErrorCreatePath": "ERRO - Não foi possível criar a pasta!\n(%VARIABLE_0%)\n%VARIABLE_1%",
"settingsErrorfpPS4NotFound": "ERRO - Não foi possível encontrar o executável do fpPS4!\nSelecione o executável nas configurações ou coloque ele dentro da pasta \"Emu\" e clique em ok.",
@@ -47,7 +47,18 @@
"gameListVersion": "Versão",
"selectGameLoadPatchErrorParamSfo": "ERRO - Não foi possível carregar PARAM.SFO desse patch!\n%VARIABLE_0%",
"path": "Caminho",
- "gamelistGamePath404": "INFO - A pasta de app / games selecionada não existe!\n%VARIABLE_0%"
+ "gamelistGamePath404": "INFO - A pasta de app / games selecionada não existe!\n%VARIABLE_0%",
+ "updateEmuFetchActionsError": "ERRO - Não foi possível obter informações do GitHub Actions!",
+ "updateEmuIsLatestVersion": "INFO - Você já está usando a versão mais recente!\nCommit ID (SHA): %VARIABLE_0%",
+ "updateEmuShaAvailable": "INFO - Uma nova atualização está disponível!\n\nVersão local: %VARIABLE_0%\nNova versão: %VARIABLE_1%\n\nVocê gostaria de atualizar?",
+ "updateEmuShaUnavailable": "INFO - O launcher detectou que nenhuma atualização foi feita\n(Ou o executável do fpPS4 não foi encontrado!)\n\nÉ possível corrigir esse problema usando o procedimento de atualização automática.\n\nVocê deseja prosseguir?",
+ "updateEmuDownloadFailed": "ERRO - Não foi possível baixar a atualização do fpPS4!\nStatus de resposta: %VARIABLE_0% - OK: %VARIABLE_1%",
+ "updateEmuProcessComplete": "INFO - Update concluído!\nNova versão (Commit ID / SHA): %VARIABLE_0%",
+ "updateEmu-1-4": "Baixando update do fpPS4 (%VARIABLE_0%)",
+ "updateEmu-2-4": "Extraíndo update",
+ "updateEmu-3-4": "Removendo arquivos de sobra",
+ "updateEmu-4-4": "Update concluído!",
+ "settingsLogEmuSha": "INFO - Versão do fpPS4: (%VARIABLE_0%)"
},
"input_text": {
@@ -95,7 +106,10 @@
"LABEL_FPPS4_OPTIONS_LAUNCHER_OPTIONS": "Opções do Launcher",
"LABEL_FPPS4_OPTIONS_HACKS": "Hacks",
"LABEL_SETTINGS_SHOW_METADATA_GUI": "Mostrar ícone e nome do app / game durante a emulação",
- "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": "EXPERIMENTAL Exibir output do emulador (stdout
e stderr
) no log interno (Aperte F12 --> Console)"
+ "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": "EXPERIMENTAL Exibir output do emulador (stdout
e stderr
) no log interno (Aperte F12 --> Console)",
+ "DIV_SETTINGS_FPPS4_UPDATER": "Atualizações do fpPS4",
+ "LABEL_SETTINGS_ENABLE_LAUNCHER_FPPS4_UPDATES": "Habilitar atualizador do fpPS4",
+ "LABEL_SETTINGS_FPPS4_UPDATE_BRANCH": "Obter atualizações da branch"
},
@@ -131,7 +145,9 @@
"BTN_FPPS4_OPTIONS_RESET_SETTINGS": "Resetar configurações",
"BTN_launcherOptionsExportMetadata": "Exportar metadados",
"BTN_RUN": "Iniciar fpPS4",
- "BTN_SETTINGS_RESTART_LAUNCHER": "Reiniciar launcher"
+ "BTN_SETTINGS_RESTART_LAUNCHER": "Reiniciar launcher",
+ "BTN_UPDATE_FPPS4": "Atualizar fpPS4",
+ "BTN_SETTINGS_FORCE_FPPS4_UPDATE": "Fazer atualização forçada"
}
}
\ No newline at end of file
diff --git a/Lang/ru-ru.json b/Lang/ru-ru.json
index a08fc5e..58d058c 100644
--- a/Lang/ru-ru.json
+++ b/Lang/ru-ru.json
@@ -8,7 +8,7 @@
"logWindowTitle": "Запуск fpPS4",
"killEmuStatus": "Основной процесс был закрыт - закройте окно лога для продолжения работы",
"logCleared": "ИНФО - Список логов был очищен!\n ",
- "about": "fpPS4 Temmie's Launcher - Версия: %VARIABLE_0%\nСоздатель - TemmieHeartz\n(https://twitter.com/themitosan)\n\nfpPS4, создатель - red-prig\n(https://github.com/red-prig/fpPS4)\n\nплагин memoryjs - разработан: Rob--\n(https://github.com/rob--/memoryjs)\n\nИконки SVG были получены с https://www.svgrepo.com/",
+ "about": "",
"mainLog": "fpPS4 Temmie's Launcher - Версия: %VARIABLE_0%\nИспользование nw.js (node-webkit) версия %VARIABLE_1% [%VARIABLE_2%]",
"settingsErrorCreatePath": "ОШИБКА - Папка не может быть создана!\n(%VARIABLE_0%)\n%VARIABLE_1%",
"settingsErrorfpPS4NotFound": "ОШИБКА - Не удалось найти исполняемый файл fpPS4!\nВыберите исполняемый файл в настройках или поместите его внутрь \"Emu\" и нажмите OK.",
@@ -47,7 +47,18 @@
"gameListVersion": "Версия",
"selectGameLoadPatchErrorParamSfo": "ОШИБКА - Невозможно загрузить PARAM.SFO из этого патча!\n%VARIABLE_0%",
"path": "Путь",
- "gamelistGamePath404": ""
+ "gamelistGamePath404": "",
+ "updateEmuFetchActionsError": "",
+ "updateEmuIsLatestVersion": "",
+ "updateEmuShaAvailable": "",
+ "updateEmuShaUnavailable": "",
+ "updateEmuDownloadFailed": "",
+ "updateEmuProcessComplete": "",
+ "updateEmu-1-4": "",
+ "updateEmu-2-4": "",
+ "updateEmu-3-4": "",
+ "updateEmu-4-4": "",
+ "settingsLogEmuSha": ""
},
"input_text": {
@@ -95,7 +106,10 @@
"LABEL_FPPS4_OPTIONS_LAUNCHER_OPTIONS": "Параметры запуска",
"LABEL_FPPS4_OPTIONS_HACKS": "Хаки",
"LABEL_SETTINGS_SHOW_METADATA_GUI": "",
- "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": ""
+ "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": "",
+ "DIV_SETTINGS_FPPS4_UPDATER": "",
+ "LABEL_SETTINGS_ENABLE_LAUNCHER_FPPS4_UPDATES": "",
+ "LABEL_SETTINGS_FPPS4_UPDATE_BRANCH": ""
},
@@ -131,7 +145,9 @@
"BTN_FPPS4_OPTIONS_RESET_SETTINGS": "Сброс настроек",
"BTN_launcherOptionsExportMetadata": "Экспорт метаданных",
"BTN_RUN": "Запустить",
- "BTN_SETTINGS_RESTART_LAUNCHER": ""
+ "BTN_SETTINGS_RESTART_LAUNCHER": "",
+ "BTN_UPDATE_FPPS4": "",
+ "BTN_SETTINGS_FORCE_FPPS4_UPDATE": ""
}
}
diff --git a/Lang/zh-s.json b/Lang/zh-s.json
index d85b01b..4fd5e07 100644
--- a/Lang/zh-s.json
+++ b/Lang/zh-s.json
@@ -8,7 +8,7 @@
"logWindowTitle": "Running fpPS4",
"killEmuStatus": "主进程被关闭 - 关闭日志窗口以继续",
"logCleared": "INFO - 日志已被清除!\n ",
- "about": "fpPS4 Temmie's Launcher - 版本号: %VARIABLE_0%\n由 TemmieHeartz 创建\n(https://twitter.com/themitosan)\n\nfpPS4 由 red-prig 创建\n(https://github.com/red-prig/fpPS4)\n\nmemoryjs 插件由 Rob-- 创建\n(https://github.com/rob--/memoryjs)\n\nSVG 图标来自 https://www.svgrepo.com/",
+ "about": "",
"mainLog": "fpPS4 Temmie's Launcher - 版本号: %VARIABLE_0%\n运行中的 nw.js (node-webkit) 版本号: %VARIABLE_1% [%VARIABLE_2%]",
"settingsErrorCreatePath": "ERROR - 无法创建文件夹!\n(%VARIABLE_0%)\n%VARIABLE_1%",
"settingsErrorfpPS4NotFound": "ERROR - 无法找到 fpPS4 的可执行文件!\n在设置中选择可执行文件或将其放在 \"Emu\" 文件夹中,然后点击确定。",
@@ -47,7 +47,18 @@
"gameListVersion": "版本号",
"selectGameLoadPatchErrorParamSfo": "ERROR - 无法从这个补丁中加载 PARAM.SFO!\n%VARIABLE_0%",
"path": "路径",
- "gamelistGamePath404": ""
+ "gamelistGamePath404": "",
+ "updateEmuFetchActionsError": "",
+ "updateEmuIsLatestVersion": "",
+ "updateEmuShaAvailable": "",
+ "updateEmuShaUnavailable": "",
+ "updateEmuDownloadFailed": "",
+ "updateEmuProcessComplete": "",
+ "updateEmu-1-4": "",
+ "updateEmu-2-4": "",
+ "updateEmu-3-4": "",
+ "updateEmu-4-4": "",
+ "settingsLogEmuSha": ""
},
"input_text": {
@@ -95,8 +106,10 @@
"LABEL_FPPS4_OPTIONS_LAUNCHER_OPTIONS": "启动器选项",
"LABEL_FPPS4_OPTIONS_HACKS": "Hacks",
"LABEL_SETTINGS_SHOW_METADATA_GUI": "在界面上显示图标和标题",
- "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": "实验性功能 在内部控制台显示 fpPS4 进程日志 (stdout
和 stderr
) (按 F12 --> Console)"
-
+ "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": "实验性功能 在内部控制台显示 fpPS4 进程日志 (stdout
和 stderr
) (按 F12 --> Console)",
+ "DIV_SETTINGS_FPPS4_UPDATER": "",
+ "LABEL_SETTINGS_ENABLE_LAUNCHER_FPPS4_UPDATES": "",
+ "LABEL_SETTINGS_FPPS4_UPDATE_BRANCH": ""
},
@@ -132,7 +145,9 @@
"BTN_FPPS4_OPTIONS_RESET_SETTINGS": "重置设置",
"BTN_launcherOptionsExportMetadata": "导出元数据",
"BTN_RUN": "运行 fpPS4",
- "BTN_SETTINGS_RESTART_LAUNCHER": "重新启动启动器"
+ "BTN_SETTINGS_RESTART_LAUNCHER": "重新启动启动器",
+ "BTN_UPDATE_FPPS4": "",
+ "BTN_SETTINGS_FORCE_FPPS4_UPDATE": ""
}
}
diff --git a/README.md b/README.md
index f29c6b8..c552100 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ If you dump your game using memoryjs - created by Rob--
-- TMS.js by TemmieHeartz (hi!)
+- node-stream-zip - created by antelle
+- TMS.js by TemmieHeartz (Hi!)
-IMPORTANT: This software does not allow you to obtain free PS4 Games / Apps.
+IMPORTANT: This software does not allow you to obtain free PS4 Games / Apps.
\ No newline at end of file
diff --git a/package.json b/package.json
index cdf18c5..e780d70 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"width": 1186,
"height": 710,
"toolbar": true,
- "min_width": 1062,
+ "min_width": 1102,
"min_height": 626,
"fullscreen": false,
"position": "center",