mirror of
https://github.com/ThatGuySam/doesitarm.git
synced 2026-05-18 06:44:46 -07:00
Test that App Scan can read plist
This commit is contained in:
parent
dc358ba5ac
commit
2a525d93d5
4 changed files with 377 additions and 44 deletions
305
helpers/scanner/client.js
Normal file
305
helpers/scanner/client.js
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
import { Blob } from 'buffer'
|
||||||
|
import plist from 'plist'
|
||||||
|
// import zip from '@zip.js/zip.js'
|
||||||
|
|
||||||
|
import {
|
||||||
|
isValidHttpUrl
|
||||||
|
} from '~/helpers/check-types.js'
|
||||||
|
|
||||||
|
|
||||||
|
// For some reason inline 'import()' works better than 'import from'
|
||||||
|
// https://gildas-lormeau.github.io/zip.js/
|
||||||
|
const zip = await import('@zip.js/zip.js')
|
||||||
|
|
||||||
|
// https://gildas-lormeau.github.io/zip.js/core-api.html#configuration
|
||||||
|
zip.configure({
|
||||||
|
// Disable Web Workers for SSR since Node doesn't support them yet
|
||||||
|
// https://vitejs.dev/guide/env-and-mode.html#env-variables
|
||||||
|
useWebWorkers: !import.meta.env.SSR
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export class AppScan {
|
||||||
|
constructor ({
|
||||||
|
fileLoader,
|
||||||
|
messageReceiver
|
||||||
|
}) {
|
||||||
|
|
||||||
|
this.fileLoader = fileLoader
|
||||||
|
this.messageReceiver = messageReceiver
|
||||||
|
|
||||||
|
this.status = 'idle'
|
||||||
|
this.file = null
|
||||||
|
this.bundleFileEntries = []
|
||||||
|
this.infoPlist = {}
|
||||||
|
this.machoMeta = {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage ( details ) {
|
||||||
|
if ( details?.status ) {
|
||||||
|
this.status = details.status
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( typeof( this.messageReceiver ) === 'function' ) {
|
||||||
|
this.messageReceiver( details )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasInfoPlist () {
|
||||||
|
return Object.keys( this.infoPlist ).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// get bundleExecutablePath () {
|
||||||
|
// if ( !this.hasInfoPlist ) return ''
|
||||||
|
|
||||||
|
// // There our CFBundleExecutable is a path to the executable
|
||||||
|
// // then use it
|
||||||
|
// if ( this.infoPlist.CFBundleExecutable.includes('/') ) return `/Contents/${ this.infoPlist.CFBundleExecutable }`
|
||||||
|
|
||||||
|
// // Use default executable path
|
||||||
|
// return `/Contents/MacOS/${ this.infoPlist.CFBundleExecutable }`
|
||||||
|
// }
|
||||||
|
|
||||||
|
async readFileEntryData ( fileEntry ) {
|
||||||
|
// Get blob data from zip
|
||||||
|
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-entry
|
||||||
|
return await fileEntry.getData(
|
||||||
|
// writer
|
||||||
|
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-writing
|
||||||
|
new zip.TextWriter(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async readFileBlob ( File ) {
|
||||||
|
return new Promise( ( resolve, reject ) => {
|
||||||
|
const fileReader = new zip.BlobReader( new Blob( File.arrayBuffer ) )
|
||||||
|
|
||||||
|
// this.sendMessage({
|
||||||
|
// message: '📖 Setting up BlobReader',
|
||||||
|
// status: 'reading',
|
||||||
|
// data: fileReader
|
||||||
|
// })
|
||||||
|
|
||||||
|
fileReader.onload = function () {
|
||||||
|
// do something on FileReader onload
|
||||||
|
this.sendMessage({
|
||||||
|
message: '📖 Reading file',
|
||||||
|
status: 'reading'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fileReader.onerror = error => {
|
||||||
|
// do something on FileReader onload
|
||||||
|
console.error('File Read Error', error)
|
||||||
|
|
||||||
|
reject( 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)
|
||||||
|
|
||||||
|
// do something on FileReader onload
|
||||||
|
this.sendMessage({
|
||||||
|
message: `📖 Reading file. ${ progress }% read`,
|
||||||
|
status: 'reading'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://gildas-lormeau.github.io/zip.js/core-api.html#zip-reading
|
||||||
|
const zipReader = new zip.ZipReader( fileReader )
|
||||||
|
|
||||||
|
// zipReader.onload = console.log
|
||||||
|
|
||||||
|
// zipReader.onprogress = console.log
|
||||||
|
|
||||||
|
// zipReader.onerror = console.error
|
||||||
|
|
||||||
|
zipReader
|
||||||
|
.getEntries()
|
||||||
|
.then( entries => {
|
||||||
|
|
||||||
|
// do something on entries
|
||||||
|
this.sendMessage({
|
||||||
|
message: '📖 Reading file complete. Entries found',
|
||||||
|
status: 'read'
|
||||||
|
})
|
||||||
|
|
||||||
|
resolve( 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 )
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
matchesRootInfoPlist ( 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
|
||||||
|
|
||||||
|
// If this entry matches the root info.plist path exactly
|
||||||
|
// then we have found the root info.plist
|
||||||
|
if ( entry.filename === 'Contents/Info.plist' ) return true
|
||||||
|
|
||||||
|
// 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 )
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fileEntryType ( fileEntry ) {
|
||||||
|
if ( !!fileEntry.directory ) return 'directory'
|
||||||
|
|
||||||
|
if ( this.matchesMacho( fileEntry ) ) return 'macho'
|
||||||
|
|
||||||
|
if ( this.matchesRootInfoPlist( fileEntry ) ) return 'rootInfoPlist'
|
||||||
|
|
||||||
|
// getData
|
||||||
|
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
storeInfoPlist = async ( fileEntry ) => {
|
||||||
|
// Throw if we have more than one target file
|
||||||
|
if ( this.hasInfoPlist ) {
|
||||||
|
throw new Error( 'More than one root info.plist found' )
|
||||||
|
}
|
||||||
|
|
||||||
|
const infoXml = await this.readFileEntryData( fileEntry )
|
||||||
|
|
||||||
|
// Parse the Info.plist data
|
||||||
|
this.infoPlist = plist.parse( infoXml )
|
||||||
|
|
||||||
|
this.sendMessage({
|
||||||
|
message: 'ℹ️ Found Info.plist',
|
||||||
|
// data: this.infoPlist,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// async storeMachoMeta ( fileEntry ) {
|
||||||
|
// const machoData = await this.readFileEntryData( fileEntry )
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
targetFiles = {
|
||||||
|
rootInfoPlist: {
|
||||||
|
method: this.storeInfoPlist
|
||||||
|
},
|
||||||
|
// macho: {
|
||||||
|
// method: this.storeMacho,
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
async findTargetFiles () {
|
||||||
|
|
||||||
|
for ( const fileEntry of this.bundleFileEntries ) {
|
||||||
|
const type = this.fileEntryType( fileEntry )
|
||||||
|
|
||||||
|
// console.log( 'fileEntry', type, fileEntry.filename )
|
||||||
|
|
||||||
|
// Check if we have a target file
|
||||||
|
if ( this.targetFiles[ type ] ) {
|
||||||
|
|
||||||
|
// Call the target file method
|
||||||
|
await this.targetFiles[ type ].method( fileEntry )
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log( 'File Entry Type:', type )
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async start () {
|
||||||
|
// Load in the file
|
||||||
|
this.sendMessage({
|
||||||
|
message: '🚛 Loading file...',
|
||||||
|
status: 'loading'
|
||||||
|
})
|
||||||
|
|
||||||
|
this.file = await this.fileLoader()
|
||||||
|
|
||||||
|
// console.log( 'File:', this.file )
|
||||||
|
|
||||||
|
this.bundleFileEntries = await this.readFileBlob( this.file )
|
||||||
|
|
||||||
|
this.sendMessage({
|
||||||
|
message: '🎬 Starting scan',
|
||||||
|
status: 'scanning'
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.findTargetFiles()
|
||||||
|
|
||||||
|
|
||||||
|
this.sendMessage({
|
||||||
|
message: '🏁 Scan complete',
|
||||||
|
status: 'complete'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// export class AppScannerClient {
|
||||||
|
|
||||||
|
// constructor ({ messageReceiver }) {
|
||||||
|
|
||||||
|
// const testResultStore = global.$config.testResultStore
|
||||||
|
|
||||||
|
// if ( !isValidHttpUrl( testResultStore ) ) {
|
||||||
|
// throw new Error( 'testResultStore is not a valid url' )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.messageReceiver = messageReceiver
|
||||||
|
// }
|
||||||
|
|
||||||
|
// scanQueue = []
|
||||||
|
|
||||||
|
// get unscanned () {
|
||||||
|
// return this.scanQueue.filter( scan => scan.status === 'idle' )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// get nextScan () {
|
||||||
|
|
||||||
|
|
||||||
|
// sendMessage ( details ) {
|
||||||
|
// if( typeof( this.messageReceiver ) === 'function' ) {
|
||||||
|
// messageReceiver( details )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// qeueScan ( File ) {
|
||||||
|
// // Create a new scan instance
|
||||||
|
// const scan = new AppScan({ File, messageReceiver: this.sendMessage })
|
||||||
|
|
||||||
|
// // Add the scan to the queue
|
||||||
|
// this.scanQueue.push( scan )
|
||||||
|
// }
|
||||||
|
|
||||||
|
// start () {
|
||||||
|
|
||||||
|
// }
|
||||||
|
// }
|
||||||
71
test/scanner/client.test.js
Normal file
71
test/scanner/client.test.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
|
||||||
|
import {
|
||||||
|
assert,
|
||||||
|
expect,
|
||||||
|
test
|
||||||
|
} from 'vitest'
|
||||||
|
|
||||||
|
// https://github.com/mrmlnc/fast-glob
|
||||||
|
import glob from 'fast-glob'
|
||||||
|
import { LocalFileData } from 'get-file-object-from-local-path'
|
||||||
|
import { Zip } from 'zip-lib'
|
||||||
|
|
||||||
|
import { AppScan } from '~/helpers/scanner/client'
|
||||||
|
|
||||||
|
|
||||||
|
const appGlobOptions = {
|
||||||
|
onlyFiles: false,
|
||||||
|
deep: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const appsPath = 'test/_artifacts/apps'
|
||||||
|
|
||||||
|
const tempPath = 'test/_artifacts/temp'
|
||||||
|
|
||||||
|
const plainAppBundles = glob.sync( `${ appsPath }/**/*.app`, appGlobOptions )
|
||||||
|
|
||||||
|
|
||||||
|
async function makeZipFromBundlePath ( bundlePath ) {
|
||||||
|
const archivePath = `${ tempPath }/${ bundlePath.split('/').pop() }.zip`
|
||||||
|
|
||||||
|
// console.log( 'archivePath', archivePath )
|
||||||
|
|
||||||
|
const zipLib = new Zip()
|
||||||
|
|
||||||
|
// Adds a folder from the file system, putting its contents at the root of archive
|
||||||
|
zipLib.addFolder( bundlePath )
|
||||||
|
|
||||||
|
// Generate zip file.
|
||||||
|
await zipLib.archive( archivePath )
|
||||||
|
|
||||||
|
// Create a File Object from the zip file
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/File/File
|
||||||
|
const archiveFile = new LocalFileData( archivePath )
|
||||||
|
|
||||||
|
// console.log( 'archiveFile', archiveFile )
|
||||||
|
|
||||||
|
return archiveFile
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Can read info.plist for .app file', async () => {
|
||||||
|
|
||||||
|
// Compress plain app bundles to zipped File Objects
|
||||||
|
for ( const bundlePath of plainAppBundles ) {
|
||||||
|
|
||||||
|
// Create a new AppScan instance
|
||||||
|
const scan = new AppScan({
|
||||||
|
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 )
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
|
|
||||||
import {
|
|
||||||
// assert,
|
|
||||||
expect,
|
|
||||||
test
|
|
||||||
} from 'vitest'
|
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
|
||||||
|
|
||||||
|
|
||||||
// import {
|
|
||||||
// isString
|
|
||||||
// } from '~/helpers/check-types.js'
|
|
||||||
|
|
||||||
// const cases = [
|
|
||||||
|
|
||||||
// ]
|
|
||||||
|
|
||||||
|
|
||||||
function buildVueInstance () {
|
|
||||||
return createApp({
|
|
||||||
template: '<div>Hello World</div>',
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
appsBeingScanned: []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
test('Can initialize AppFilesScanner within Vite context', async () => {
|
|
||||||
const { default: AppFilesScanner } = await import('~/helpers/app-files-scanner.js')
|
|
||||||
|
|
||||||
const vueInstance:any = buildVueInstance()
|
|
||||||
|
|
||||||
const scanner = new AppFilesScanner({
|
|
||||||
observableFilesArray: vueInstance.appsBeingScanned,
|
|
||||||
testResultStore: global.$config.testResultStore
|
|
||||||
})
|
|
||||||
|
|
||||||
// Expect the scanner to be an instance of AppFilesScanner
|
|
||||||
expect( scanner ).toBeInstanceOf( AppFilesScanner )
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
@ -11,6 +11,7 @@ const vitestConfig = {
|
||||||
...astroConfig.vite,
|
...astroConfig.vite,
|
||||||
|
|
||||||
test: {
|
test: {
|
||||||
|
testTimeout: 60 * 1000,
|
||||||
setupFiles: 'tsconfig-paths/register'
|
setupFiles: 'tsconfig-paths/register'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue