Display app and binary size

This commit is contained in:
Sam Carlton 2021-02-03 14:27:36 -06:00
parent da1ce05470
commit b5adbe5b78
2 changed files with 239 additions and 182 deletions

View file

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