+
+
+
+
+
diff --git a/components/navbar.vue b/components/navbar.vue
index 4ccec3b..8615eb8 100644
--- a/components/navbar.vue
+++ b/components/navbar.vue
@@ -161,6 +161,10 @@ export default {
label: 'Games',
url: '/games',
},
+ // {
+ // label: 'Apple Silicon App Test',
+ // url: '/apple-silicon-app-test',
+ // },
])
}
},
diff --git a/helpers/app-files-scanner.js b/helpers/app-files-scanner.js
new file mode 100644
index 0000000..5154f3c
--- /dev/null
+++ b/helpers/app-files-scanner.js
@@ -0,0 +1,616 @@
+import plist from 'plist'
+import axios from 'axios'
+
+import parseMacho from './macho/index.js'
+
+const prettyBytes = require('pretty-bytes')
+
+
+const knownArchiveExtensions = new Set([
+ 'app',
+ 'dmg',
+ // 'pkg',
+ 'zip',
+ // 'gz',
+ // 'bz2'
+])
+
+const notAppFileTypes = new Set([
+ 'image',
+ 'text',
+ 'audio',
+ 'video'
+])
+
+const knownAppExtensions = new Set([
+ '.app',
+ '.app.zip'
+])
+
+function isString( maybeString ) {
+ return (typeof maybeString === 'string' || maybeString instanceof String)
+}
+
+function isValidHttpUrl( string ) {
+ if ( !isString( string ) ) return false
+
+ let url
+
+ try {
+ url = new URL(string)
+ } catch (_) {
+ return false
+ }
+
+ 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 {
+
+ constructor( {
+ observableFilesArray,
+ testResultStore
+ } ) {
+ // Files to process
+ this.files = observableFilesArray
+
+ this.testResultStore = testResultStore
+
+ // https://gildas-lormeau.github.io/zip.js/
+ zip = require('@zip.js/zip.js')
+
+ // https://gildas-lormeau.github.io/zip.js/core-api.html#configuration
+ zip.configure({
+ workerScripts: true,
+ // workerScripts: {
+ // inflate: ["lib/z-worker-pako.js", "pako_inflate.min.js"]
+ // }
+ })
+ }
+
+
+ isApp ( file ) {
+
+ if ( file.type.includes('/') && notAppFileTypes.has( file.type.split('/')[0] ) ) return false
+
+ return true
+ }
+
+ getStatusMessage () {
+ // 'Drag and drop one or multiple apps'
+
+ // return `Searching for apps at ${ file.url }`
+
+
+ }
+
+ getFileStatusMessage ( file ) {
+
+
+ // CORS error - 'This page has asked not to be scanned. '
+
+ // Status Code Error - 'This page is not loading properly. '
+
+ // No app urls found - 'No apps found on this page. Try a different page or entering the package URL directly. You can also manually download the package then drop it on here. '
+
+ // 'Found # apps'
+
+ // Fetching / File Loading from drag and drop - 'Loading # apps'
+
+ // Unzipping, archive search and Parsing - 'Processing # of #'
+
+ // Not able to unzip - 'Unable to open package. Try a different file. '
+
+ // No Mach-o binary found - 'Could not find Mac App data in package. Try a different package. '
+
+ // Mach-o Parsing Error - 'Unable to scan package. Try a different one. '
+
+ // No ARM64 Architecture found - 'This App's binary is not compatible with Apple Silicon and will 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. You can try submitting the download page link for an app and we'll scan that. '
+
+ // ARM64 Architecture found -
+ return 'This App is natively compatible with Apple Silicon!'
+ }
+
+
+ // async scanPageForAppUrls () {
+
+ // }
+
+ // async downloadArchiveFromUrl () {
+
+ // }
+
+ async unzipFile ( file ) {
+ const fileReader = new zip.BlobReader( file.instance )//new FileReader()
+
+ fileReader.onload = function() {
+
+ // do something on FileReader onload
+ console.log('File Read')
+
+ file.statusMessage = '📖 Reading file'
+ }
+
+ fileReader.onerror = error => {
+
+ // do something on FileReader onload
+ console.error('File Read Error', error)
+
+ throw new Error('File Read Error', error)
+ }
+
+ fileReader.onprogress = (data) => {
+ if (data.lengthComputable) {
+ const progress = parseInt( ((data.loaded / data.total) * 100), 10 );
+ console.log('Read progress', progress)
+
+ file.statusMessage = `📖 Reading file. ${ progress }% read`
+ }
+ }
+
+ // console.log('fileReader', fileReader)
+
+ // https://gildas-lormeau.github.io/zip.js/core-api.html#zip-reading
+ const zipReader = new zip.ZipReader( fileReader )
+
+ // zipReader.onprogress = console.log
+
+ // zipReader.onerror = console.log
+
+ const entries = await zipReader.getEntries()
+ .then( entries => entries.map( entry => {
+ return entry
+
+ // return {
+ // filename: entry.filename,
+ // directory: entry.directory
+ // }
+ }) )
+ .catch( error => {
+ // console.warn('Unzip Error', error)
+
+ return error
+ })
+
+ // console.log('entries', entries)
+
+ if ( !Array.isArray(entries) ) {
+ file.statusMessage = '❔ Could not decompress file'
+ file.status = 'finished'
+
+ throw new Error('Could not decompress file')
+
+ // return new Error('Could not decompress file')
+ }
+
+ return entries
+ }
+
+ matchesMacho ( entry ) {
+ // Skip files that are deeper than 3 folders
+ if ( entry.filename.split('/').length > 4 ) return false
+
+ // Skip folders
+ if ( entry.filename.endsWith('/') ) return false
+
+ // `${ appName }.app/Contents/MacOS/${ appName }`
+ // Does this entry path match any of our wanted paths
+ return [
+ // `${ appName }.app/Contents/MacOS/${ appName }`
+ `.app/Contents/MacOS/`
+ ].some( pathToMatch => {
+ return entry.filename.includes(pathToMatch)
+ })
+ }
+
+ matchesRootInfo ( entry ) {
+ // Skip files that are deeper than 2 folders
+ if ( entry.filename.split('/').length > 3 ) return false
+
+ // Skip folders
+ if ( entry.filename.endsWith('/') ) return false
+
+ // Does this entry path match any of our wanted paths
+ return [
+ // `zoom.us.app/Contents/Info.plist`
+ `.app/Contents/Info.plist`,
+ `.zip/Contents/Info.plist`
+ ].some( pathToMatch => {
+ return entry.filename.endsWith(pathToMatch)
+ })
+ }
+
+ findEntries ( entries, matchersObject ) {
+
+ const matches = {}
+
+ // const matcherKeys = Object.keys( matchers )
+
+ // Create a new set to store found App Names
+ const appNamesInArchive = new Set()
+
+ // Search App Names in entries
+ entries.forEach( entry => {
+ // Look through filename parts
+ entry.filename.split('/').forEach( filenamePart => {
+ if ( filenamePart.includes('.app') ) {
+ const appName = filenamePart.split('.')[0]
+
+ appNamesInArchive.add( appName )
+ }
+ } )
+
+
+ for ( const key in matchersObject ) {
+
+ // Deos it match the matcher method
+ const entryMatches = matchersObject[key]( entry )
+
+ if ( entryMatches ) {
+ // If we haven't set up an array for this key
+ // then create one
+ if ( !Array.isArray(matches[key]) ) matches[key] = []
+
+ // Push this entry to our matching list
+ matches[key].push( entry )
+ }
+ }
+
+ } )
+
+ return matches
+ }
+
+ async parseMachOBlob ( machOBlob, fileName ) {
+ const machOFile = new File([machOBlob], fileName)
+
+ return await parseMacho( machOFile )
+ }
+
+ getBundleExecutablePath ( info ) {
+ if ( info.CFBundleExecutable.includes('/') ) return `/Contents/${ info.CFBundleExecutable }`
+
+ return `/Contents/MacOS/${ info.CFBundleExecutable }`
+ }
+
+ classifyBinaryEntryArchitecture ( binaryEntry ) {
+ // Find an ARM Architecture
+ const armArchitecture = binaryEntry.architectures.find( architecture => {
+ // if ( architecture.processorType === 0 ) return false
+
+ // If processorType not a string
+ // then return false
+ if ( !isString(architecture.processorType) ) return false
+
+ return architecture.processorType.toLowerCase().includes('arm')
+ })
+
+ // Was an ARM Architecture found
+ return (armArchitecture !== undefined)
+ }
+
+ async submitScanInfo ({
+ filename,
+ appVersion,
+ result,
+ machoMeta,
+ infoPlist
+ }) {
+ // Each file scanned: Filename, Type(Drop or URL), File URL, Datetime, Architectures, Mach-o Meta
+
+ // console.log( 'this.testResultStore', this.testResultStore )
+
+ const { supportedVersionNumber } = await axios.post( this.testResultStore , {
+ filename,
+ appVersion,
+ result,
+ machoMeta: JSON.stringify( machoMeta ),
+ infoPlist: JSON.stringify( infoPlist )
+ })
+ .then( response => response.data )
+ .catch(function (error) {
+ console.error(error)
+ })
+
+ return {
+ supportedVersionNumber
+ }
+ }
+
+ 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 )
+
+ this.submitScanInfo ({
+ filename: file.name,
+ appVersion: null,
+ result: 'error_decompression_error',
+ machoMeta: null,
+ infoPlist: null
+ })
+
+ // 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)
+
+ this.submitScanInfo ({
+ filename: file.name,
+ appVersion: null,
+ result: 'error_no_macho_files',
+ machoMeta: null,
+ infoPlist: null
+ })
+
+ 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}`)
+ // }
+ }
+ )
+
+ // 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`)
+
+ // console.log('info.CFBundleExecutable', info.CFBundleExecutable)
+ // console.log('info', info)
+ // console.log('file.machOEntries', file.machOEntries)
+
+ const bundelExecutablePath = this.getBundleExecutablePath( info )
+
+ const bundleExecutables = file.machOEntries.filter( machoEntry => {
+ return machoEntry.filename.includes(bundelExecutablePath)
+ })
+
+ // Warn if Bundle Executable doesn't look right
+ if ( bundleExecutables.length > 1) {
+ console.warn('More than one root bundleExecutable found', bundleExecutables)
+ } else if ( bundleExecutables.length === 0 ) {
+ console.warn('No root bundleExecutable found', bundleExecutables)
+ }
+
+ const [ bundleExecutable ] = bundleExecutables
+
+ console.log('Parsing ', bundleExecutable.filename, bundleExecutable.uncompressedSize / 1000 )
+
+ file.displayBinarySize = prettyBytes( bundleExecutable.uncompressedSize )
+ file.binarySize = bundleExecutable.uncompressedSize
+
+ // Get blob data from zip
+ // https://gildas-lormeau.github.io/zip.js/core-api.html#zip-entry
+ const bundleExecutableBlob = await bundleExecutable.getData(
+ // writer
+ // https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing
+ new zip.BlobWriter(),
+ // options
+ {
+ useWebWorkers: true
+ }
+ )
+
+ const mainExecutableMeta = await this.parseMachOBlob( bundleExecutableBlob, file.name )
+ console.log( 'mainExecutableMeta', mainExecutableMeta )
+
+ const binarySupportsNative = this.classifyBinaryEntryArchitecture( mainExecutableMeta )
+
+
+ // Submit the scan to get any reports on preexisting native reports
+ const { supportedVersionNumber } = await this.submitScanInfo ({
+ filename: file.name,
+ appVersion: file.appVersion,
+ result: finishedStatusMessage,
+ machoMeta: {
+ ...mainExecutableMeta,
+ file: undefined,
+ architectures: mainExecutableMeta.architectures.map( architecture => {
+ return {
+ bits: architecture.bits,
+ fileType: architecture.fileType,
+ header: architecture.header,
+ loadCommandsInfo: architecture.loadCommandsInfo,
+ magic: architecture.magic,
+ offset: architecture.offset,
+ processorSubType: architecture.processorSubType,
+ processorType: architecture.processorType,
+ }
+ })
+ },
+ infoPlist: info
+ })
+
+ console.log('supportedVersionNumber', supportedVersionNumber)
+
+
+ let finishedStatusMessage = ''
+
+ if ( binarySupportsNative ) {
+ finishedStatusMessage = '✅ This app is natively compatible with Apple Silicon!'
+
+ // Shift this scan to the top
+ this.files.unshift( this.files.splice( scanIndex, 1 )[0] )
+ } else if ( supportedVersionNumber !== null ) {
+
+ finishedStatusMessage = [
+ '✅ A native version of this has been reported',
+ (supportedVersionNumber.length > 0) ? `as of v${supportedVersionNumber}` : null
+ ].join(' ')
+
+ } else {
+ finishedStatusMessage = `🔶 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. `
+ }
+
+ file.statusMessage = finishedStatusMessage
+ file.status = 'finished'
+
+ return
+ }
+
+ async scan ( fileList ) {
+
+ // Push files to our files array
+ Array.from(fileList).forEach( (fileInstance, scanIndex) => {
+ this.files.unshift( {
+ status: 'loaded',
+ displayName: null,
+ statusMessage: '⏳ File Loaded and Queud',
+ details: [],
+ appVersion: null,
+ displayAppSize: prettyBytes( fileInstance.size ),
+ displayBinarySize: null,
+ binarySize: null,
+
+ name: fileInstance.name,
+ size: fileInstance.size,
+ type: fileList.item( scanIndex ).type,
+ lastModifiedDate: fileInstance.lastModifiedDate,
+ instance: fileInstance,
+ item: fileList.item( scanIndex )
+ } )
+ })
+
+ const scanTimeoutSeconds = 30
+
+ // Scan for archives
+ await Promise.all( this.files.map( ( file, scanIndex ) => {
+ return new Promise( (resolve, reject) => {
+
+ const timer = setTimeout(() => {
+ file.statusMessage = '❔ Scan timed out'
+ file.status = 'finished'
+
+ reject(new Error('Scan timed out'))
+ }, scanTimeoutSeconds * 1000)
+
+ this.scanFile( file, scanIndex ).then(
+ response => resolve(response),
+ err => reject(new Error(err))
+ ).finally(() => clearTimeout(timer))
+ })
+ }))
+
+ // Go through and set all files to finished to clean up any straglers
+ this.files.forEach( file => {
+ file.status = 'finished'
+ })
+
+ console.log('All Scans Finished')
+
+
+ return
+ }
+
+}
diff --git a/helpers/macho/index.js b/helpers/macho/index.js
new file mode 100644
index 0000000..6770937
--- /dev/null
+++ b/helpers/macho/index.js
@@ -0,0 +1,4 @@
+import MachoParser from './macho.js'
+
+
+export default MachoParser
diff --git a/helpers/macho/macho.constants.js b/helpers/macho/macho.constants.js
new file mode 100644
index 0000000..c78a021
--- /dev/null
+++ b/helpers/macho/macho.constants.js
@@ -0,0 +1,13 @@
+export let SECTION_ATTRIBUTES_USR = 0xff000000;
+export let S_ATTR_PURE_INSTRUCTIONS = 0x80000000;
+export let SECTION_ATTRIBUTES_SYS = 0x00ffff00;
+export let S_ATTR_SOME_INSTRUCTIONS = 0x0000400;
+export let S_ATTR_EXT_RELOC = 0x00000200;
+export let S_ATTR_LOC_RELOC = 0x00000100;
+
+export let uint128_t = 16;
+export let uint64_t = 8;
+export let uint32_t = 4;
+export let uint16_t = 2;
+export let uint8_t = 1;
+
diff --git a/helpers/macho/macho.cpu.js b/helpers/macho/macho.cpu.js
new file mode 100644
index 0000000..b41ad0b
--- /dev/null
+++ b/helpers/macho/macho.cpu.js
@@ -0,0 +1,134 @@
+//Global Constants
+var CPU_TYPES = [];
+var CPUSubTypeARM = [];
+
+let CPU_ARCH_CONST = {
+ MASK: 0x01000000,
+ ABI64: 0xff000000,
+ toString: function() {
+ JSON.stringify(this);
+ }
+};
+
+let CPU_TYPE = {
+ ANY: -1,
+ VAX: 1,
+ MC680: 6,
+ X86: 7,
+ MIPS: 8,
+ MC98000: 10,
+ HPPA: 11,
+ ARM: 12,
+ ARM64: 16777228,
+ MC88000: 13,
+ SPARC: 14,
+ I860: 15,
+ POWERPC: 18,
+ POWERPC64: 16777234,
+ DESCRIPTION: function(search) {
+ let result = CPU_TYPES[search];
+ return (result != undefined) ? result : search;
+ },
+ toString: function() {
+ return JSON.stringify(this);
+ }
+};
+
+CPU_TYPES[CPU_TYPE.ANY] = "Any";
+CPU_TYPES[CPU_TYPE.VAX] = "VAX";
+CPU_TYPES[CPU_TYPE.MC680] = "MC680";
+CPU_TYPE[CPU_TYPE.HPPA] = "HPPA";
+CPU_TYPES[CPU_TYPE.ARM] = "ARM";
+CPU_TYPES[CPU_TYPE.ARM64] = "ARM64";
+CPU_TYPES[CPU_TYPE.X86] = "X86";
+CPU_TYPES[CPU_TYPE.I860] = "I860";
+CPU_TYPES[CPU_TYPE.MIPS] = "Mips";
+CPU_TYPES[CPU_TYPE.MC98000] = "MC98000";
+CPU_TYPES[CPU_TYPE.SPARC] = "Sparc";
+CPU_TYPES[CPU_TYPE.POWERPC] = "Power PC";
+CPU_TYPES[CPU_TYPE.POWERPC64] = "Power PC 64-bit";
+
+let CPU_SUB_TYPE = {
+ ARM: {
+ MULTIPLE: -1,
+ ALL: 0,
+ ARM_A500_ARCH: 1,
+ ARM_A500: 2,
+ ARM_A440: 3,
+ ARM_M4: 4,
+ V4T: 5,
+ V6: 6,
+ V5TEJ: 7,
+ XSCALE: 8,
+ V7: 9,
+ V7F: 10,
+ V7S: 11,
+ V7K: 12,
+ V8: 13,
+ V6M: 14,
+ V7M: 15,
+ V7EM: 16,
+ DESCRIPTION: function(search) {
+
+
+ let result = CPUSubTypeARM[search];
+ return (result != undefined) ? result : search;
+ },
+ toString: function() {
+ return JSON.stringify(this);
+ }
+ },
+ ARM64: {
+ MULTIPLE: -1,
+ ALL: 0,
+ V8: 1,
+ DESCRIPTION: function(search) {
+ var CPUSubTypeARM64 = [];
+ CPUSubTypeARM64[CPU_SUB_TYPE.ARM64.ALL] = 'all';
+ let result = CPUSubTypeARM64[search];
+ return (result != undefined) ? result : search;
+ },
+ toString: function() {
+ return JSON.stringify(this);
+ }
+ },
+ POWERPC64: {
+ MULTIPLE: -1,
+ POWERPC_ALL: 0,
+ POWERPC_601: 1,
+ POWERPC_602: 2,
+ POWERPC_603: 3,
+ POWERPC_603e: 4,
+ POWERPC_603ev: 5,
+ POWERPC_604: 6,
+ POWERPC_604e: 7,
+ POWERPC_620: 8,
+ POWERPC_750: 9,
+ POWERPC_7400: 10,
+ POWERPC_7450: 11,
+ POWERPC_970: 100,
+ POWERPC_ALL_LIB64: 2147483648,
+ toString: function() {
+ return JSON.stringify(this);
+ }
+ },
+ toString: function() {
+ return JSON.stringify(this);
+ }
+};
+
+CPUSubTypeARM[CPU_SUB_TYPE.ARM.ALL] = 'all';
+CPUSubTypeARM[CPU_SUB_TYPE.ARM.V4T] = 'v4t';
+CPUSubTypeARM[CPU_SUB_TYPE.ARM.V6] = 'v6';
+CPUSubTypeARM[CPU_SUB_TYPE.ARM.V5] = 'v5';
+CPUSubTypeARM[CPU_SUB_TYPE.ARM.XSCALE] = 'xscale';
+CPUSubTypeARM[CPU_SUB_TYPE.ARM.V7] = 'v7';
+CPUSubTypeARM[CPU_SUB_TYPE.ARM.V7F] = 'v7f';
+CPUSubTypeARM[CPU_SUB_TYPE.ARM.V7S] = 'v7s';
+CPUSubTypeARM[CPU_SUB_TYPE.ARM.V7K] = 'v7k';
+CPUSubTypeARM[CPU_SUB_TYPE.ARM.V6M] = 'v6m';
+CPUSubTypeARM[CPU_SUB_TYPE.ARM.V7M] = 'v7m';
+CPUSubTypeARM[CPU_SUB_TYPE.ARM.V7EM] = 'v7em';
+
+
+export { CPU_TYPES, CPUSubTypeARM, CPU_ARCH_CONST, CPU_TYPE, CPU_SUB_TYPE }
diff --git a/helpers/macho/macho.cstr.js b/helpers/macho/macho.cstr.js
new file mode 100644
index 0000000..4c94b5c
--- /dev/null
+++ b/helpers/macho/macho.cstr.js
@@ -0,0 +1,12 @@
+var Cstr = function Cstr(buf) {
+ this.toString = function() {
+ return JSON.stringify(this);
+ };
+ var cstr = '';
+ for(var i = 0; i < buf.length; i++) {
+ cstr += String.fromCharCode(buf[i]);
+ }
+ return cstr;
+};
+
+export default Cstr
diff --git a/helpers/macho/macho.dylib.js b/helpers/macho/macho.dylib.js
new file mode 100644
index 0000000..12369a1
--- /dev/null
+++ b/helpers/macho/macho.dylib.js
@@ -0,0 +1,72 @@
+var Dylib = function Dylib(name, timestamp, current_version, compatibility_version) {
+ this.name = name || 0x00000000;
+ this.timestamp = timestamp || 0x00000000;
+ this.current_version = current_version || 0x00000000;
+ this.compatibility_version = compatibility_version || 0x00000000;
+ this.toString = function() {
+ return JSON.stringify(this);
+ };
+};
+
+var DylibCommand = function DylibCommand(cmd, cmdsize, dylib) {
+ this.cmd = cmd || 0x00000000;
+ this.cmdsize = cmdsize || 0x00000000;
+ this.dylib = dylib || new Dylib(); //needs better input validation
+ this.toString = function() {
+ return JSON.stringify(this);
+ };
+};
+
+var DylibTableOfContents = function DylibTableOfContents(symbol_index, module_index) {
+ this.symbol_index = symbol_index || 0x00000000;
+ this.module_index = module_index || 0x00000000;
+ this.toString = function() {
+ return JSON.stringify(this);
+ };
+};
+
+var DylibModule = function DylibModule(module_name, iextdefsym, nextdefsym, irefsym, nrefsym, ilocalsym, nlocalsym, iextrel, nextrel, iinit_iterm, ninit_nterm, objc_module_info_addr, objc_module_info_size) {
+ this.module_name = module_name || 0x00000000;
+ this.iextdefsym = iextdefsym || 0x00000000;
+ this.nextdefsym = nextdefsym || 0x00000000;
+ this.irefsym = irefsym || 0x00000000;
+ this.nrefsym = nrefsym || 0x00000000;
+ this.ilocalsym = ilocalsym || 0x00000000;
+ this.nlocalsym = nlocalsym || 0x00000000;
+ this.iextrel = iextrel || 0x00000000;
+ this.nextrel = nextrel || 0x00000000;
+ this.iinit_iterm = iinit_iterm || 0x00000000;
+ this.ninit_nterm = ninit_nterm || 0x00000000;
+ this.objc_module_info_addr = objc_module_info_addr || 0x00000000;
+ this.objc_module_info_size = objc_module_info_size || 0x00000000;
+ this.toString = function() {
+ return JSON.stringify(this);
+ };
+};
+
+var DylibModule64 = function DylibModule64(module_name, iextdefsym, nextdefsym, irefsym, nrefsym, ilocalsym, nlocalsym, iextrel, nextrel, iinit_iterm, ninit_nterm, objc_module_info_addr, objc_module_info_size) {
+ this.module_name = module_name || 0x00000000;
+ this.iextdefsym = iextdefsym || 0x00000000;
+ this.nextdefsym = nextdefsym || 0x00000000;
+ this.irefsym = irefsym || 0x00000000;
+ this.nrefsym = nrefsym || 0x00000000;
+ this.ilocalsym = ilocalsym || 0x00000000;
+ this.nlocalsym = nlocalsym || 0x00000000;
+ this.iextrel = iextrel || 0x00000000;
+ this.nextrel = nextrel || 0x00000000;
+ this.iinit_iterm = iinit_iterm || 0x00000000;
+ this.ninit_nterm = ninit_nterm || 0x00000000;
+ this.objc_module_info_addr = objc_module_info_addr || 0x00000000;
+ this.objc_module_info_size = objc_module_info_size || 0x0000000000000000;
+ this.toString = function() {
+ return JSON.stringify(this);
+ };
+};
+
+var DylibReference = {
+ isym:24,
+ flags:8,
+ toString: function() {
+ return JSON.stringify(this);
+ }
+};
\ No newline at end of file
diff --git a/helpers/macho/macho.dylinker.js b/helpers/macho/macho.dylinker.js
new file mode 100644
index 0000000..49850e5
--- /dev/null
+++ b/helpers/macho/macho.dylinker.js
@@ -0,0 +1,7 @@
+var DylinkerCommand = function DylinkerCommand(cmd, cmdsize) {
+ this.cmd = 0x00000000;
+ this.cmdsize = 0x00000000;
+ this.toString = function() {
+ return JSON.stringify(this);
+ };
+};
\ No newline at end of file
diff --git a/helpers/macho/macho.file.js b/helpers/macho/macho.file.js
new file mode 100644
index 0000000..416e9aa
--- /dev/null
+++ b/helpers/macho/macho.file.js
@@ -0,0 +1,73 @@
+var FILE_TYPES = [];
+var FILE_FLAGS = [];
+
+
+/*
+ @Class FILE_TYPE
+
+ @Description
+ File Type is the class for identifying what MACH-O file is being dealed with.
+*/
+let FILE_TYPE = {
+ MH_OBJECT: 0x1, /* relocatable object file */
+ MH_EXECUTE: 0x2, /* demand paged executable file */
+ MH_FVMLIB: 0x3, /* fixed VM shared library file */
+ MH_CORE: 0x4, /* core file */
+ MH_PRELOAD: 0x5, /* preloaded executable file */
+ MH_DYLIB: 0x6, /* dynamicly bound shared library file*/
+ MH_DYLINKER: 0x7, /* dynamic link editor */
+ MH_BUNDLE: 0x8, /* dynamicly bound bundle file */
+ MH_DYLIB_STUB: 0x9,
+ MH_DSYM: 0xa,
+ MH_KEXT_BUNDLE: 0xb,
+
+ DESCRIPTION: function(search) {
+ let result = FILE_TYPES[search];
+ return (result != undefined) ? result : search;
+ },
+
+ toString: function() {
+ return JSON.stringify(this);
+ },
+
+ debugdescription: ""
+};
+
+FILE_TYPES[FILE_TYPE.MH_OBJECT] = 'Relocatable Object File';
+FILE_TYPES[FILE_TYPE.MH_EXECUTE] = 'Demand Paged Executable File';
+FILE_TYPES[FILE_TYPE.MH_FVMLIB] = 'Fixed Virtual Memory Shared Library File';
+FILE_TYPES[FILE_TYPE.MH_CORE] = 'Core File';
+FILE_TYPES[FILE_TYPE.MH_PRELOAD] = 'Preloaded Executable File';
+FILE_TYPES[FILE_TYPE.MH_DYLIB] = 'Dynamically Bound Shared Library File';
+FILE_TYPES[FILE_TYPE.MH_DYLINKER] = 'Dynamic Link Editor';
+FILE_TYPES[FILE_TYPE.MH_BUNDLE] = 'Dynamically Bound Bundle File';
+FILE_TYPES[FILE_TYPE.MH_DYLIB_STUB] = 'Dynamic Library Predefined Symbol';
+FILE_TYPES[FILE_TYPE.MH_DSYM] = 'Dynamic Symbol';
+FILE_TYPES[FILE_TYPE.MH_KEXT_BUNDLE] = 'Kernel Extension Bundle';
+
+let FILE_FLAG = {
+ MH_NOUNDEFS: 0x1, /* the object file has no undefined references, can be executed */
+ MH_INCRLINK: 0x2, /* the object file is the output of an incremental link against a base file and can't be link edited again */
+ MH_DYLDLINK: 0x4, /* the object file is input for the dynamic linker and can't be staticly link edited again */
+ MH_BINDATLOAD: 0x8, /* the object file's undefined references are bound by the dynamic linker when loaded. */
+ MH_PREBOUND: 0x10, /* the file has it's dynamic undefined references prebound. */
+
+ DESCRIPTION: function(search) {
+
+ let result = FILE_FLAGS[search];
+ return (result != undefined) ? result : search;
+ },
+
+ toString: function() {
+ return JSON.stringify(this);
+ }
+};
+
+FILE_FLAGS[FILE_FLAG.MH_NOUNDEFS] = 'The object file has no undefined references and is executable.';
+FILE_FLAGS[FILE_FLAG.MH_INCRLINK] = 'The object file is the output of an incremental link against a base file and can not be link edited again.';
+FILE_FLAGS[FILE_FLAG.MH_DYLDLINK] = 'The object file is the input for the dynamic linker and can not be staticly link edited again.';
+FILE_FLAGS[FILE_FLAG.MH_BINDATLOAD] = 'The object file\'s undefined references are bound by the dynamic linker when loaded.';
+FILE_FLAGS[FILE_FLAG.MH_PREBOUND] = 'The file has it\'s dynamic undefined references prebound.';
+
+
+export { FILE_TYPES, FILE_FLAGS, FILE_TYPE, FILE_FLAG }
diff --git a/helpers/macho/macho.flags.js b/helpers/macho/macho.flags.js
new file mode 100644
index 0000000..bb1f0fe
--- /dev/null
+++ b/helpers/macho/macho.flags.js
@@ -0,0 +1,31 @@
+let FLAGS = {
+ NOUNDEFS: 1,
+ INCRLINK: 2,
+ DYLDLINK: 4,
+ BINDATLOAD: 8,
+ PREBOUND: 16,
+ SPLIT_SEGS: 32,
+ LAZY_INIT: 64,
+ TWOLEVEL: 128,
+ FORCE_FLAT: 256,
+ NOMULTIDEFS: 512,
+ NOFIXPREBINDING: 1024,
+ PREBINDABLE: 2048,
+ ALLMODSBOUND: 4096,
+ SUBSECTIONS_VIA_SYMBOLS: 8192,
+ CANONICAL: 32768,
+ WEAK_DEFINES: 32768,
+ BINDS_TO_WEAK: 65536,
+ ALLOW_STACK_EXECUTION: 131072,
+ ROOT_SAFE: 262144,
+ SETUID_SAFE: 524288,
+ NOREEXPORTED_DYLIBS: 1048576,
+ PIE: 2097152,
+ DEAD_STRIPPABLE_DYLIB: 4194304,
+ HAS_TLV_DESCRIPTORS: 8388608,
+ NO_HEAP_EXECUTION: 16777216,
+ APP_EXTENSION_SAFE: 33554432
+ toString: function(){
+ return JSON.stringify(this);
+ }
+};
\ No newline at end of file
diff --git a/helpers/macho/macho.fvmlib.js b/helpers/macho/macho.fvmlib.js
new file mode 100644
index 0000000..2ddf954
--- /dev/null
+++ b/helpers/macho/macho.fvmlib.js
@@ -0,0 +1,17 @@
+var Fvmlib = function Fvmlib(name, minor_version, header_addr) {
+ this.name = name || 0x00000000;
+ this.minor_version = minor_version || 0x00000000;
+ this.header_addr = header_addr || 0x00000000;
+ this.toString = function() {
+ return JSON.stringify(this);
+ };
+};
+
+var FvmlibCommand = function FvmlibCommand(cmd, cmdsize, fvmlib) {
+ this.cmd = cmd || 0x00000000;
+ this.cmdsize = cmdsize || 0x00000000;
+ this.fvmlib = fvmlib || new Fvmlib(); //needs better input validation
+ this.toString = function() {
+ return JSON.stringify(this);
+ };
+};
\ No newline at end of file
diff --git a/helpers/macho/macho.header.js b/helpers/macho/macho.header.js
new file mode 100644
index 0000000..b5ae248
--- /dev/null
+++ b/helpers/macho/macho.header.js
@@ -0,0 +1,49 @@
+//Mach-O Fat file header
+export var FatHeader = function FatHeader() {
+ this.magic = 0x00000000;
+ this.nfat_arch = 0x00000000;
+ this.toString = function() {
+ return JSON.stringify(this);
+ }
+};
+
+//Mach-O Fat file header
+export var FatArch = function FatArch() {
+ this.cputype = 0x00000000;
+ this.cpusubtype = 0x00000000;
+ this.offset = 0x00000000;
+ this.size = 0x00000000;
+ this.align = 0x00000000;
+ this.toString = function() {
+ return JSON.stringify(this);
+ }
+}
+
+//Mach-O Binary Executable header 32-bit
+export var MachoHeader = function MachoHeader() {
+ this.magic = 0x00000000;
+ this.cputype = 0x00000000;
+ this.cpusubtype = 0x00000000;
+ this.filetype = 0x00000000;
+ this.ncmds = 0x00000000;
+ this.sizeofcmds = 0x00000000;
+ this.flags = 0x00000000;
+ this.toString = function() {
+ return JSON.stringify(this);
+ }
+};
+
+//Mach-O Binary Executable header 64-bit
+export var MachoHeader64 = function MachoHeader64() {
+ this.magic = 0x00000000;
+ this.cputype = 0x00000000;
+ this.cpusubtype = 0x00000000;
+ this.filetype = 0x00000000;
+ this.ncmds = 0x00000000;
+ this.sizeofcmds = 0x00000000;
+ this.flags = 0x00000000;
+ this.reserved = 0x00000000;
+ this.toString = function() {
+ return JSON.stringify(this);
+ }
+};
diff --git a/helpers/macho/macho.js b/helpers/macho/macho.js
new file mode 100644
index 0000000..43f5b48
--- /dev/null
+++ b/helpers/macho/macho.js
@@ -0,0 +1,317 @@
+//macho.js
+//Written by Sem Voigtländer
+//Licensed under the MIT License
+
+import { mime_binary } from './mimetypes.js'
+import { ReadUint32, ReadUint16LE, ReadUint32LE, ReadUint16 } from './memory.js'
+
+import { uint32_t, uint64_t } from './macho.constants.js'
+import { MAGIC } from './macho.magic.js'
+import { MachoHeader64, MachoHeader } from './macho.header.js'
+import { LOAD_COMMAND_TYPE, LoadCommand } from './macho.loadcommand.js'
+import { CPU_TYPE, CPU_SUB_TYPE } from './macho.cpu.js'
+import { FILE_FLAGS, FILE_TYPE } from './macho.file.js'
+import Cstr from './macho.cstr.js'
+import { SegmentCommand } from './macho.segment.js'
+
+function default_callback(buffer) {
+ console.log('Received ' + buffer.byteLength / (1024 * 1024) + ' MB');
+}
+
+// https://stackoverflow.com/a/57139182/1397641
+async function arrayBufferToBlob( buffer ) {
+ return new Blob([buffer])
+}
+
+let readers = []
+
+let tempFile
+let tempFiles
+
+var ChunkReader = function ChunkReader(file, chunksize = (1024 * 1024), callback = default_callback) {
+
+ if(file == undefined) {
+ throw new Error('Invalid argument for file parameter.');
+ }
+
+ if(readers == undefined) { readers = []; } //Free list
+
+
+ this.chunksize = chunksize; //Read a kilobyte at a time
+ this.filesize = file.size;
+ this.offset = 0;
+
+ //Start reading the chunks
+ while(this.offset + this.chunksize <= this.filesize) {
+
+ this.blob = file.slice(this.offset, this.offset+this.chunksize);
+
+ readers[readers.length] = new FileReader();
+ readers[readers.length-1].onloadend = function(e) {
+ callback(e.target.result);
+ };
+ readers[readers.length-1].readAsArrayBuffer(this.blob);
+ // console.log('Sending chunk from 0x'+this.offset.toString(16));
+ this.offset+=this.chunksize;
+ }
+
+ for(obj = 0; obj < readers.length; obj++) {
+ readers[obj] = undefined;
+ }
+
+
+ // Clean up readers
+ readers = undefined
+ // free("readers");
+};
+
+export function MachoParser(file, callback) {
+
+ const machoOutputData = {}
+
+ //properties
+ this.reader = new FileReader();
+
+ function writeToCallback(val) {
+ return
+
+ // if(callbackElement) {
+ // callbackElement.innerHTML +='
'+val+'
';
+ // } else {
+ // throw new Error('Invalid callback.');
+ // }
+ }
+
+ /*
+ @Function FindMagic
+ @Params (Uint8Array) buffer, (bool) breakwhenfound
+ @Return (Array) offsets of found magics
+ */
+ function FindMagic(data, breakwhenfound = false) {
+ var results = [];
+ for(var byte = 0; byte < (data.length - uint32_t); byte++) { //Read 32-bits at a time until the end of the buffer
+ var magic = ReadUint32(data, byte); //Read the next 32-bit magic value
+ if(MAGIC.VALIDATE(magic)) {
+ results.push(byte);
+ if(breakwhenfound) {
+ break;
+ }
+ }
+ }
+ return results;
+ }
+
+
+ /*
+ @Function MapFlags
+ @Params (Uint8Array)flags, (Object)map
+ @Return (Object) key-value mapped dictionary
+ */
+ function MapFlags(value, map) {
+ var res = {};
+ for (var bit = 1; (value < 0 || bit <= value) && bit !== 0; bit <<= 1)
+ if (value & bit) res[map[bit]] = true; //If value and the bit are equal then map the value to the result
+
+ return res;
+ }
+
+ function ParseCommand(type, data, size, off) {
+ var cmd = null;
+ if(type == LOAD_COMMAND_TYPE.LC_SEGMENT) {
+ if(data.length < 48) {
+ if(process.env.VERBOSE){ console.log('Segment command OOB'); }
+ return new LoadCommand(type, data, size, off);
+ }
+ let name = new Cstr(data.slice(0, (4*uint32_t)));
+ cmd = new SegmentCommand(
+ type,
+ size,
+ name,
+ ReadUint32(data, (4*uint32_t)),
+ ReadUint32(data, (5*uint32_t)),
+ ReadUint32(data, (6*uint32_t)),
+ ReadUint32(data, (7*uint32_t)),
+ ReadUint32(data, (8*uint32_t)),
+ ReadUint32(data, (9*uint32_t)),
+ ReadUint32(data, (10*uint32_t)),
+ ReadUint32(data, (11*uint32_t))
+ );
+
+ function prot(p) {
+ var res = {read: false, write: false, exec: false};
+ return res;
+ }
+
+ let sectSize = 17 * uint32_t;
+ var sections = [];
+ //for(var i = 0, off = 48; i < nsects)
+ return cmd;
+ } else {
+ return new LoadCommand(type, data, size, off);
+ }
+ }
+
+ this.reader.onloadend = function(e) {
+
+ let fileType = mime_binary;
+
+ //Handle the file buffer and eventually create a Blob of the result
+ // blobUtil.
+ arrayBufferToBlob(e.target.result, fileType).then(function(blob) {
+
+ let filesize = ((blob.size / 1024) / 1024); //Calculate FileSize in Megabyte
+ tempFile = blob;
+
+ //Set up the file and print out for verbosity
+ machoOutputData.file = file
+ machoOutputData.fileSize = filesize
+ machoOutputData.architectures = []
+ // writeToCallback('
File
'+file.name.toString()+'
');
+ // writeToCallback('
Size
' + filesize.toFixed(2).toString() + 'MB
');
+
+ //Construct a new 8-bit array from the file buffer
+ let data = new Uint8Array(e.target.result);
+ let magics = FindMagic(data, false); //Try to find all Mach-O magics in the byte array
+
+ if ( process.env.DEBUG ) { console.log('Parsing all magics...'); }
+
+ //If magics where found, parse the binary.
+ if(magics.length > 0) {
+
+ for(var cMagic = 0; cMagic < magics.length; cMagic++) { //Start parsing the binary from each found magic's offset
+
+ const architecture = {}
+
+ let magicOff = magics[cMagic]; //The offset of the magic currently being parsed
+ let magic = ReadUint32(data, magicOff); //Read the magic from the byte array
+ let littleendian = MAGIC.ISLITTLEENDIAN(magic); //Get the endianness of the magic
+ let x64 = MAGIC.IS64BIT(magic); //Check which bit architecture is being used
+
+ // machoOutputData = {}; //Create the Mach-O information object
+ architecture.bits = (x64 ? "64-bit" : "32-bit"); //Get the architecture
+ architecture.endianness = (littleendian ? "little endian" : "big endian"); //Get the endianness
+ architecture.header = (x64 ? new MachoHeader64() : new MachoHeader()); //Depending on architecture, construct a new header
+ architecture.header.magic = magic; //Add the magic to the header
+ architecture.header.cputype = (littleendian ? ReadUint16LE(data, magicOff+uint32_t) : ReadUint16(data, magicOff+uint32_t)); //Read the cputype which comes after the magic
+ architecture.header.cpusubtype = (littleendian ? ReadUint16LE(data, magicOff+(2*uint32_t)) : ReadUint16(data, magicOff+(2*uint32_t))); //Read the cpu subtype which comes after the cputype
+ architecture.header.filetype = (littleendian ? ReadUint32LE(data, magicOff+(3*uint32_t)) : ReadUint32(data, magicOff+(3*uint32_t))); //Read the file type which comes after the cpu subtype
+ architecture.header.ncmds = (littleendian ? ReadUint32LE(data, magicOff+(4*uint32_t)) : ReadUint32(data, magicOff+(4*uint32_t))); //Read the number of commands which comes after the filetype
+ architecture.header.sizeofcmds =(littleendian ? ReadUint32LE(data, magicOff+(5*uint32_t)) : ReadUint32(data, magicOff+(5*uint32_t))); //Read the size of the commands which comes after the number of commands
+ architecture.header.flags = (littleendian ? ReadUint32LE(data, magicOff+(6*uint32_t)) : ReadUint32(data, magicOff+(6*uint32_t))); //Read the flags which come after the size of the commands
+ architecture.loadcommands = [];
+
+ var align = (x64 ? uint64_t : uint32_t); //Depending on our architecture set an align for parsing the load commands
+
+ for(var i = 0, off = magicOff; off < data.length, i < architecture.header.ncmds; i++) {
+
+ var curr_cmd = {};
+ curr_cmd.cmd = (littleendian ? ReadUint32LE(data, off) : ReadUint32(data, off)); //Read the command type from the offset
+ curr_cmd.cmdsize = (littleendian ? ReadUint32LE(data, off+align) : ReadUint32(data, off+align)); //Read the size of the command from the offset
+ curr_cmd.fileoff = off; //Add the offset of the loadcommand for later use
+ curr_cmd.data = data.slice(off, curr_cmd.cmdsize);
+
+ curr_cmd = ParseCommand(curr_cmd.cmd, curr_cmd.data, curr_cmd.cmdsize, curr_cmd.fileoff);
+ if(curr_cmd.cmdsize > 0) { //Commands with a size of zero are not valid or not interesting
+ architecture.loadcommands.push(curr_cmd);
+ }
+
+ off+=8;
+
+ i++; //Increate the loadcommand counter
+ }
+
+
+ architecture.offset = magicOff.toString(16)
+ architecture.magic = architecture.header.magic.toString(16)
+ architecture.processorType = CPU_TYPE.DESCRIPTION(architecture.header.cputype)
+ architecture.processorSubType = CPU_SUB_TYPE.ARM.DESCRIPTION(architecture.header.cpusubtype)
+ architecture.fileType = FILE_TYPE.DESCRIPTION(architecture.header.filetype)
+
+ /* Parse all collected information to Human Readable strings */
+ // writeToCallback('');
+ // writeToCallback('