From 95cf06000f4282d1d1609ef00cec761dbb87df3e Mon Sep 17 00:00:00 2001 From: TemmieHeartz Date: Mon, 16 Jan 2023 21:19:29 -0300 Subject: [PATCH] General: Implement fpPS4 updater [NEED TESTING], Css: Various fixes, Emu: Fixed not being able to close fpPS4 using stop fpPS4 button and more! --- App/css/style.css | 96 +- App/img/logo.ico | Bin 0 -> 139659 bytes App/index.htm | 432 +++--- App/js/design.js | 67 +- App/js/emumanager.js | 7 +- App/js/language.js | 18 +- App/js/main.js | 6 + App/js/settings.js | 22 +- App/js/updateEmu.js | 260 ++++ App/node_modules/node-stream-zip/LICENSE | 44 + App/node_modules/node-stream-zip/README.md | 224 +++ .../node-stream-zip/node_stream_zip.d.ts | 199 +++ .../node-stream-zip/node_stream_zip.js | 1210 +++++++++++++++++ App/node_modules/node-stream-zip/package.json | 47 + Lang/about-translations.md | 2 +- Lang/fr-fr.json | 24 +- Lang/it-it.json | 24 +- Lang/pt-br.json | 24 +- Lang/ru-ru.json | 24 +- Lang/zh-s.json | 25 +- README.md | 5 +- package.json | 2 +- 22 files changed, 2513 insertions(+), 249 deletions(-) create mode 100644 App/img/logo.ico create mode 100644 App/js/updateEmu.js create mode 100644 App/node_modules/node-stream-zip/LICENSE create mode 100644 App/node_modules/node-stream-zip/README.md create mode 100644 App/node_modules/node-stream-zip/node_stream_zip.d.ts create mode 100644 App/node_modules/node-stream-zip/node_stream_zip.js create mode 100644 App/node_modules/node-stream-zip/package.json diff --git a/App/css/style.css b/App/css/style.css index 63645e8..6d305c1 100644 --- a/App/css/style.css +++ b/App/css/style.css @@ -6,6 +6,7 @@ Main application stylesheet ****************************************************************************** */ + html, body { color: #fff; overflow: hidden; @@ -33,6 +34,10 @@ input[type='range'] { input[disabled='disabled'], input[disabled] { cursor: no-drop; } +input[type='checkbox'] { + margin-right: 8px; + vertical-align: middle; +} img { -webkit-user-drag: none; } @@ -318,7 +323,7 @@ img { text-align: center; border-radius: 10px; box-shadow: 0px 0px 30px #000a; - background-image: linear-gradient(180deg, #2a3a56, #0d1932); + background-image: linear-gradient(180deg, #2f405f, #131f38); } .DIV_SETTINGS_BG { top: 0px; @@ -352,7 +357,7 @@ img { text-align: left; margin-left: 8px; margin-bottom: 8px; - border-radius: 6px; + border-radius: 4px; font-family: sans-serif; background-color: #111d; width: calc(100% - 32px); @@ -368,14 +373,14 @@ img { } .DIV_settingsEntryFlex { display: flex; - flex-wrap: nowrap; align-items: center; + flex-direction: row; align-content: center; } .DIV_settingsH2 { font-size: 20px; - font-style: italic; - font-family: monospace; + font-weight: 600; + font-family: system-ui; margin: 0px 0px 8px 2px; } .DIV_settingsSave { @@ -439,6 +444,58 @@ img { margin-bottom: 2px; font-family: system-ui; } +.DIV_FPPS4_UPDATER { + top: 0px; + left: 0px; + width: 100%; + height: 100%; + z-index: 110; + cursor: wait; + display: none; + transition: 0.4s; + flex-wrap: nowrap; + position: absolute; + align-items: center; + flex-direction: row; + align-content: center; + font-family: system-ui; + justify-content: center; + backdrop-filter: blur(4px); + background-color: #03033144; +} +.DIV_PROGRESSBAR { + width: 72%; + height: 10px; + cursor: wait; + overflow: hidden; + margin-top: 44px; + border-width: 1px; + position: absolute; + border-style: solid; + background-color: #c5c5c5ad; +} +.DIV_PROGRESSBAR_INTERNAL { + top: 0px; + left: 0px; + width: 0%; + height: 100%; + transition: 0.4s; + position: absolute; + background-image: linear-gradient(90deg, #ccc, #fff); +} +.DIV_DESIGN_LINES { + left: 0px; + width: 100%; + height: 1px; + position: absolute; + background-color: #fffb; +} +.LINE_TOP { + top: 12%; +} +.LINE_BOTTOM { + bottom: 12%; +} /* Images @@ -542,12 +599,18 @@ img { } .BTN_STOP { margin: 0px 4px 0px 4px; - padding: 1px 70px 1px 70px; + padding: 1px 30px 1px 30px; } .BTN_SAVE { + border: none; font-size: 18px; min-width: 300px; min-height: 60px; + border-radius: 6px; + background-image: linear-gradient(180deg, #fff, #ccc); +} +.BTN_SAVE:active { + background-image: linear-gradient(0deg, #fff, #ccc); } .BTN_selectPath { float: right; @@ -568,6 +631,17 @@ img { padding: 4px 10px 4px 10px; background-image: linear-gradient(180deg, #fff, #bbb); } +.SETTINGS_TEXT { + color: #0f0; + border: none; + outline: none; + min-width: 160px; + margin-left: 4px; + border-radius: 4px; + background-color: #000; + font-family: monospace; + padding: 6px 0px 6px 6px; +} /* Labels @@ -586,7 +660,7 @@ img { } .LABEL_checkbox { cursor: pointer; - font-size: 16px; + font-size: 14px; font-style: italic; } .LABEL_emuColor { @@ -610,10 +684,16 @@ img { .LABEL_settingsExperimental { padding: 6px; cursor: pointer; - font-family: math; + font-family: monospace; background-color: #000; text-shadow: 0px 0px 10px #f00; } +.LABEL_FLEX_MARGIN { + margin: 4px; +} +.LABEL_monospace { + font-family: monospace; +} /* Animations diff --git a/App/img/logo.ico b/App/img/logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..33d66f2f5e2bd1520e1659f16a3218b9e8da2f7d GIT binary patch literal 139659 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 + +
+ + +
+
+ + + + + + +
+
+
+ +
+
-
-
Launcher Settings
- - -
- - -
- - -
Language
- -
- : -
- -
-
- - -
Paths
-
- - : - - - - - -
- -
- - : - - - - -
- - -
-
Game List
- -
+
+
Launcher Settings
+ + +
+ + +
- : - - -
- -
- - : - - - % - -
- -
- - : - - % - -
- -
+ +
Language
- : +
+ : +
+ () +
+
+ + +
Paths
+
+ + : + + + + + +
- - % +
+ + : + + + + +
+ + +
+
Game List
+ +
+ + : + + +
+ +
+ + : + + + % + +
+ +
+ + : + + % + +
+ +
+ + : + + + % + +
+ + +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
Emu Running
+ +
+ : + + % +
+ +
+ : + + % +
+ +
+ + +
+ +
+ + +
+ + +
+
Log Options
+ +
+ : + +
+ +
+ + +
+ +
+ + +
+ +
+
fpPS4 Updater
+ +
+ + +
+ +
+ : + +
+ +
+ +
+ + +
+
Misc.
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
- - -
- - -
- -
- - -
- -
- - -
- - -
-
Emu Running
- -
- : - - % -
- -
- : - - % -
- -
- - -
- -
- - -
- - -
-
Log Options
- -
- : - -
- -
- - -
- -
- - -
- - -
-
Misc.
- -
- - -
- -
- - -
- -
- - + + +
+ +
- -
- - -
- -
+
@@ -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 ()", + "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": " Visualizza l'output dell'emulatore (stdout e stderr) nel log interno (premi F12 --> Console)" + "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": " 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 ()", + "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": " Exibir output do emulador (stdout e stderr) no log interno (Aperte F12 --> Console)" + "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": " 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 进程日志 (stdoutstderr) (按 F12 --> Console)" - + "LABEL_SETTINGS_EXPERIMENTAL_FPPS4_INTERNAL_LOG": " 在内部控制台显示 fpPS4 进程日志 (stdoutstderr) (按 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",