|
|
@@ -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
|
|
|
}
|