diff --git a/helpers/app-files-scanner.js b/helpers/app-files-scanner.js index f10b9a4..b5540cd 100644 --- a/helpers/app-files-scanner.js +++ b/helpers/app-files-scanner.js @@ -2,7 +2,8 @@ import plist from 'plist' import parseMacho from './macho/index.js' -// console.log('MachOParser', MachOParser) +const prettyBytes = require('pretty-bytes') + const knownArchiveExtensions = new Set([ 'app', @@ -43,6 +44,16 @@ function isValidHttpUrl( string ) { return url.protocol === "http:" || url.protocol === "https:" } +function callWithTimeout(timeout, func) { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error("timeout")), timeout) + func().then( + response => resolve(response), + err => reject(new Error(err)) + ).finally(() => clearTimeout(timer)) + }) +} + let zip export default class AppFilesScanner { @@ -270,6 +281,204 @@ export default class AppFilesScanner { // Each file scanned: Filename, Type(Drop or URL), File URL, Datetime, Architectures, Mach-o Meta } + async scanFile ( file, scanIndex ) { + + // If we've already scanned this + // then skip + if ( file.status === 'finished' ) return + + if ( !this.isApp( file ) ) { + file.statusMessage = '⏭ Skipped. Not app or archive' + file.status = 'finished' + + return + } + + // console.log('file', file) + + await new Promise(r => setTimeout(r, 1500 * scanIndex)) + + file.statusMessage = '🗃 Decompressing file' + console.log(`Decompressing file at ${ file.size }`) + + let entries + + try { + entries = await this.unzipFile( file ) + } catch ( Error ) { + // console.warn( Error ) + + // Set status message as error + file.statusMessage = `❔ ${ Error.message }` + file.status = 'finished' + + return + } + + file.statusMessage = '👀 Scanning App Files' + console.log(`Searching entries`) + + const foundEntries = this.findEntries( entries, { + macho: this.matchesMacho, + rootInfo: this.matchesRootInfo + }) + + // Clean out entries now that we're done with them + entries = undefined + + // console.log('foundEntries', foundEntries) + + // file.machOEntries = this.findMachOEntries( entries ) + file.machOEntries = foundEntries.macho + + // If no Macho files were found + // then report and stop + if ( file.machOEntries.length === 0 ) { + console.log(`No Macho files found for ${file.name}`, file.machOEntries) + + file.statusMessage = `❔ Unkown app format` + file.status = 'finished' + + return + } + + // Warn if Info.plist doesn't look right + if ( foundEntries.rootInfo.length > 1) { + console.warn('More than one root Info.plist found', foundEntries.rootInfo) + } else if ( foundEntries.rootInfo.length === 0 ) { + console.warn('No root Info.plist found', foundEntries.rootInfo) + } + + // Break out root entry into a variable + const [ rootInfoEntry ] = foundEntries.rootInfo + + // Get blob data from zip + // https://gildas-lormeau.github.io/zip.js/core-api.html#zip-entry + const infoXml = await rootInfoEntry.getData( + // writer + // https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing + new zip.TextWriter(), + // options + { + useWebWorkers: true, + // onprogress: (index, max) => { + + // const percentageNumber = (index / max * 100) + // // onprogress callback + // console.log(`Writer progress ${percentageNumber}`) + // } + } + ) + + // console.log('infoXml', infoXml) + + // Parse the Info.plist data + const info = plist.parse( infoXml ) + + file.appVersion = info.CFBundleShortVersionString + file.displayName = info.CFBundleDisplayName + + // Set details + const detailsData = [ + [ 'Version', info.CFBundleShortVersionString ], + [ 'Bundle Identifier', info.CFBundleIdentifier ], + [ 'File Mime Type', file.type ], + [ 'Copyright', info.NSHumanReadableCopyright ], + // [ 'Version', info.CFBundleShortVersionString ], + ] + + detailsData.forEach( ([ label, value ]) => { + if ( !value || value.length === 0 ) return + + file.details.push({ + label, + value, + }) + } ) + + // console.log('infoFiles', file.name, { + // path: rootInfoEntry.filename, + // info + // }) + + + console.log(`Parsing Macho ${ file.machOEntries.length } files`) + + const parsedMachoEntries = await Promise.all( file.machOEntries.map( async ( machOEntry, machEntryIndex ) => { + console.log('Parsing ', machOEntry.filename, machOEntry.uncompressedSize / 1000 ) + + if ( machEntryIndex === 0 ) { + file.displayBinarySize = prettyBytes( machOEntry.uncompressedSize ) + file.binarySize = machOEntry.uncompressedSize + } + + // Get blob data from zip + // https://gildas-lormeau.github.io/zip.js/core-api.html#zip-entry + const machOBlob = await machOEntry.getData( + // writer + // https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing + // new zip.TextWriter(), + new zip.BlobWriter(), + // options + { + useWebWorkers: true, + // onprogress: (index, max) => { + // const percentageNumber = (index / max * 100) + // // onprogress callback + // console.log(`Writer progress ${percentageNumber}`) + // } + } + ) + + return await this.parseMachOBlob( machOBlob, file.name ) + } ) ) + + // console.log('parsedMachoEntries', parsedMachoEntries) + + // file.statusMessage = `🏁 Scan Finished. ${file.machOEntries.length} Mach-o files` + // file.statusMessage = `🏁 Scan Finished. ` + console.log(`Searching ${ parsedMachoEntries.length } binaries for architecture info`) + + + let supportedBinaries = 0 + let unsupportedBinaries = 0 + + // Count supported and unsupported binaries + parsedMachoEntries.forEach( binaryEntry => { + const armBinary = binaryEntry.architectures.find( architecture => { + if ( architecture.processorType === 0 ) return false + + return architecture.processorType.toLowerCase().includes('arm') + }) + + if ( armBinary !== undefined ) { + supportedBinaries++ + } else { + unsupportedBinaries++ + } + } ) + + console.log(`Found ${ supportedBinaries } supportedBinaries and ${unsupportedBinaries} unsupportedBinaries`) + + // console.log('supportedBinaries', supportedBinaries) + // console.log('unsupportedBinaries', unsupportedBinaries) + + if (supportedBinaries !== 0 && unsupportedBinaries !== 0) { + file.statusMessage = `🔶 App has some support. ` + } else if ( unsupportedBinaries !== 0 ) { + file.statusMessage = `🔶 This app file is not natively compatible with Apple Silicon and may only run via Rosetta 2 translation, however, software vendors will sometimes will ship separate install files for Intel and ARM instead of a single one. ` + } else if ( supportedBinaries !== 0 ) { + file.statusMessage = '✅ This app is natively compatible with Apple Silicon!' + + // Shift this scan to the top + this.files.unshift( this.files.splice( scanIndex, 1 )[0] ) + } + + file.status = 'finished' + + return + } + async scan ( fileList ) { // Push files to our files array @@ -280,6 +489,9 @@ export default class AppFilesScanner { statusMessage: '⏳ File Loaded and Queud', details: [], appVersion: null, + displayAppSize: prettyBytes( fileInstance.size ), + displayBinarySize: null, + binarySize: null, name: fileInstance.name, size: fileInstance.size, @@ -290,191 +502,24 @@ export default class AppFilesScanner { } ) }) + const scanTimeoutSeconds = 30 + // Scan for archives - await Promise.all( this.files.map( async (file, scanIndex) => { + await Promise.all( this.files.map( ( file, scanIndex ) => { + return new Promise( (resolve, reject) => { - // If we've already scanned this - // then skip - if ( file.status === 'finished' ) return + const timer = setTimeout(() => { + file.statusMessage = '❔ Scan timed out' + file.status = 'finished' - if ( !this.isApp( file ) ) { - file.statusMessage = '⏭ Skipped. Not app or archive' - file.status = 'finished' + reject(new Error('Scan timed out')) + }, scanTimeoutSeconds * 1000) - return - } - - // console.log('file', file) - - await new Promise(r => setTimeout(r, 1500 * scanIndex)) - - file.statusMessage = '🗃 Decompressing file' - - let entries - - try { - entries = await this.unzipFile( file ) - } catch ( Error ) { - // console.warn( Error ) - - // Set status message as error - file.statusMessage = `❔ ${ Error.message }` - file.status = 'finished' - - return - } - - file.statusMessage = '👀 Scanning App Files' - - const foundEntries = this.findEntries( entries, { - macho: this.matchesMacho, - rootInfo: this.matchesRootInfo + this.scanFile( file, scanIndex ).then( + response => resolve(response), + err => reject(new Error(err)) + ).finally(() => clearTimeout(timer)) }) - - // Clean out entries now that we're done with them - entries = undefined - - // console.log('foundEntries', foundEntries) - - // file.machOEntries = this.findMachOEntries( entries ) - file.machOEntries = foundEntries.macho - - // If no Macho files were found - // then report and stop - if ( file.machOEntries.length === 0 ) { - console.log(`No Macho files found for ${file.name}`, file.machOEntries) - - file.statusMessage = `❔ Unkown app format` - file.status = 'finished' - - return - } - - // Warn if Info.plist doesn't look right - if ( foundEntries.rootInfo.length > 1) { - console.warn('More than one root Info.plist found', foundEntries.rootInfo) - } else if ( foundEntries.rootInfo.length === 0 ) { - console.warn('No root Info.plist found', foundEntries.rootInfo) - } - - // Break out root entry into a variable - const [ rootInfoEntry ] = foundEntries.rootInfo - - // Get blob data from zip - const infoXml = await rootInfoEntry.getData( - // writer - // https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing - new zip.TextWriter(), - // options - { - useWebWorkers: true, - // onprogress: (index, max) => { - - // const percentageNumber = (index / max * 100) - // // onprogress callback - // console.log(`Writer progress ${percentageNumber}`) - // } - } - ) - - // console.log('infoXml', infoXml) - - // Parse the Info.plist data - const info = plist.parse( infoXml ) - - file.appVersion = info.CFBundleShortVersionString - file.displayName = info.CFBundleDisplayName - - // Set details - const detailsData = [ - [ 'Version', info.CFBundleShortVersionString ], - [ 'Bundle Identifier', info.CFBundleIdentifier ], - [ 'File Mime Type', file.type ], - [ 'Copyright', info.NSHumanReadableCopyright ], - // [ 'Version', info.CFBundleShortVersionString ], - ] - - detailsData.forEach( ([ label, value ]) => { - if ( !value || value.length === 0 ) return - - file.details.push({ - label, - value, - }) - } ) - - // console.log('infoFiles', file.name, { - // path: rootInfoEntry.filename, - // info - // }) - - // const machOBlob = await file.machOEntries - - - const parsedMachoEntries = await Promise.all( file.machOEntries.map( async machOEntry => { - // console.log('Parsing ', machOEntry.filename) - - // Get blob data from zip - const machOBlob = await machOEntry.getData( - // writer - // https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing - // new zip.TextWriter(), - new zip.BlobWriter(), - // options - { - useWebWorkers: true, - // onprogress: (index, max) => { - // const percentageNumber = (index / max * 100) - // // onprogress callback - // console.log(`Writer progress ${percentageNumber}`) - // } - } - ) - - return await this.parseMachOBlob( machOBlob, file.name ) - } ) ) - - // console.log('parsedMachoEntries', parsedMachoEntries) - - // file.statusMessage = `🏁 Scan Finished. ${file.machOEntries.length} Mach-o files` - file.statusMessage = `🏁 Scan Finished. ` - - let supportedBinaries = 0 - let unsupportedBinaries = 0 - - // Count supported and unsupported binaries - parsedMachoEntries.forEach( binaryEntry => { - const armBinary = binaryEntry.architectures.find( architecture => { - if ( architecture.processorType === 0 ) return false - - return architecture.processorType.toLowerCase().includes('arm') - }) - - if ( armBinary !== undefined ) { - supportedBinaries++ - } else { - unsupportedBinaries++ - } - } ) - - - // console.log('supportedBinaries', supportedBinaries) - // console.log('unsupportedBinaries', unsupportedBinaries) - - if (supportedBinaries !== 0 && unsupportedBinaries !== 0) { - file.statusMessage = `🔶 App has some support. ` - } else if ( unsupportedBinaries !== 0 ) { - file.statusMessage = `🔶 This app file is not natively compatible with Apple Silicon and may only run via Rosetta 2 translation, however, software vendors will sometimes will ship separate install files for Intel and ARM instead of a single one. ` - } else if ( supportedBinaries !== 0 ) { - file.statusMessage = '✅ This app is natively compatible with Apple Silicon!' - - // Shift this scan to the top - this.files.unshift( this.files.splice( scanIndex, 1 )[0] ) - } - - file.status = 'finished' - - return })) diff --git a/pages/apple-silicon-compatibility.vue b/pages/apple-silicon-compatibility.vue index b0c14a6..dae7024 100644 --- a/pages/apple-silicon-compatibility.vue +++ b/pages/apple-silicon-compatibility.vue @@ -89,11 +89,23 @@ - {{ appScan.displayName || appScan.name }} {{ appScan.appVersion ? `- v${appScan.appVersion}` : '' }} + {{ appScan.displayName || appScan.name }} + {{ appScan.appVersion ? `- v${appScan.appVersion}` : '' }} + {{ appScan.displayAppSize ? `- App ${appScan.displayAppSize}` : '' }} + {{ appScan.displayBinarySize ? `- Binary ${appScan.displayBinarySize}` : '' }}
{{ appScan.statusMessage }}
+ appScan.binarySize: {{ appScan.binarySize }} + +
+ ⚠️ Large Binary - This scan may take a while an/or have issues +
+
Details