mirror of
https://github.com/ThatGuySam/doesitarm.git
synced 2026-05-18 06:44:46 -07:00
Display app and binary size
This commit is contained in:
parent
da1ce05470
commit
b5adbe5b78
2 changed files with 239 additions and 182 deletions
|
|
@ -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
|
||||
}))
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue