#!/usr/bin/env node const program = require('commander') const chalk = require('chalk') const log = require('./util/log') const report = require('./reports') const matcher = require('./util/matcher') const Group = report.Group const core = require('./index') const packageJSON = require('../package.json') const { runSingleReport, runSwitchedReport } = require('./util/report_runner') var formatters = { 'json': require('./formatters/json'), 'table': require('./formatters/table'), 'raw': require('./formatters/raw-json'), 'raw-json': require('./formatters/raw-json'), 'csv': require('./formatters/csv'), 'raw-csv': require('./formatters/raw-csv') } process.on('unhandledRejection', (e) => { console.log('[cli.js] unhandled rejection', e) process.exit(1) }) program .version(packageJSON.version) .option('-l, --list', 'List Backups') .option(`-b, --backup `, 'Backup ID') .option(`-d, --dir `, `Backup Directory (default: ${core.base})`) .option('-r, --report ', 'Select a report type. see below for a full list.') .option('-i, --id ', 'Specify an ID for filtering certain reports') .option('-f, --formatter ', 'Specify output format. default: table') .option(`-e, --extract `, 'Extract data for commands. supported by: voicemail-files, manifest, addressbook') .option('-o, --output ', 'Specify an output directory for files to be written to.') .option(`-v, --verbose`, 'Verbose debugging output') .option(` --plugins `, 'List of pluging modules to use') .option(` --filter `, 'Filters output for individual reports.') .option(` --regex-filter `, 'Filters output for individual reports using a regular expression.') .option(' --join-reports', 'Join JSON reports together. (available for -f json or -f raw only!)') .option(` --no-color`, 'Disable colorized output') .option(` --dump`, 'alias for "--formatter raw"') .option(` --quiet`, 'quiet all messages, except for errors and raw output') .option(` --available`, 'output a list of available reports') program.on('--help', function () { console.log('') console.log(`Version: ${packageJSON.version}`) console.log('') console.log('Run ibackuptool --available for a listing of report types.') console.log('') console.log("If you're interested to know how this works, check out my post:") console.log('https://www.richinfante.com/2017/3/16/reverse-engineering-the-ios-backup') console.log('') console.log('Issue tracker:') console.log('https://github.com/richinfante/iphonebackuptools/issues') console.log('') }) function printReportList () { // Print a report group and recursively print children function printGroup (group, i, pn) { i = i || 0 pn = pn || '' for (let [name, report] of Object.entries(group)) { // Ignore groups if (name === '__group') { continue } if (report instanceof Group || report.__group === true) { // console.log(`${' '.repeat(i * 2)}- ${pn}${name}`.padStart(i * 2)) printGroup(report, i + 1, `${pn}${name}.`) } else { console.log(`- ${chalk.green(pn + name)}${report.deprecated ? chalk.gray(' [deprecated]') : ''}: ${report.description} `) } } } printGroup(core.modules) } process.on('unhandledRejection', (e) => { console.log('[index.js] unhandled rejection', e) process.exit(1) }) // If we're the main module, run some things. if (require.main === module) { init() } // Initialize the tool. function init () { program.parse(process.argv) if (program.available) { printReportList() process.exit(0) } // Set the log level log.setVerbose(program.quiet ? 0 : (program.verbose ? 2 : 1)) // Parse plugins program.plugins = program.plugins || process.env.IBACKUPTOOL_PLUGINS || '' program.plugins = program.plugins.split(',') .map(name => name.trim()) .filter(name => name !== '') .map(path => require(path)) // Register witht the core module core.registerModule(program.plugins) log.verbose('Plugins:', program.plugins) // Save the formatter program.formatter = formatters[program.formatter] || formatters.table // Legacy support for `--dump` flag. if (program.dump) { program.formatter = formatters.raw } // Disable color for non-ttys. if (!process.stdout.isTTY) { program.color = false } // Find the base if it is set if (program.dir) { core.base = program.dir } log.verbose('Base Directory:', core.base) main().then(() => {}) } /** * Main CLI function */ async function main () { // Legacy support for --list (-l) flag if (program.list) { program.report = 'backups.list' } log.verbose('Top-level modules', Object.keys(core.modules)) if (program.report) { var reportContents = [] // Turn the report argument into an array of report type names var selectedTypes = program.report .split(',') .map(el => el.trim()) .filter(el => el !== '') let selectedReports = [] // Match an array of reports. for (let query of selectedTypes) { selectedReports = [ ...selectedReports, ...matcher(core.modules, query, (el) => !(el instanceof Group || el.__group === true)) ] } let set = new Set(selectedReports) log.verbose('selected set', set) // If the set's size is 0, throw an error. if (set.size === 0) { log.error(`Couldn't run reports specified by: '${program.report}'.`) log.error(`No matching reports were found.`) process.exit(1) } // Iterate over each item in the set. for (let report of set.values()) { if (report.version && report.version >= 3) { try { log.begin('run', report.name) var filters = program.filter || []; for (var filter of program.regexFilter || []) filters.push(new RegExp(filter)); // Create some parameters to send by default. let params = { backup: program.backup, extract: program.extract, filter: filters, id: program.id, raw: !!program.formatter.isRaw } // Run a v3 report. let contents = await core.runReport(report, params) // Possibly symbolicate contents = await core.compileReport(report, contents, params) // Format the v3 report's result. let formattedContent = program.formatter.format(contents, { program }) // Push onto the list to be compiled. reportContents.push({ name: report.name, contents: formattedContent }) log.end() } catch (e) { log.end() log.error(`Couldn't run '${report.name}'.`) log.error(e) } } else { // Older reports still work for file and screen output // They just may not be as compatible // They cannot be called from the module's api. if (selectedReports.length > 1 && !report.usesPromises) { log.warning('the report', report.name, 'does not utilize promises.') log.warning('this may not work') } log.begin('run', report.name) // Check if there's a backup specified and one is required. if (report.requiresBackup) { if (!program.backup) { log.error('use -b or --backup to specify backup.') process.exit(1) } } try { if (report.func) { let contents = await runSingleReport(report, program) if (contents == null) { log.end(); continue } reportContents.push({ name: report.name, contents: contents }) } else if (report.functions) { let contents = await runSwitchedReport(report, program) if (contents == null) { log.end(); continue } reportContents.push({ name: report.name, contents: contents }) } } catch (e) { log.error(`Couldn't run '${report.name}'.`) log.error(e) } log.end() } } program.formatter.finalReport(reportContents, program) } else { program.outputHelp() } }