Enable parsing macho file

This commit is contained in:
Sam Carlton 2022-07-16 22:01:01 -05:00
parent e852d275ef
commit 70e72a86e4
2 changed files with 115 additions and 45 deletions

View file

@ -1,10 +1,10 @@
import { Blob } from 'buffer' import { Blob } from 'buffer'
import plist from 'plist' import plist from 'plist'
import prettyBytes from 'pretty-bytes'
// import zip from '@zip.js/zip.js' // import zip from '@zip.js/zip.js'
import FileApi, { File } from 'file-api'
import { import parseMacho from '~/helpers/macho/index.js'
isValidHttpUrl
} from '~/helpers/check-types.js'
// For some reason inline 'import()' works better than 'import from' // For some reason inline 'import()' works better than 'import from'
@ -32,8 +32,12 @@ export class AppScan {
this.file = null this.file = null
this.bundleFileEntries = [] this.bundleFileEntries = []
this.infoPlist = {} this.infoPlist = {}
this.machoExcutables = []
this.machoMeta = {} this.machoMeta = {}
this.bundleExecutable = null
this.displayBinarySize = ''
this.binarySize = 0
} }
sendMessage ( details ) { sendMessage ( details ) {
@ -50,31 +54,35 @@ export class AppScan {
return Object.keys( this.infoPlist ).length > 0 return Object.keys( this.infoPlist ).length > 0
} }
// get bundleExecutablePath () { get hasMachoMeta () {
// if ( !this.hasInfoPlist ) return '' return Object.keys( this.machoMeta ).length > 0
}
// // There our CFBundleExecutable is a path to the executable get bundleExecutablePath () {
// // then use it if ( !this.hasInfoPlist ) return ''
// if ( this.infoPlist.CFBundleExecutable.includes('/') ) return `/Contents/${ this.infoPlist.CFBundleExecutable }`
// // Use default executable path // There our CFBundleExecutable is a path to the executable
// return `/Contents/MacOS/${ this.infoPlist.CFBundleExecutable }` // then use it
// } if ( this.infoPlist.CFBundleExecutable.includes('/') ) return `/Contents/${ this.infoPlist.CFBundleExecutable }`
async readFileEntryData ( fileEntry ) { // Use default executable path
return `/Contents/MacOS/${ this.infoPlist.CFBundleExecutable }`
}
async readFileEntryData ( fileEntry, Writer = zip.TextWriter ) {
// Get blob data from zip // Get blob data from zip
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-entry // https://gildas-lormeau.github.io/zip.js/core-api.html#zip-entry
return await fileEntry.getData( return await fileEntry.getData(
// writer // writer
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing // https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing
new zip.TextWriter(), new Writer()//zip.TextWriter(),
) )
} }
async readFileBlob ( File ) { async readFileBlob ( FileInstance ) {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
const fileReader = new zip.BlobReader( new Blob( File.arrayBuffer ) ) const fileReader = new zip.BlobReader( new Blob( FileInstance.arrayBuffer ) )
// this.sendMessage({ // this.sendMessage({
// message: '📖 Setting up BlobReader', // message: '📖 Setting up BlobReader',
@ -135,18 +143,19 @@ export class AppScan {
}) })
} }
matchesMacho ( entry ) { matchesMachoExecutable ( entry ) {
// Skip files that are deeper than 3 folders // Skip files that are deeper than 3 folders
if ( entry.filename.split('/').length > 4 ) return false if ( entry.filename.split('/').length > 4 ) return false
// Skip folders // Skip folders
if ( entry.filename.endsWith('/') ) return false // if ( !!entry.directory ) return false
// `${ appName }.app/Contents/MacOS/${ appName }` // `${ appName }.app/Contents/MacOS/${ appName }`
// Does this entry path match any of our wanted paths // Does this entry path match any of our wanted paths
return [ return [
// `${ appName }.app/Contents/MacOS/${ appName }` // `${ appName }.app/Contents/MacOS/${ appName }`
`.app/Contents/MacOS/` // `.app/Contents/MacOS/`,
`Contents/MacOS/`
].some( pathToMatch => { ].some( pathToMatch => {
return entry.filename.includes( pathToMatch ) return entry.filename.includes( pathToMatch )
}) })
@ -176,7 +185,7 @@ export class AppScan {
fileEntryType ( fileEntry ) { fileEntryType ( fileEntry ) {
if ( !!fileEntry.directory ) return 'directory' if ( !!fileEntry.directory ) return 'directory'
if ( this.matchesMacho( fileEntry ) ) return 'macho' if ( this.matchesMachoExecutable( fileEntry ) ) return 'machoExecutable'
if ( this.matchesRootInfoPlist( fileEntry ) ) return 'rootInfoPlist' if ( this.matchesRootInfoPlist( fileEntry ) ) return 'rootInfoPlist'
@ -202,18 +211,64 @@ export class AppScan {
}) })
} }
// async storeMachoMeta ( fileEntry ) { storeMachoExecutable = ( fileEntry ) => {
// const machoData = await this.readFileEntryData( fileEntry ) this.machoExcutables.push( fileEntry )
// }
this.sendMessage({
message: '🥊 Found a Macho executable',
// data: machoExecutable,
})
}
storeMachoMeta = async ( fileEntry ) => {
// Throw if we have more than one target file
if ( this.hasMachoMeta ) {
throw new Error( 'More than one primary Macho executable found' )
}
// Get blob data from zip
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-entry
const bundleExecutableBlob = await this.readFileEntryData( fileEntry, zip.Uint8ArrayWriter )
// console.log( 'bundleExecutableBlob', bundleExecutableBlob.buffer )
const machoFileInstance = new File({
name: this.bundleExecutable.filename,
type: 'application/x-mach-binary',
buffer: bundleExecutableBlob,
})
this.machoMeta = await parseMacho( machoFileInstance, FileApi ) //await this.parseMachOBlob( bundleExecutableBlob, file.name )
// console.log( 'this.machoMeta', this.machoMeta )
}
targetFiles = { targetFiles = {
rootInfoPlist: { rootInfoPlist: {
method: this.storeInfoPlist method: this.storeInfoPlist
}, },
// macho: { machoExecutable: {
// method: this.storeMacho, method: this.storeMachoExecutable,
// } }
}
findMainExecutable () {
// Now that we have the info.plist Determine our entry Macho Executable from the list of Macho Executables
const bundleExecutables = this.machoExcutables.filter( machoEntry => {
return this.bundleExecutablePath.includes( machoEntry.filename )
})
// Warn if Bundle Executable doesn't look right
if ( bundleExecutables.length > 1) {
throw new Error('More than one root bundleExecutable found', bundleExecutables)
} else if ( bundleExecutables.length === 0 ) {
throw new Error('No root bundleExecutable found', bundleExecutables)
}
return bundleExecutables[ 0 ]
} }
async findTargetFiles () { async findTargetFiles () {
@ -225,6 +280,7 @@ export class AppScan {
// Check if we have a target file // Check if we have a target file
if ( this.targetFiles[ type ] ) { if ( this.targetFiles[ type ] ) {
// console.log( 'fileEntry', type, fileEntry.filename )
// Call the target file method // Call the target file method
await this.targetFiles[ type ].method( fileEntry ) await this.targetFiles[ type ].method( fileEntry )
@ -233,6 +289,18 @@ export class AppScan {
// console.log( 'File Entry Type:', type ) // console.log( 'File Entry Type:', type )
} }
// Now that we have the info.plist Determine our entry Macho Executable from the list of Macho Executables
// Set the bundleExecutable
this.bundleExecutable = this.findMainExecutable()
console.log('Parsing ', this.bundleExecutable.filename, this.bundleExecutable.uncompressedSize / 1000 )
this.displayBinarySize = prettyBytes( this.bundleExecutable.uncompressedSize )
this.binarySize = this.bundleExecutable.uncompressedSize
await this.storeMachoMeta( this.bundleExecutable )
} }
async start () { async start () {
@ -255,7 +323,6 @@ export class AppScan {
await this.findTargetFiles() await this.findTargetFiles()
this.sendMessage({ this.sendMessage({
message: '🏁 Scan complete', message: '🏁 Scan complete',
status: 'complete' status: 'complete'

View file

@ -49,32 +49,35 @@ async function makeZipFromBundlePath ( bundlePath ) {
} }
describe.concurrent('Apps', () => { describe.concurrent('Apps', async () => {
// Compress plain app bundles to zipped File Objects // Compress plain app bundles to zipped File Objects
for ( const bundlePath of plainAppBundles ) { for ( const bundlePath of plainAppBundles ) {
it( `Can read info.plist for ${ path.basename( bundlePath ) } bundle` , async () => { const appName = path.basename( bundlePath )
// Compress plain app bundles to zipped File Objects // Create a new AppScan instance
for ( const bundlePath of plainAppBundles ) { const scan = new AppScan({
fileLoader: () => makeZipFromBundlePath( bundlePath ),
// Create a new AppScan instance messageReceiver: ( details ) => {
const scan = new AppScan({ console.log( 'Scan message:', details )
fileLoader: () => makeZipFromBundlePath( bundlePath ),
messageReceiver: ( details ) => {
console.log( 'Scan message:', details )
}
})
// Scan the archive
await scan.start()
// console.log( 'infoPlist', scan.infoPlist )
expect( scan.hasInfoPlist ).toBe( true )
} }
})
// Scan the archive
await scan.start()
it( `Can read info.plist for ${ appName } bundle` , () => {
// console.log( 'infoPlist', scan.infoPlist )
expect( scan.hasInfoPlist ).toBe( true )
})
it( `Can read macho meta for entry ${ appName } bundle`, () => {
// console.log( 'machoMeta', scan.machoMeta )
expect( scan.hasMachoMeta ).toBe( true )
}) })
} }