UNPKG

hookfinder

Version:

Find hooks when working with Xposed development

188 lines (168 loc) 5.88 kB
#!/usr/bin/env node 'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var fs = _interopDefault(require('fs')); var path = require('path'); var jsonfile = _interopDefault(require('jsonfile')); var chalk = require('chalk'); var json = _interopDefault(require('comment-json')); function __async(g){return new Promise(function(s,j){function c(a,x){try{var r=g[x?"throw":"next"](a);}catch(e){j(e);return}r.done?s(r.value):Promise.resolve(r.value).then(c,d);}function d(e){c(e,1);}c();})} class Hookfinder { constructor (options = {}) { this.targets = options.targets; this.basefolder = options.basefolder; this.saveReport = options.saveReport; } start () {return __async(function*(){ const result = yield Promise.all(Object.keys(this.targets).map(classPath => __async(function*(){ const filePath = path.join(this.basefolder, classPath.replace(/\./g, '/') + '.smali'); const source = yield this.readFile(filePath); const methods = (this.targets[classPath].methods || []).map(method => { let implementations = source // match entire method .match(new RegExp(method.filter.source + /(.|\n|\r)*?\.end method/.source, 'g')) // use implementation filter if defined .filter(impl => { if (typeof method.implementationFilter === 'undefined') return true return impl.match(method.implementationFilter) !== null }); let names = implementations.map(impl => this.getCapture(impl, method.filter)); return { name: method.name, matches: names, implementations: implementations } }); const fields = (this.targets[classPath].fields || []).map(field => { let fieldNames = source.match(field.filter) .map(smali => this.getCapture(smali, field.filter)); return { name: field.name, matches: fieldNames } }); return { name: classPath, methods, fields } }.call(this)))); result.forEach(r => { console.log('#', r.name); const {methods, fields} = r; if (methods.length) { console.log('## Methods'); methods.forEach(m => { console.log(m.name, '=', m.matches.join(',')); if (m.matches.length > 1) { console.log(chalk.yellow(m.implementations.join('\n'))); } }); } if (fields.length) { console.log('## Fields'); fields.forEach(f => console.log(f.name, '=', f.matches.join(','))); } console.log(); }); if (this.saveReport) { this.writeReport(result); } }.call(this))} getCapture (source, regex) { return source.match(new RegExp(regex.source))[1] } writeReport (data) { jsonfile.writeFile(path.join(process.cwd(), this.saveReport), data, {spaces: 2}, function (err) { if (err) { console.error('Failed to write report'); throw err } }); } readFile (filePath) { return new Promise((resolve, reject) => { fs.readFile(filePath, 'utf8', (err, data) => { if (err) return reject(err) resolve(data); }); }) } } class Mapper { constructor (options = {}) { this.hooksPath = options.hooksPath; this.reportPath = options.reportPath; this.updatedHooksPath = options.updatedHooksPath; } start () { try { this.report = jsonfile.readFileSync(path.join(process.cwd(), this.reportPath)); this.hooks = json.parse(fs.readFileSync(path.join(process.cwd(), this.hooksPath), 'utf8')); } catch (err) { console.error('Failed to load json files'); throw err } let flat = []; this.report.forEach(rClass => { flat = flat.concat(rClass.methods, rClass.fields); }); flat.forEach(fieldOrMethod => { Object.keys(this.hooks) .forEach(hooksKey => { if (fieldOrMethod.name === hooksKey) { console.log('Matched', fieldOrMethod.name); if (fieldOrMethod.matches.length > 1) console.warn('Multiple matches for', hooksKey, 'using first'); this.hooks[hooksKey] = fieldOrMethod.matches[0]; } }); }); fs.writeFileSync(path.join(process.cwd(), this.updatedHooksPath), json.stringify(this.hooks, null, 2)); } } var version = "0.1.0"; const cmd = require('commander'); cmd .version(version) .option('-s, --settings <settings-file>', 'Specify settings') .option('--save <report.json>', 'Save report') .option('-v, --verbose', 'Show stacktraces'); cmd .command('find <base-folder>') .description('search in the specified directory') .action((basefolderOpt) => { let basefolder, targets; try { const settings = require(path.join(process.cwd(), cmd.settings || 'hookfinder.settings')); basefolder = path.join(basefolderOpt, settings.PACKAGE_BASE); targets = settings.targets; } catch (err) { console.error('Failed to load settings file'); if (cmd.verbose) console.error(err); return } new Hookfinder({ basefolder: basefolder, targets, saveReport: cmd.save }).start() .catch(err => { console.error(err); }); }); cmd .command('map <report.json> <hooks.json> <updated-hooks.json>') .description('try to map report results to the oddly specific JodelXposed hooks.json format') .action((reportPath, hooksPath, updatedHooksPath) => __async(function*(){ try { let mapper = new Mapper({ hooksPath, reportPath, updatedHooksPath }); yield mapper.start(); } catch (err) { console.error(err); } }())); cmd.parse(process.argv);