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'
|
import parseMacho from './macho/index.js'
|
||||||
|
|
||||||
// console.log('MachOParser', MachOParser)
|
const prettyBytes = require('pretty-bytes')
|
||||||
|
|
||||||
|
|
||||||
const knownArchiveExtensions = new Set([
|
const knownArchiveExtensions = new Set([
|
||||||
'app',
|
'app',
|
||||||
|
|
@ -43,6 +44,16 @@ function isValidHttpUrl( string ) {
|
||||||
return url.protocol === "http:" || url.protocol === "https:"
|
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
|
let zip
|
||||||
|
|
||||||
export default class AppFilesScanner {
|
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
|
// 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 ) {
|
async scan ( fileList ) {
|
||||||
|
|
||||||
// Push files to our files array
|
// Push files to our files array
|
||||||
|
|
@ -280,6 +489,9 @@ export default class AppFilesScanner {
|
||||||
statusMessage: '⏳ File Loaded and Queud',
|
statusMessage: '⏳ File Loaded and Queud',
|
||||||
details: [],
|
details: [],
|
||||||
appVersion: null,
|
appVersion: null,
|
||||||
|
displayAppSize: prettyBytes( fileInstance.size ),
|
||||||
|
displayBinarySize: null,
|
||||||
|
binarySize: null,
|
||||||
|
|
||||||
name: fileInstance.name,
|
name: fileInstance.name,
|
||||||
size: fileInstance.size,
|
size: fileInstance.size,
|
||||||
|
|
@ -290,191 +502,24 @@ export default class AppFilesScanner {
|
||||||
} )
|
} )
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const scanTimeoutSeconds = 30
|
||||||
|
|
||||||
// Scan for archives
|
// 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
|
const timer = setTimeout(() => {
|
||||||
// then skip
|
file.statusMessage = '❔ Scan timed out'
|
||||||
if ( file.status === 'finished' ) return
|
file.status = 'finished'
|
||||||
|
|
||||||
if ( !this.isApp( file ) ) {
|
reject(new Error('Scan timed out'))
|
||||||
file.statusMessage = '⏭ Skipped. Not app or archive'
|
}, scanTimeoutSeconds * 1000)
|
||||||
file.status = 'finished'
|
|
||||||
|
|
||||||
return
|
this.scanFile( file, scanIndex ).then(
|
||||||
}
|
response => resolve(response),
|
||||||
|
err => reject(new Error(err))
|
||||||
// console.log('file', file)
|
).finally(() => clearTimeout(timer))
|
||||||
|
|
||||||
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
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 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
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,11 +89,23 @@
|
||||||
<div class="absolute hidden left-0 h-12 w-12 rounded-full md:flex items-center justify-center bg-darker">
|
<div class="absolute hidden left-0 h-12 w-12 rounded-full md:flex items-center justify-center bg-darker">
|
||||||
{{ appScan.name.charAt(0) }}
|
{{ appScan.name.charAt(0) }}
|
||||||
</div>
|
</div>
|
||||||
{{ 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}` : '' }}
|
||||||
<div class="text-sm leading-5 font-bold">
|
<div class="text-sm leading-5 font-bold">
|
||||||
{{ appScan.statusMessage }}
|
{{ appScan.statusMessage }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
appScan.binarySize: {{ appScan.binarySize }}
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="appScan.binarySize && appScan.binarySize < (10 ^ 6)"
|
||||||
|
class="text-sm leading-5 font-bold"
|
||||||
|
>
|
||||||
|
⚠️ Large Binary - This scan may take a while an/or have issues
|
||||||
|
</div>
|
||||||
|
|
||||||
<details class="w-full pt-6">
|
<details class="w-full pt-6">
|
||||||
<summary class="cursor-pointer mb-3">Details</summary>
|
<summary class="cursor-pointer mb-3">Details</summary>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue