hookfinder
Version:
Find hooks when working with Xposed development
188 lines (168 loc) • 5.88 kB
JavaScript
;
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);