Selaa lähdekoodia

Break up assetguard.

Daniel Scalzi 6 vuotta sitten
vanhempi
sitoutus
5c0a293390

+ 10 - 4
app/assets/js/assetexec.js

@@ -1,8 +1,14 @@
-const { AssetGuard } = require('./assetguard')
+let target = require('./assetguard')[process.argv[2]]
+if(target == null){
+    process.send({context: 'error', data: null, error: 'Invalid class name'})
+    console.error('Invalid class name passed to argv[2], cannot continue.')
+    process.exit(1)
+}
+let tracker = new target(...(process.argv.splice(3)))
 
 process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
 
-const tracker = new AssetGuard(process.argv[2], process.argv[3])
+//const tracker = new AssetGuard(process.argv[2], process.argv[3])
 console.log('AssetExec Started')
 
 // Temporary for debug purposes.
@@ -24,8 +30,8 @@ tracker.on('error', (data, error) => {
 process.on('message', (msg) => {
     if(msg.task === 'execute'){
         const func = msg.function
-        let nS = tracker[func]
-        let iS = AssetGuard[func]
+        let nS = tracker[func] // Nonstatic context
+        let iS = target[func] // Static context
         if(typeof nS === 'function' || typeof iS === 'function'){
             const f = typeof nS === 'function' ? nS : iS
             const res = f.apply(f === nS ? tracker : null, msg.argsArr)

+ 293 - 286
app/assets/js/assetguard.js

@@ -145,246 +145,37 @@ class DLTracker {
 
 }
 
-/**
- * Central object class used for control flow. This object stores data about
- * categories of downloads. Each category is assigned an identifier with a 
- * DLTracker object as its value. Combined information is also stored, such as
- * the total size of all the queued files in each category. This event is used
- * to emit events so that external modules can listen into processing done in
- * this module.
- */
-class AssetGuard extends EventEmitter {
-
-    /**
-     * Create an instance of AssetGuard.
-     * On creation the object's properties are never-null default
-     * values. Each identifier is resolved to an empty DLTracker.
-     * 
-     * @param {string} commonPath The common path for shared game files.
-     * @param {string} javaexec The path to a java executable which will be used
-     * to finalize installation.
-     */
-    constructor(commonPath, javaexec){
-        super()
-        this.totaldlsize = 0
-        this.progress = 0
-        this.assets = new DLTracker([], 0)
-        this.libraries = new DLTracker([], 0)
-        this.files = new DLTracker([], 0)
-        this.forge = new DLTracker([], 0)
-        this.java = new DLTracker([], 0)
-        this.extractQueue = []
-        this.commonPath = commonPath
-        this.javaexec = javaexec
-    }
-
-    // Static Utility Functions
-    // #region
-
-    // Static Hash Validation Functions
-    // #region
-
-    /**
-     * Calculates the hash for a file using the specified algorithm.
-     * 
-     * @param {Buffer} buf The buffer containing file data.
-     * @param {string} algo The hash algorithm.
-     * @returns {string} The calculated hash in hex.
-     */
-    static _calculateHash(buf, algo){
-        return crypto.createHash(algo).update(buf).digest('hex')
-    }
-
-    /**
-     * Used to parse a checksums file. This is specifically designed for
-     * the checksums.sha1 files found inside the forge scala dependencies.
-     * 
-     * @param {string} content The string content of the checksums file.
-     * @returns {Object} An object with keys being the file names, and values being the hashes.
-     */
-    static _parseChecksumsFile(content){
-        let finalContent = {}
-        let lines = content.split('\n')
-        for(let i=0; i<lines.length; i++){
-            let bits = lines[i].split(' ')
-            if(bits[1] == null) {
-                continue
-            }
-            finalContent[bits[1]] = bits[0]
-        }
-        return finalContent
-    }
-
-    /**
-     * Validate that a file exists and matches a given hash value.
-     * 
-     * @param {string} filePath The path of the file to validate.
-     * @param {string} algo The hash algorithm to check against.
-     * @param {string} hash The existing hash to check against.
-     * @returns {boolean} True if the file exists and calculated hash matches the given hash, otherwise false.
-     */
-    static _validateLocal(filePath, algo, hash){
-        if(fs.existsSync(filePath)){
-            //No hash provided, have to assume it's good.
-            if(hash == null){
-                return true
-            }
-            let buf = fs.readFileSync(filePath)
-            let calcdhash = AssetGuard._calculateHash(buf, algo)
-            return calcdhash === hash
-        }
-        return false
-    }
-
-    /**
-     * Validates a file in the style used by forge's version index.
-     * 
-     * @param {string} filePath The path of the file to validate.
-     * @param {Array.<string>} checksums The checksums listed in the forge version index.
-     * @returns {boolean} True if the file exists and the hashes match, otherwise false.
-     */
-    static _validateForgeChecksum(filePath, checksums){
-        if(fs.existsSync(filePath)){
-            if(checksums == null || checksums.length === 0){
-                return true
-            }
-            let buf = fs.readFileSync(filePath)
-            let calcdhash = AssetGuard._calculateHash(buf, 'sha1')
-            let valid = checksums.includes(calcdhash)
-            if(!valid && filePath.endsWith('.jar')){
-                valid = AssetGuard._validateForgeJar(filePath, checksums)
-            }
-            return valid
-        }
-        return false
-    }
+class Util {
 
     /**
-     * Validates a forge jar file dependency who declares a checksums.sha1 file.
-     * This can be an expensive task as it usually requires that we calculate thousands
-     * of hashes.
+     * Returns true if the actual version is greater than
+     * or equal to the desired version.
      * 
-     * @param {Buffer} buf The buffer of the jar file.
-     * @param {Array.<string>} checksums The checksums listed in the forge version index.
-     * @returns {boolean} True if all hashes declared in the checksums.sha1 file match the actual hashes.
+     * @param {string} desired The desired version.
+     * @param {string} actual The actual version.
      */
-    static _validateForgeJar(buf, checksums){
-        // Double pass method was the quickest I found. I tried a version where we store data
-        // to only require a single pass, plus some quick cleanup but that seemed to take slightly more time.
-
-        const hashes = {}
-        let expected = {}
-
-        const zip = new AdmZip(buf)
-        const zipEntries = zip.getEntries()
-
-        //First pass
-        for(let i=0; i<zipEntries.length; i++){
-            let entry = zipEntries[i]
-            if(entry.entryName === 'checksums.sha1'){
-                expected = AssetGuard._parseChecksumsFile(zip.readAsText(entry))
-            }
-            hashes[entry.entryName] = AssetGuard._calculateHash(entry.getData(), 'sha1')
-        }
-
-        if(!checksums.includes(hashes['checksums.sha1'])){
-            return false
-        }
+    static mcVersionAtLeast(desired, actual){
+        const des = desired.split('.')
+        const act = actual.split('.')
 
-        //Check against expected
-        const expectedEntries = Object.keys(expected)
-        for(let i=0; i<expectedEntries.length; i++){
-            if(expected[expectedEntries[i]] !== hashes[expectedEntries[i]]){
+        for(let i=0; i<des.length; i++){
+            if(!(parseInt(act[i]) >= parseInt(des[i]))){
                 return false
             }
         }
         return true
     }
 
-    // #endregion
-
-    // Miscellaneous Static Functions
-    // #region
+}
 
-    /**
-     * Extracts and unpacks a file from .pack.xz format.
-     * 
-     * @param {Array.<string>} filePaths The paths of the files to be extracted and unpacked.
-     * @returns {Promise.<void>} An empty promise to indicate the extraction has completed.
-     */
-    static _extractPackXZ(filePaths, javaExecutable){
-        console.log('[PackXZExtract] Starting')
-        return new Promise((resolve, reject) => {
 
-            let libPath
-            if(isDev){
-                libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar')
-            } else {
-                if(process.platform === 'darwin'){
-                    libPath = path.join(process.cwd(),'Contents', 'Resources', 'libraries', 'java', 'PackXZExtract.jar')
-                } else {
-                    libPath = path.join(process.cwd(), 'resources', 'libraries', 'java', 'PackXZExtract.jar')
-                }
-            }
+class JavaGuard extends EventEmitter {
 
-            const filePath = filePaths.join(',')
-            const child = child_process.spawn(javaExecutable, ['-jar', libPath, '-packxz', filePath])
-            child.stdout.on('data', (data) => {
-                console.log('[PackXZExtract]', data.toString('utf8'))
-            })
-            child.stderr.on('data', (data) => {
-                console.log('[PackXZExtract]', data.toString('utf8'))
-            })
-            child.on('close', (code, signal) => {
-                console.log('[PackXZExtract]', 'Exited with code', code)
-                resolve()
-            })
-        })
-    }
-
-    /**
-     * Function which finalizes the forge installation process. This creates a 'version'
-     * instance for forge and saves its version.json file into that instance. If that
-     * instance already exists, the contents of the version.json file are read and returned
-     * in a promise.
-     * 
-     * @param {Asset} asset The Asset object representing Forge.
-     * @param {string} commonPath The common path for shared game files.
-     * @returns {Promise.<Object>} A promise which resolves to the contents of forge's version.json.
-     */
-    static _finalizeForgeAsset(asset, commonPath){
-        return new Promise((resolve, reject) => {
-            fs.readFile(asset.to, (err, data) => {
-                const zip = new AdmZip(data)
-                const zipEntries = zip.getEntries()
-
-                for(let i=0; i<zipEntries.length; i++){
-                    if(zipEntries[i].entryName === 'version.json'){
-                        const forgeVersion = JSON.parse(zip.readAsText(zipEntries[i]))
-                        const versionPath = path.join(commonPath, 'versions', forgeVersion.id)
-                        const versionFile = path.join(versionPath, forgeVersion.id + '.json')
-                        if(!fs.existsSync(versionFile)){
-                            fs.ensureDirSync(versionPath)
-                            fs.writeFileSync(path.join(versionPath, forgeVersion.id + '.json'), zipEntries[i].getData())
-                            resolve(forgeVersion)
-                        } else {
-                            //Read the saved file to allow for user modifications.
-                            resolve(JSON.parse(fs.readFileSync(versionFile, 'utf-8')))
-                        }
-                        return
-                    }
-                }
-                //We didn't find forge's version.json.
-                reject('Unable to finalize Forge processing, version.json not found! Has forge changed their format?')
-            })
-        })
+    constructor(mcVersion){
+        super()
+        this.mcVersion = mcVersion
     }
 
-    // #endregion
-
-    // Static Java Utility
-    // #region
-
     /**
      * @typedef OracleJREData
      * @property {string} uri The base uri of the JRE.
@@ -484,9 +275,9 @@ class AssetGuard extends EventEmitter {
     static parseJavaRuntimeVersion(verString){
         const major = verString.split('.')[0]
         if(major == 1){
-            return AssetGuard._parseJavaRuntimeVersion_8(verString)
+            return JavaGuard._parseJavaRuntimeVersion_8(verString)
         } else {
-            return AssetGuard._parseJavaRuntimeVersion_9(verString)
+            return JavaGuard._parseJavaRuntimeVersion_9(verString)
         }
     }
 
@@ -529,36 +320,16 @@ class AssetGuard extends EventEmitter {
         return ret
     }
 
-    /**
-     * Returns true if the actual version is greater than
-     * or equal to the desired version.
-     * 
-     * @param {string} desired The desired version.
-     * @param {string} actual The actual version.
-     */
-    static mcVersionAtLeast(desired, actual){
-        const des = desired.split('.')
-        const act = actual.split('.')
-
-        for(let i=0; i<des.length; i++){
-            if(!(parseInt(act[i]) >= parseInt(des[i]))){
-                return false
-            }
-        }
-        return true
-    }
-
     /**
      * Validates the output of a JVM's properties. Currently validates that a JRE is x64
      * and that the major = 8, update > 52.
      * 
      * @param {string} stderr The output to validate.
-     * @param {string} mcVersion The minecraft version we are scanning for.
      * 
      * @returns {Promise.<Object>} A promise which resolves to a meta object about the JVM.
      * The validity is stored inside the `valid` property.
      */
-    static _validateJVMProperties(stderr, mcVersion){
+    _validateJVMProperties(stderr){
         const res = stderr
         const props = res.split('\n')
 
@@ -582,7 +353,7 @@ class AssetGuard extends EventEmitter {
             } else if(props[i].indexOf('java.runtime.version') > -1){
                 let verString = props[i].split('=')[1].trim()
                 console.log(props[i].trim())
-                const verOb = AssetGuard.parseJavaRuntimeVersion(verString)
+                const verOb = JavaGuard.parseJavaRuntimeVersion(verString)
                 if(verOb.major < 9){
                     // Java 8
                     if(verOb.major === 8 && verOb.update > 52){
@@ -594,7 +365,7 @@ class AssetGuard extends EventEmitter {
                     }
                 } else {
                     // Java 9+
-                    if(AssetGuard.mcVersionAtLeast('1.13', mcVersion)){
+                    if(Util.mcVersionAtLeast('1.13', this.mcVersion)){
                         console.log('Java 9+ not yet tested.')
                         /* meta.version = verOb
                         ++checksum
@@ -620,21 +391,20 @@ class AssetGuard extends EventEmitter {
      * removed.
      * 
      * @param {string} binaryExecPath Path to the java executable we wish to validate.
-     * @param {string} mcVersion The minecraft version we are scanning for.
      * 
      * @returns {Promise.<Object>} A promise which resolves to a meta object about the JVM.
      * The validity is stored inside the `valid` property.
      */
-    static _validateJavaBinary(binaryExecPath, mcVersion){
+    _validateJavaBinary(binaryExecPath){
 
         return new Promise((resolve, reject) => {
-            if(!AssetGuard.isJavaExecPath(binaryExecPath)){
+            if(!JavaGuard.isJavaExecPath(binaryExecPath)){
                 resolve({valid: false})
             } else if(fs.existsSync(binaryExecPath)){
                 child_process.exec('"' + binaryExecPath + '" -XshowSettings:properties', (err, stdout, stderr) => {
                     try {
                         // Output is stored in stderr?
-                        resolve(this._validateJVMProperties(stderr, mcVersion))
+                        resolve(this._validateJVMProperties(stderr))
                     } catch (err){
                         // Output format might have changed, validation cannot be completed.
                         resolve({valid: false})
@@ -782,7 +552,7 @@ class AssetGuard extends EventEmitter {
     static _scanInternetPlugins(){
         // /Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java
         const pth = '/Library/Internet Plug-Ins/JavaAppletPlugin.plugin'
-        const res = fs.existsSync(AssetGuard.javaExecFromRoot(pth))
+        const res = fs.existsSync(JavaGuard.javaExecFromRoot(pth))
         return res ? pth : null
     }
 
@@ -811,7 +581,7 @@ class AssetGuard extends EventEmitter {
                             for(let i=0; i<files.length; i++){
 
                                 const combinedPath = path.join(scanDir, files[i])
-                                const execPath = AssetGuard.javaExecFromRoot(combinedPath)
+                                const execPath = JavaGuard.javaExecFromRoot(combinedPath)
 
                                 fs.exists(execPath, (v) => {
 
@@ -843,19 +613,18 @@ class AssetGuard extends EventEmitter {
     /**
      * 
      * @param {Set.<string>} rootSet A set of JVM root strings to validate.
-     * @param {string} mcVersion The minecraft version we are scanning for.
      * @returns {Promise.<Object[]>} A promise which resolves to an array of meta objects
      * for each valid JVM root directory.
      */
-    static async _validateJavaRootSet(rootSet, mcVersion){
+    async _validateJavaRootSet(rootSet){
 
         const rootArr = Array.from(rootSet)
         const validArr = []
 
         for(let i=0; i<rootArr.length; i++){
 
-            const execPath = AssetGuard.javaExecFromRoot(rootArr[i])
-            const metaOb = await AssetGuard._validateJavaBinary(execPath, mcVersion)
+            const execPath = JavaGuard.javaExecFromRoot(rootArr[i])
+            const metaOb = await this._validateJavaBinary(execPath)
 
             if(metaOb.valid){
                 metaOb.execPath = execPath
@@ -937,33 +706,32 @@ class AssetGuard extends EventEmitter {
      * If versions are equal, JRE > JDK.
      * 
      * @param {string} dataDir The base launcher directory.
-     * @param {string} mcVersion The minecraft version we are scanning for.
      * @returns {Promise.<string>} A Promise which resolves to the executable path of a valid 
      * x64 Java installation. If none are found, null is returned.
      */
-    static async _win32JavaValidate(dataDir, mcVersion){
+    async _win32JavaValidate(dataDir){
 
         // Get possible paths from the registry.
-        let pathSet1 = await AssetGuard._scanRegistry()
+        let pathSet1 = await JavaGuard._scanRegistry()
         if(pathSet1.length === 0){
             // Do a manual file system scan of program files.
-            pathSet1 = AssetGuard._scanFileSystem('C:\\Program Files\\Java')
+            pathSet1 = JavaGuard._scanFileSystem('C:\\Program Files\\Java')
         }
 
         // Get possible paths from the data directory.
-        const pathSet2 = await AssetGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
+        const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
 
         // Merge the results.
         const uberSet = new Set([...pathSet1, ...pathSet2])
 
         // Validate JAVA_HOME.
-        const jHome = AssetGuard._scanJavaHome()
+        const jHome = JavaGuard._scanJavaHome()
         if(jHome != null && jHome.indexOf('(x86)') === -1){
             uberSet.add(jHome)
         }
 
-        let pathArr = await AssetGuard._validateJavaRootSet(uberSet, mcVersion)
-        pathArr = AssetGuard._sortValidJavaArray(pathArr)
+        let pathArr = await this._validateJavaRootSet(uberSet)
+        pathArr = JavaGuard._sortValidJavaArray(pathArr)
 
         if(pathArr.length > 0){
             return pathArr[0].execPath
@@ -983,25 +751,24 @@ class AssetGuard extends EventEmitter {
      * If versions are equal, JRE > JDK.
      * 
      * @param {string} dataDir The base launcher directory.
-     * @param {string} mcVersion The minecraft version we are scanning for.
      * @returns {Promise.<string>} A Promise which resolves to the executable path of a valid 
      * x64 Java installation. If none are found, null is returned.
      */
-    static async _darwinJavaValidate(dataDir, mcVersion){
+    async _darwinJavaValidate(dataDir){
 
-        const pathSet1 = await AssetGuard._scanFileSystem('/Library/Java/JavaVirtualMachines')
-        const pathSet2 = await AssetGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
+        const pathSet1 = await JavaGuard._scanFileSystem('/Library/Java/JavaVirtualMachines')
+        const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
 
         const uberSet = new Set([...pathSet1, ...pathSet2])
 
         // Check Internet Plugins folder.
-        const iPPath = AssetGuard._scanInternetPlugins()
+        const iPPath = JavaGuard._scanInternetPlugins()
         if(iPPath != null){
             uberSet.add(iPPath)
         }
 
         // Check the JAVA_HOME environment variable.
-        let jHome = AssetGuard._scanJavaHome()
+        let jHome = JavaGuard._scanJavaHome()
         if(jHome != null){
             // Ensure we are at the absolute root.
             if(jHome.contains('/Contents/Home')){
@@ -1010,8 +777,8 @@ class AssetGuard extends EventEmitter {
             uberSet.add(jHome)
         }
 
-        let pathArr = await AssetGuard._validateJavaRootSet(uberSet, mcVersion)
-        pathArr = AssetGuard._sortValidJavaArray(pathArr)
+        let pathArr = await this._validateJavaRootSet(uberSet)
+        pathArr = JavaGuard._sortValidJavaArray(pathArr)
 
         if(pathArr.length > 0){
             return pathArr[0].execPath
@@ -1029,25 +796,24 @@ class AssetGuard extends EventEmitter {
      * If versions are equal, JRE > JDK.
      * 
      * @param {string} dataDir The base launcher directory.
-     * @param {string} mcVersion The minecraft version we are scanning for.
      * @returns {Promise.<string>} A Promise which resolves to the executable path of a valid 
      * x64 Java installation. If none are found, null is returned.
      */
-    static async _linuxJavaValidate(dataDir, mcVersion){
+    async _linuxJavaValidate(dataDir){
 
-        const pathSet1 = await AssetGuard._scanFileSystem('/usr/lib/jvm')
-        const pathSet2 = await AssetGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
+        const pathSet1 = await JavaGuard._scanFileSystem('/usr/lib/jvm')
+        const pathSet2 = await JavaGuard._scanFileSystem(path.join(dataDir, 'runtime', 'x64'))
         
         const uberSet = new Set([...pathSet1, ...pathSet2])
 
         // Validate JAVA_HOME
-        const jHome = AssetGuard._scanJavaHome()
+        const jHome = JavaGuard._scanJavaHome()
         if(jHome != null){
             uberSet.add(jHome)
         }
         
-        let pathArr = await AssetGuard._validateJavaRootSet(uberSet, mcVersion)
-        pathArr = AssetGuard._sortValidJavaArray(pathArr)
+        let pathArr = await this._validateJavaRootSet(uberSet)
+        pathArr = JavaGuard._sortValidJavaArray(pathArr)
 
         if(pathArr.length > 0){
             return pathArr[0].execPath
@@ -1060,11 +826,250 @@ class AssetGuard extends EventEmitter {
      * Retrieve the path of a valid x64 Java installation.
      * 
      * @param {string} dataDir The base launcher directory.
-     * @param {string} mcVersion The minecraft version we are scanning for.
      * @returns {string} A path to a valid x64 Java installation, null if none found.
      */
-    static async validateJava(dataDir, mcVersion){
-        return await AssetGuard['_' + process.platform + 'JavaValidate'](dataDir, mcVersion)
+    async validateJava(dataDir){
+        return await this['_' + process.platform + 'JavaValidate'](dataDir)
+    }
+
+}
+
+
+
+
+/**
+ * Central object class used for control flow. This object stores data about
+ * categories of downloads. Each category is assigned an identifier with a 
+ * DLTracker object as its value. Combined information is also stored, such as
+ * the total size of all the queued files in each category. This event is used
+ * to emit events so that external modules can listen into processing done in
+ * this module.
+ */
+class AssetGuard extends EventEmitter {
+
+    /**
+     * Create an instance of AssetGuard.
+     * On creation the object's properties are never-null default
+     * values. Each identifier is resolved to an empty DLTracker.
+     * 
+     * @param {string} commonPath The common path for shared game files.
+     * @param {string} javaexec The path to a java executable which will be used
+     * to finalize installation.
+     */
+    constructor(commonPath, javaexec){
+        super()
+        this.totaldlsize = 0
+        this.progress = 0
+        this.assets = new DLTracker([], 0)
+        this.libraries = new DLTracker([], 0)
+        this.files = new DLTracker([], 0)
+        this.forge = new DLTracker([], 0)
+        this.java = new DLTracker([], 0)
+        this.extractQueue = []
+        this.commonPath = commonPath
+        this.javaexec = javaexec
+    }
+
+    // Static Utility Functions
+    // #region
+
+    // Static Hash Validation Functions
+    // #region
+
+    /**
+     * Calculates the hash for a file using the specified algorithm.
+     * 
+     * @param {Buffer} buf The buffer containing file data.
+     * @param {string} algo The hash algorithm.
+     * @returns {string} The calculated hash in hex.
+     */
+    static _calculateHash(buf, algo){
+        return crypto.createHash(algo).update(buf).digest('hex')
+    }
+
+    /**
+     * Used to parse a checksums file. This is specifically designed for
+     * the checksums.sha1 files found inside the forge scala dependencies.
+     * 
+     * @param {string} content The string content of the checksums file.
+     * @returns {Object} An object with keys being the file names, and values being the hashes.
+     */
+    static _parseChecksumsFile(content){
+        let finalContent = {}
+        let lines = content.split('\n')
+        for(let i=0; i<lines.length; i++){
+            let bits = lines[i].split(' ')
+            if(bits[1] == null) {
+                continue
+            }
+            finalContent[bits[1]] = bits[0]
+        }
+        return finalContent
+    }
+
+    /**
+     * Validate that a file exists and matches a given hash value.
+     * 
+     * @param {string} filePath The path of the file to validate.
+     * @param {string} algo The hash algorithm to check against.
+     * @param {string} hash The existing hash to check against.
+     * @returns {boolean} True if the file exists and calculated hash matches the given hash, otherwise false.
+     */
+    static _validateLocal(filePath, algo, hash){
+        if(fs.existsSync(filePath)){
+            //No hash provided, have to assume it's good.
+            if(hash == null){
+                return true
+            }
+            let buf = fs.readFileSync(filePath)
+            let calcdhash = AssetGuard._calculateHash(buf, algo)
+            return calcdhash === hash
+        }
+        return false
+    }
+
+    /**
+     * Validates a file in the style used by forge's version index.
+     * 
+     * @param {string} filePath The path of the file to validate.
+     * @param {Array.<string>} checksums The checksums listed in the forge version index.
+     * @returns {boolean} True if the file exists and the hashes match, otherwise false.
+     */
+    static _validateForgeChecksum(filePath, checksums){
+        if(fs.existsSync(filePath)){
+            if(checksums == null || checksums.length === 0){
+                return true
+            }
+            let buf = fs.readFileSync(filePath)
+            let calcdhash = AssetGuard._calculateHash(buf, 'sha1')
+            let valid = checksums.includes(calcdhash)
+            if(!valid && filePath.endsWith('.jar')){
+                valid = AssetGuard._validateForgeJar(filePath, checksums)
+            }
+            return valid
+        }
+        return false
+    }
+
+    /**
+     * Validates a forge jar file dependency who declares a checksums.sha1 file.
+     * This can be an expensive task as it usually requires that we calculate thousands
+     * of hashes.
+     * 
+     * @param {Buffer} buf The buffer of the jar file.
+     * @param {Array.<string>} checksums The checksums listed in the forge version index.
+     * @returns {boolean} True if all hashes declared in the checksums.sha1 file match the actual hashes.
+     */
+    static _validateForgeJar(buf, checksums){
+        // Double pass method was the quickest I found. I tried a version where we store data
+        // to only require a single pass, plus some quick cleanup but that seemed to take slightly more time.
+
+        const hashes = {}
+        let expected = {}
+
+        const zip = new AdmZip(buf)
+        const zipEntries = zip.getEntries()
+
+        //First pass
+        for(let i=0; i<zipEntries.length; i++){
+            let entry = zipEntries[i]
+            if(entry.entryName === 'checksums.sha1'){
+                expected = AssetGuard._parseChecksumsFile(zip.readAsText(entry))
+            }
+            hashes[entry.entryName] = AssetGuard._calculateHash(entry.getData(), 'sha1')
+        }
+
+        if(!checksums.includes(hashes['checksums.sha1'])){
+            return false
+        }
+
+        //Check against expected
+        const expectedEntries = Object.keys(expected)
+        for(let i=0; i<expectedEntries.length; i++){
+            if(expected[expectedEntries[i]] !== hashes[expectedEntries[i]]){
+                return false
+            }
+        }
+        return true
+    }
+
+    // #endregion
+
+    // Miscellaneous Static Functions
+    // #region
+
+    /**
+     * Extracts and unpacks a file from .pack.xz format.
+     * 
+     * @param {Array.<string>} filePaths The paths of the files to be extracted and unpacked.
+     * @returns {Promise.<void>} An empty promise to indicate the extraction has completed.
+     */
+    static _extractPackXZ(filePaths, javaExecutable){
+        console.log('[PackXZExtract] Starting')
+        return new Promise((resolve, reject) => {
+
+            let libPath
+            if(isDev){
+                libPath = path.join(process.cwd(), 'libraries', 'java', 'PackXZExtract.jar')
+            } else {
+                if(process.platform === 'darwin'){
+                    libPath = path.join(process.cwd(),'Contents', 'Resources', 'libraries', 'java', 'PackXZExtract.jar')
+                } else {
+                    libPath = path.join(process.cwd(), 'resources', 'libraries', 'java', 'PackXZExtract.jar')
+                }
+            }
+
+            const filePath = filePaths.join(',')
+            const child = child_process.spawn(javaExecutable, ['-jar', libPath, '-packxz', filePath])
+            child.stdout.on('data', (data) => {
+                console.log('[PackXZExtract]', data.toString('utf8'))
+            })
+            child.stderr.on('data', (data) => {
+                console.log('[PackXZExtract]', data.toString('utf8'))
+            })
+            child.on('close', (code, signal) => {
+                console.log('[PackXZExtract]', 'Exited with code', code)
+                resolve()
+            })
+        })
+    }
+
+    /**
+     * Function which finalizes the forge installation process. This creates a 'version'
+     * instance for forge and saves its version.json file into that instance. If that
+     * instance already exists, the contents of the version.json file are read and returned
+     * in a promise.
+     * 
+     * @param {Asset} asset The Asset object representing Forge.
+     * @param {string} commonPath The common path for shared game files.
+     * @returns {Promise.<Object>} A promise which resolves to the contents of forge's version.json.
+     */
+    static _finalizeForgeAsset(asset, commonPath){
+        return new Promise((resolve, reject) => {
+            fs.readFile(asset.to, (err, data) => {
+                const zip = new AdmZip(data)
+                const zipEntries = zip.getEntries()
+
+                for(let i=0; i<zipEntries.length; i++){
+                    if(zipEntries[i].entryName === 'version.json'){
+                        const forgeVersion = JSON.parse(zip.readAsText(zipEntries[i]))
+                        const versionPath = path.join(commonPath, 'versions', forgeVersion.id)
+                        const versionFile = path.join(versionPath, forgeVersion.id + '.json')
+                        if(!fs.existsSync(versionFile)){
+                            fs.ensureDirSync(versionPath)
+                            fs.writeFileSync(path.join(versionPath, forgeVersion.id + '.json'), zipEntries[i].getData())
+                            resolve(forgeVersion)
+                        } else {
+                            //Read the saved file to allow for user modifications.
+                            resolve(JSON.parse(fs.readFileSync(versionFile, 'utf-8')))
+                        }
+                        return
+                    }
+                }
+                //We didn't find forge's version.json.
+                reject('Unable to finalize Forge processing, version.json not found! Has forge changed their format?')
+            })
+        })
     }
 
     // #endregion
@@ -1401,7 +1406,7 @@ class AssetGuard extends EventEmitter {
             for(let ob of modules){
                 const type = ob.getType()
                 if(type === DistroManager.Types.ForgeHosted || type === DistroManager.Types.Forge){
-                    if(AssetGuard.mcVersionAtLeast('1.13', server.getMinecraftVersion())){
+                    if(Util.mcVersionAtLeast('1.13', server.getMinecraftVersion())){
                         for(let sub of ob.getSubModules()){
                             if(sub.getType() === DistroManager.Types.VersionManifest){
                                 const versionFile = path.join(self.commonPath, 'versions', sub.getIdentifier(), `${sub.getIdentifier()}.json`)
@@ -1748,7 +1753,9 @@ class AssetGuard extends EventEmitter {
 }
 
 module.exports = {
+    Util,
     AssetGuard,
+    JavaGuard,
     Asset,
     Library
 }

+ 4 - 4
app/assets/js/processbuilder.js

@@ -6,7 +6,7 @@ const os                    = require('os')
 const path                  = require('path')
 const { URL }               = require('url')
 
-const { AssetGuard, Library }  = require('./assetguard')
+const { Util, Library }  = require('./assetguard')
 const ConfigManager            = require('./configmanager')
 const DistroManager            = require('./distromanager')
 const LoggerUtil               = require('./loggerutil')
@@ -43,7 +43,7 @@ class ProcessBuilder {
         const modObj = this.resolveModConfiguration(ConfigManager.getModConfiguration(this.server.getID()).mods, this.server.getModules())
         
         // Mod list below 1.13
-        if(!AssetGuard.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
+        if(!Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
             this.constructModList('forge', modObj.fMods, true)
             if(this.usingLiteLoader){
                 this.constructModList('liteloader', modObj.lMods, true)
@@ -53,7 +53,7 @@ class ProcessBuilder {
         const uberModArr = modObj.fMods.concat(modObj.lMods)
         let args = this.constructJVMArguments(uberModArr, tempNativePath)
 
-        if(AssetGuard.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
+        if(Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
             args = args.concat(this.constructModArguments(modObj.fMods))
         }
 
@@ -273,7 +273,7 @@ class ProcessBuilder {
      * @returns {Array.<string>} An array containing the full JVM arguments for this process.
      */
     constructJVMArguments(mods, tempNativePath){
-        if(AssetGuard.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
+        if(Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
             return this._constructJVMArguments113(mods, tempNativePath)
         } else {
             return this._constructJVMArguments112(mods, tempNativePath)

+ 6 - 4
app/assets/js/scripts/landing.js

@@ -96,7 +96,8 @@ document.getElementById('launch_button').addEventListener('click', function(e){
         toggleLaunchArea(true)
         setLaunchPercentage(0, 100)
 
-        AssetGuard._validateJavaBinary(jExe, mcVersion).then((v) => {
+        const jg = new JavaGuard(mcVersion)
+        jg._validateJavaBinary(jExe).then((v) => {
             loggerLanding.log('Java version meta', v)
             if(v.valid){
                 dlAsync()
@@ -297,8 +298,8 @@ function asyncSystemScan(mcVersion, launchAfter = true){
 
     // Fork a process to run validations.
     sysAEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [
-        ConfigManager.getCommonDirectory(),
-        ConfigManager.getJavaExecutable()
+        'JavaGuard',
+        mcVersion
     ], {
         env: forkEnv,
         stdio: 'pipe'
@@ -452,7 +453,7 @@ function asyncSystemScan(mcVersion, launchAfter = true){
 
     // Begin system Java scan.
     setLaunchDetails('Checking system info..')
-    sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getDataDirectory(), mcVersion]})
+    sysAEx.send({task: 'execute', function: 'validateJava', argsArr: [ConfigManager.getDataDirectory()]})
 
 }
 
@@ -496,6 +497,7 @@ function dlAsync(login = true){
 
     // Start AssetExec to run validations and downloads in a forked process.
     aEx = cp.fork(path.join(__dirname, 'assets', 'js', 'assetexec.js'), [
+        'AssetGuard',
         ConfigManager.getCommonDirectory(),
         ConfigManager.getJavaExecutable()
     ], {

+ 4 - 3
app/assets/js/scripts/settings.js

@@ -2,7 +2,7 @@
 const os     = require('os')
 const semver = require('semver')
 
-const { AssetGuard } = require('./assets/js/assetguard')
+const { JavaGuard } = require('./assets/js/assetguard')
 const DropinModUtil  = require('./assets/js/dropinmodutil')
 
 const settingsState = {
@@ -1117,7 +1117,8 @@ function populateMemoryStatus(){
  * @param {string} execPath The executable path to populate against.
  */
 function populateJavaExecDetails(execPath){
-    AssetGuard._validateJavaBinary(execPath).then(v => {
+    const jg = new JavaGuard(DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion())
+    jg._validateJavaBinary(execPath).then(v => {
         if(v.valid){
             if(v.version.major < 9) {
                 settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major} Update ${v.version.update} (x${v.arch})`
@@ -1326,4 +1327,4 @@ function prepareSettings(first = false) {
 }
 
 // Prepare the settings UI on startup.
-prepareSettings(true)
+//prepareSettings(true)

+ 1 - 0
app/assets/js/scripts/uibinder.js

@@ -61,6 +61,7 @@ function showMainUI(data){
         ipcRenderer.send('autoUpdateAction', 'initAutoUpdater', ConfigManager.getAllowPrerelease())
     }
 
+    prepareSettings(true)
     updateSelectedServer(data.getServer(ConfigManager.getSelectedServer()))
     refreshServerStatus()
     setTimeout(() => {