Pārlūkot izejas kodu

Release 3.0 (#18)

Add ESLint to check code conventions
Add tests to package.json
Add tests for mac address parser
Add ability to derive filename hashes from name and domain
Add tests for filename hash generation
Update code to match conventions
Update README
Update Example report module
Add Contributing.md
Updated command-line output formatting to be a bit more readable.
Update `cookies` report to use custom-built parser
Add log module to allow for more verbose output / quiet output
Rename report-output flag to match docs
Fix require for file hash
Add travis CI
Add better logging library
Add better error handling
Fix manifest export requiring a filter
Update README
Add custom-built cookie parser
Remove binary-plist dependency
Add more fields to cookie report
Add `--quiet` flag to quiet all messages but errors and raw output.
Rich Infante 7 gadi atpakaļ
vecāks
revīzija
eaefcc7361

+ 205 - 0
.eslintrc.json

@@ -0,0 +1,205 @@
+{
+    "parserOptions": {
+      "ecmaVersion": 8,
+      "ecmaFeatures": {
+        "experimentalObjectRestSpread": true,
+        "jsx": true
+      },
+      "sourceType": "module"
+    },
+  
+    "env": {
+      "es6": true,
+      "node": true
+    },
+  
+    "plugins": [
+      "import",
+      "node",
+      "promise",
+      "standard"
+    ],
+  
+    "globals": {
+      "document": false,
+      "navigator": false,
+      "window": false
+    },
+  
+    "rules": {
+      "accessor-pairs": "error",
+      "arrow-spacing": ["error", { "before": true, "after": true }],
+      "block-spacing": ["error", "always"],
+      "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
+      "camelcase": ["error", { "properties": "never" }],
+      "comma-dangle": ["error", {
+        "arrays": "never",
+        "objects": "never",
+        "imports": "never",
+        "exports": "never",
+        "functions": "never"
+      }],
+      "comma-spacing": ["error", { "before": false, "after": true }],
+      "comma-style": ["error", "last"],
+      "constructor-super": "error",
+      "curly": ["error", "multi-line"],
+      "dot-location": ["error", "property"],
+      "eol-last": "error",
+      "eqeqeq": ["error", "always", { "null": "ignore" }],
+      "func-call-spacing": ["error", "never"],
+      "generator-star-spacing": ["error", { "before": true, "after": true }],
+      "handle-callback-err": ["error", "^(err|error)$" ],
+      "indent": ["error", 2, {
+        "SwitchCase": 1,
+        "VariableDeclarator": 1,
+        "outerIIFEBody": 1,
+        "MemberExpression": 1,
+        "FunctionDeclaration": { "parameters": 1, "body": 1 },
+        "FunctionExpression": { "parameters": 1, "body": 1 },
+        "CallExpression": { "arguments": 1 },
+        "ArrayExpression": 1,
+        "ObjectExpression": 1,
+        "ImportDeclaration": 1,
+        "flatTernaryExpressions": false,
+        "ignoreComments": false
+      }],
+      "key-spacing": ["error", { "beforeColon": false, "afterColon": true }],
+      "keyword-spacing": ["error", { "before": true, "after": true }],
+      "new-cap": ["error", { "newIsCap": true, "capIsNew": false }],
+      "new-parens": "error",
+      "no-array-constructor": "error",
+      "no-caller": "error",
+      "no-class-assign": "error",
+      "no-compare-neg-zero": "error",
+      "no-cond-assign": "error",
+      "no-const-assign": "error",
+      "no-constant-condition": ["error", { "checkLoops": false }],
+      "no-control-regex": "error",
+      "no-debugger": "error",
+      "no-delete-var": "error",
+      "no-dupe-args": "error",
+      "no-dupe-class-members": "error",
+      "no-dupe-keys": "error",
+      "no-duplicate-case": "error",
+      "no-empty-character-class": "error",
+      "no-empty-pattern": "error",
+      "no-eval": "error",
+      "no-ex-assign": "error",
+      "no-extend-native": "error",
+      "no-extra-bind": "error",
+      "no-extra-boolean-cast": "error",
+      "no-extra-parens": ["error", "functions"],
+      "no-fallthrough": "error",
+      "no-floating-decimal": "error",
+      "no-func-assign": "error",
+      "no-global-assign": "error",
+      "no-implied-eval": "error",
+      "no-inner-declarations": ["error", "functions"],
+      "no-invalid-regexp": "error",
+      "no-irregular-whitespace": "error",
+      "no-iterator": "error",
+      "no-label-var": "error",
+      "no-labels": ["error", { "allowLoop": false, "allowSwitch": false }],
+      "no-lone-blocks": "error",
+      "no-mixed-operators": ["error", {
+        "groups": [
+          ["==", "!=", "===", "!==", ">", ">=", "<", "<="],
+          ["&&", "||"],
+          ["in", "instanceof"]
+        ],
+        "allowSamePrecedence": true
+      }],
+      "no-mixed-spaces-and-tabs": "error",
+      "no-multi-spaces": "error",
+      "no-multi-str": "error",
+      "no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0 }],
+      "no-negated-in-lhs": "error",
+      "no-new": "error",
+      "no-new-func": "error",
+      "no-new-object": "error",
+      "no-new-require": "error",
+      "no-new-symbol": "error",
+      "no-new-wrappers": "error",
+      "no-obj-calls": "error",
+      "no-octal": "error",
+      "no-octal-escape": "error",
+      "no-path-concat": "error",
+      "no-proto": "error",
+      "no-redeclare": "error",
+      "no-regex-spaces": "error",
+      "no-return-assign": ["error", "except-parens"],
+      "no-return-await": "error",
+      "no-self-assign": "error",
+      "no-self-compare": "error",
+      "no-sequences": "error",
+      "no-shadow-restricted-names": "error",
+      "no-sparse-arrays": "error",
+      "no-tabs": "error",
+      "no-template-curly-in-string": "error",
+      "no-this-before-super": "error",
+      "no-throw-literal": "error",
+      "no-trailing-spaces": "error",
+      "no-undef": "error",
+      "no-undef-init": "error",
+      "no-unexpected-multiline": "error",
+      "no-unmodified-loop-condition": "error",
+      "no-unneeded-ternary": ["error", { "defaultAssignment": false }],
+      "no-unreachable": "error",
+      "no-unsafe-finally": "error",
+      "no-unsafe-negation": "error",
+      "no-unused-expressions": ["error", { "allowShortCircuit": true, "allowTernary": true, "allowTaggedTemplates": true }],
+      "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": true }],
+      "no-use-before-define": ["error", { "functions": false, "classes": false, "variables": false }],
+      "no-useless-call": "error",
+      "no-useless-computed-key": "error",
+      "no-useless-constructor": "error",
+      "no-useless-escape": "error",
+      "no-useless-rename": "error",
+      "no-useless-return": "error",
+      "no-whitespace-before-property": "error",
+      "no-with": "error",
+      "object-property-newline": ["error", { "allowMultiplePropertiesPerLine": true }],
+      "one-var": ["error", { "initialized": "never" }],
+      "operator-linebreak": ["error", "after", { "overrides": { "?": "before", ":": "before" } }],
+      "padded-blocks": ["error", { "blocks": "never", "switches": "never", "classes": "never" }],
+      "prefer-promise-reject-errors": "error",
+      "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }],
+      "rest-spread-spacing": ["error", "never"],
+      "semi": ["error", "never"],
+      "semi-spacing": ["error", { "before": false, "after": true }],
+      "space-before-blocks": ["error", "always"],
+      "space-before-function-paren": ["error", "always"],
+      "space-in-parens": ["error", "never"],
+      "space-infix-ops": "error",
+      "space-unary-ops": ["error", { "words": true, "nonwords": false }],
+      "spaced-comment": ["error", "always", {
+        "line": { "markers": ["*package", "!", "/", ",", "="] },
+        "block": { "balanced": true, "markers": ["*package", "!", ",", ":", "::", "flow-include"], "exceptions": ["*"] }
+      }],
+      "symbol-description": "error",
+      "template-curly-spacing": ["error", "never"],
+      "template-tag-spacing": ["error", "never"],
+      "unicode-bom": ["error", "never"],
+      "use-isnan": "error",
+      "valid-typeof": ["error", { "requireStringLiterals": true }],
+      "wrap-iife": ["error", "any", { "functionPrototypeMethods": true }],
+      "yield-star-spacing": ["error", "both"],
+      "yoda": ["error", "never"],
+  
+      "import/export": "error",
+      "import/first": "error",
+      "import/no-duplicates": "error",
+      "import/no-webpack-loader-syntax": "error",
+  
+      "node/no-deprecated-api": "error",
+      "node/process-exit-as-throw": "error",
+  
+      "promise/param-names": "error",
+  
+      "standard/array-bracket-even-spacing": ["error", "either"],
+      "standard/computed-property-even-spacing": ["error", "even"],
+      "standard/no-callback-literal": "error",
+      "standard/object-curly-even-spacing": ["error", "either"]
+    }
+  }
+  

+ 7 - 0
.travis.yml

@@ -0,0 +1,7 @@
+language: node_js
+node_js:
+  - '8'
+  - '9'
+  - 'node'
+before_script:
+  - npm install -g tap

+ 11 - 0
Contributing.md

@@ -0,0 +1,11 @@
+# Contributing
+
+This project is designed to be able to support new kinds of reporting easily without modifying it's core. This project relies on contributions to make give it as many features as possible! If you're able to, 
+
+## Pull requests
+If you've built a new reporting type or added a feature, feel free to open a pull request, and request a review. This way, it's easier to review potential additions.
+
+## Issues
+If you're encountering an issue getting a report to work, it's possible that it has't been tested against the specific iOS version that created the backup. 
+
+When adding an issue for a bug report, be sure to include the iOS version that causes it.

+ 44 - 24
Readme.md

@@ -1,4 +1,9 @@
 # iPhone backup tools
+
+[![Build Status](https://travis-ci.org/richinfante/iphonebackuptools.svg?branch=master)](https://travis-ci.org/richinfante/iphonebackuptools)
+[![npm](https://img.shields.io/npm/v/ibackuptool.svg)](http://npmjs.com/ibackuptool)
+![license](https://img.shields.io/github/license/richinfante/iphonebackuptools.svg)
+
 Are _you_ storing unencrypted iPhone backups on your personal computer? With very little effort, we can dump **all** the saved messages from the backup, as well as notes, photo locations, and other data. 
 
 Check out my recently updated post about my work on backups here: [Reverse Engineering the iOS Backup](https://www.richinfante.com/2017/3/16/reverse-engineering-the-ios-backup)
@@ -8,9 +13,7 @@ Check out my recently updated post about my work on backups here: [Reverse Engin
 Currently works on macOS, not tested on windows but should work on windows by setting the attribute `--dir` to the backups directory location.
 
 ## iOS Support
-- iOS 9 - backup version: `2.4`
-- iOS 10 - backup version: `3.2`
-- iOS 11 - backup version: `3.2`
+iOS Support depends on the individual reporting types and which files are specifically present inside of the backup. When a report type is not supported, an error message is outputted to the terminal. Some reports, such as the manifest report, support iOS versions as early as iOS 5 to the latest iOS (iOS 11 as of this writing).
 
 ## Installing
 
@@ -44,8 +47,11 @@ UDID="0c1bc52c50016933679b0980ccff3680e5831162"
 - Current types:
     - `apps` - List all installed applications and container IDs.
     - `calls` - List all call records contained in the backup.
+    - `calls-statistics` List call statistics
     - `conversations` - List all SMS and iMessage conversations
-    - `list` - List of all backups. alias for -l
+    - `conversations_full` - Output raw message contents along with conversations. Requires `-f raw-json` or `--dump`.
+    - `cookies` - List cookie domains for all apps
+    - `list` - List of all backups. alias for `-l`. Does not require the `-b` flag.
     - `manifest` - List all the files contained in the backup (iOS 10+)
     - `messages` - List all SMS and iMessage messages in a conversation. This requires using the `--id` flag to specify a conversation to inspect.
     - `notes` - List all iOS notes
@@ -96,6 +102,18 @@ ibackuptool -b $UDID --report manifest --extract BACKUP --filter all
 ibackuptool -b $UDID --report manifest --extract BACKUP --filter CameraRollDomain
 ```
 
+### Messages Access
+
+```bash
+# List of all conversations, indexed by ID.
+# Each row starts with an ID number, which is needed for the next step.
+ibackuptool -b $UDID -r conversations
+
+# Now, Fetch the messages with the following command
+# Replace $CONVERSATION_ID with a row ID from `ibackuptool -b $UDID -r conversations`
+ibackuptool -b $UDID -r messages --id $CONVERSATION_ID
+```
+
 ### Reporting formats
 iBackupTool now supports multiple kinds of data export:
 - `table` - Selected data columns in an ascii table
@@ -106,47 +124,49 @@ Additionally, there are more comprehensive export functions that will export ALL
 - `raw-csv` - Full-data CSV export from each of the tables.
 - `raw`, `raw-json` - Full-data JSON export from each of the tables. This output can be quite large.
 
-### Multiple-Reporting 
+#### Multiple-Reporting 
 You can also provide a comma separated list of reports to generate. Additionally, there is a special `all` report type which will run all available reports. This is best paired with the `-o` option for saving to disk and the `-f` option for selecting a format such as CSV, or JSON.
 
 ```bash
+# Report wifi, calls, voicemail
 ibackuptool -b $UDID --report wifi,calls,voicemail
-```
 
-the `-o` option specifies a folder to export reports to:
-```bash
-ibackuptool -b $UDID --report wifi,calls,voicemail -o exported
+# Report all possible
+ibackuptool -b $UDID --report all
 ```
 
 #### Joined Reports
 Additionally, for the `json` and `raw-json` types, there's a `--join-reports` flag which will merge all of the data into a single JSON file, where the top level object has a key for each report type that is selected.
 
+```bash
+# Generate both wifi and calls reports, joined as JSON
+ibackuptool -b $UDID -r wifi,calls -f json --join-reports
+```
 
-### Messages Access
+### Output to disk
+the `-o <path>` (`--output <path>`option specifies a folder to export reports to. If the directory does not exist, it will be created. For joined JSON reports, a single json file is exported instead of multiple files in a directory.
 
 ```bash
-# List of all conversations, indexed by ID.
-# Each row starts with an ID number, which is needed for the next step.
-ibackuptool -b $UDID --report conversations
-
-# Now, Fetch the messages with the following command
-# Replace $CONVERSATION_ID with a row ID from `ibackuptool -b $UDID --conversations`
-ibackuptool -b $UDID --report messages --id $CONVERSATION_ID
+# Export wifi, calls, voicemail as CSV files to disk in a folder named "exported/"
+ibackuptool -b $UDID --report wifi,calls,voicemail -f csv -o exported
 ```
+## Running Tests
+first, install [tap](https://www.npmjs.com/package/tap)
+
+next, run `npm test`.
 
-## Need More Data?
-- !! This will cause the program to output **Everything** to STDOUT as formatted JSON. !!
-- Append the `--dump` flag to the program.
-- I'd recommend piping this output to a file.
+## Important!
+You should make a backup of the backups you look at using this tool, even though they are opened as read-only, you should still do that do you don't accidentally do something to lose data.
 
-- You should make a backup of the backups you look at using this tool, even though they are opened as read-only, you should still do that do you don't accidentally do something to lose data.
+## Contributing
+See [Contributing.md](Contributing.md)
 
 ## TODO
-- Contact name lookup for newer iOS10, iOS11 backups
+See [Roadmap](https://github.com/richinfante/iphonebackuptools/wiki/Roadmap-and-Vision)
 
 ## Legal
 
-Copyright &copy; 2017 Richard Infante.
+Copyright &copy; 2017-2018 Richard Infante.
 
 Available under the MIT License.
 

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 777 - 73
package-lock.json


+ 6 - 2
package.json

@@ -4,7 +4,7 @@
   "description": "Read Messages and info from iOS Backups",
   "main": "tools/index.js",
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1",
+    "test": "tap tests",
     "start": "node tools/index.js"
   },
   "bin": {
@@ -15,7 +15,6 @@
   "author": "@richinfante",
   "license": "MIT",
   "dependencies": {
-    "binary-cookies": "^0.1.1",
     "bplist-parser": "^0.1.1",
     "buffer-reader": "^0.1.0",
     "chalk": "^1.1.3",
@@ -28,6 +27,11 @@
     "zpad": "^0.5.0"
   },
   "devDependencies": {
+    "eslint": "^4.18.1",
+    "eslint-plugin-import": "^2.9.0",
+    "eslint-plugin-node": "^6.0.1",
+    "eslint-plugin-promise": "^3.6.0",
+    "eslint-plugin-standard": "^3.0.1",
     "tap": "^11.1.0"
   }
 }

+ 12 - 0
tests/test_filehash.js

@@ -0,0 +1,12 @@
+const tap = require('tap')
+const fileHash = require('../tools/util/backup_filehash')
+
+tap.test('hashes', function (childTest) {
+  childTest.equal(fileHash('Library/SMS/sms.db'), '3d0d7e5fb2ce288813306e4d4636395e047a3d28')
+
+  childTest.equal(
+    fileHash('SystemConfiguration/com.apple.wifi.plist', 'SystemPreferencesDomain'), 'ade0340f576ee14793c607073bd7e8e409af07a8'
+  )
+
+  childTest.end()
+})

+ 9 - 0
tests/test_macaddress.js

@@ -0,0 +1,9 @@
+const tap = require('tap')
+const macParse = require('../tools/util/mac_address_parse')
+
+tap.test('mac-normalize', function (childTest) {
+  childTest.equal(macParse.pad_zeros('ff:1:22:10'), 'ff:01:22:10')
+  childTest.equal(macParse.pad_zeros('ff:01:22:10'), 'ff:01:22:10')
+
+  childTest.end()
+})

+ 9 - 9
tests/test_version.js

@@ -2,12 +2,12 @@ const tap = require('tap')
 const version = require('../tools/util/version_compare')
 
 tap.test('version', function (childTest) {
-    childTest.equal(version.versionCheck('9.1', '=9.1'), true)
-    childTest.equal(version.versionCheck('9.1', '<=9.1'), true)
-    childTest.equal(version.versionCheck('9.1', '>=9.1'), true)
-    childTest.equal(version.versionCheck('10.0', '<=9.1'), false)
-    childTest.equal(version.versionCheck('10.0', '<9.1'), false)
-    childTest.equal(version.versionCheck('10.0', '>9.1'), true)
-    childTest.equal(version.versionCheck('10.0', '>=9.1'), true)
-    childTest.end()
-})
+  childTest.equal(version.versionCheck('9.1', '=9.1'), true)
+  childTest.equal(version.versionCheck('9.1', '<=9.1'), true)
+  childTest.equal(version.versionCheck('9.1', '>=9.1'), true)
+  childTest.equal(version.versionCheck('10.0', '<=9.1'), false)
+  childTest.equal(version.versionCheck('10.0', '<9.1'), false)
+  childTest.equal(version.versionCheck('10.0', '>9.1'), true)
+  childTest.equal(version.versionCheck('10.0', '>=9.1'), true)
+  childTest.end()
+})

+ 15 - 14
tools/formatters/_default.js

@@ -1,18 +1,19 @@
 const fs = require('fs-extra')
 const path = require('path')
+const log = require('../util/log')
 
-module.exports.finalReport = async function(reports, program) {
-    if (program.reportOutput === undefined) {
-      return
-    }
-    
-    // Ensure the output directory exists.
-    fs.ensureDirSync(program.reportOutput)
+module.exports.finalReport = async function (reports, program) {
+  if (program.output === undefined) {
+    return
+  }
 
-    // Write each report to the disk
-    for(var report of reports) {
-      var outPath = path.join(program.reportOutput, report.name + '.json')
-      console.log('saving', outPath)
-      fs.writeFileSync(outPath, JSON.stringify(report.contents), 'utf8')
-    }
-  }
+  // Ensure the output directory exists.
+  fs.ensureDirSync(program.output)
+
+  // Write each report to the disk
+  for (var report of reports) {
+    var outPath = path.join(program.output, report.name + '.json')
+    log.action('saving', outPath)
+    fs.writeFileSync(outPath, JSON.stringify(report.contents), 'utf8')
+  }
+}

+ 21 - 25
tools/formatters/csv.js

@@ -1,50 +1,46 @@
 const json2csv = require('json2csv')
-
+const fs = require('fs-extra')
+const path = require('path')
+const log = require('../util/log')
 
 module.exports.format = function (data, options) {
-  var data = data.map(el => {
+  var processedData = data.map(el => {
     var row = {}
-    
+
     // Iterate over the columns and add each item to the new row.
     for (var key in options.columns) {
       row[key] = options.columns[key](el)
     }
-    
+
     return row
   })
 
-  const csv = json2csv({ data })
-  
-  
-  if(options.program) {
+  const csv = json2csv({ data: processedData })
+
+  if (options.program) {
     // If reporting output is defined, ignore console log here.
-    if (options.program.reportOutput === undefined) {
-      console.log(csv)
+    if (options.program.output === undefined) {
+      log.raw(csv)
     }
   } else {
-    console.log(csv)
+    log.raw(csv)
   }
-  
-  
+
   return csv
 }
 
-const fs = require('fs-extra')
-const path = require('path')
-
-
-module.exports.finalReport = async function(reports, program) {
-  if (program.reportOutput === undefined) {
+module.exports.finalReport = async function (reports, program) {
+  if (program.output === undefined) {
     return
   }
-  
+
   // Ensure the output directory exists.
-  fs.ensureDirSync(program.reportOutput)
+  fs.ensureDirSync(program.output)
 
   // Write each report to the disk
-  for(var report of reports) {
-    var outPath = path.join(program.reportOutput, report.name + '.csv')
-    console.log('saving', outPath)
+  for (var report of reports) {
+    var outPath = path.join(program.output, report.name + '.csv')
+    log.action('saving', outPath)
     fs.writeFileSync(outPath, report.contents, 'utf8')
   }
-}
+}

+ 31 - 31
tools/formatters/json.js

@@ -1,64 +1,64 @@
+const fs = require('fs-extra')
+const path = require('path')
+const log = require('../util/log')
+
 module.exports.format = function (data, options) {
-  var data = data.map(el => {
+  var processedData = data.map(el => {
     var row = {}
-    
+
     // Iterate over the columns and add each item to the new row.
     for (var key in options.columns) {
       row[key] = options.columns[key](el)
     }
-    
+
     return row
   })
-  
+
   // Strigify the output, using 2 space indent.
-  var output = JSON.stringify(data, null, 2)
-  
-  if(options.program) {
+  var output = JSON.stringify(processedData, null, 2)
+
+  if (options.program) {
     // If reporting output is defined, ignore console log here.
-    if (options.program.reportOutput === undefined) {
-      console.log(output)
+    if (options.program.output === undefined) {
+      log.raw(output)
     } else {
-      return data
+      return processedData
     }
   } else {
-    console.log(output)
+    log.raw(output)
   }
-  
-  return data
-}
 
-const fs = require('fs-extra')
-const path = require('path')
+  return processedData
+}
 
-module.exports.finalReport = async function(reports, program) {
-  if (program.reportOutput === undefined) {
+module.exports.finalReport = async function (reports, program) {
+  if (program.output === undefined) {
     return
   }
 
   if (program.joinReports) {
     var out = {}
 
-    for(var report of reports) {
-      console.log('saving report', report.name)
+    for (var report of reports) {
+      log.action('compiling', report.name)
       out[report.name] = report.contents
     }
 
-    if (program.reportOutput == '-') {
-      console.log(JSON.stringify(out, null, 2))
+    if (program.output === '-') {
+      log.raw(JSON.stringify(out, null, 2))
     } else {
-      // fs.ensureDirSync(path.dirname(program.reportOutput))
-      //fs.copySync(sourceFile, outDir)
-      var outPath = program.reportOutput + '.json'
-      console.log('writing joined to', outPath)
+      // fs.ensureDirSync(path.dirname(program.output))
+      // fs.copySync(sourceFile, outDir)
+      let outPath = program.output + '.json'
+      log.action('saving', outPath)
       fs.writeFileSync(outPath, JSON.stringify(out, null, 2), 'utf8')
     }
   } else {
-    console.log(program.reportOutput)
-    fs.ensureDirSync(program.reportOutput)
+    fs.ensureDirSync(program.output)
 
-    for(var report of reports) {
-      var outPath = path.join(program.reportOutput, report.name + '.json')
-      console.log('saving', outPath)
+    for (let report of reports) {
+      let outPath = path.join(program.output, report.name + '.json')
+      log.action('saving', outPath)
       fs.writeFileSync(outPath, JSON.stringify(report.contents, null, 2), 'utf8')
     }
   }

+ 15 - 18
tools/formatters/raw-csv.js

@@ -1,39 +1,36 @@
 const json2csv = require('json2csv')
-
+const log = require('../util/log')
 
 module.exports.format = function (data, options) {
   const csv = json2csv({ data })
-  
-  
-  if(options.program) {
+
+  if (options.program) {
     // If reporting output is defined, ignore console log here.
-    if (options.program.reportOutput === undefined) {
-      console.log(csv)
+    if (options.program.output === undefined) {
+      log.raw(csv)
     }
   } else {
-    console.log(csv)
+    log.raw(csv)
   }
-  
-  
+
   return csv
 }
 
 const fs = require('fs-extra')
 const path = require('path')
 
-
-module.exports.finalReport = async function(reports, program) {
-  if (program.reportOutput === undefined) {
+module.exports.finalReport = async function (reports, program) {
+  if (program.output === undefined) {
     return
   }
-  
+
   // Ensure the output directory exists.
-  fs.ensureDirSync(program.reportOutput)
+  fs.ensureDirSync(program.output)
 
   // Write each report to the disk
-  for(var report of reports) {
-    var outPath = path.join(program.reportOutput, report.name + '.csv')
-    console.log('saving', outPath)
+  for (var report of reports) {
+    var outPath = path.join(program.output, report.name + '.csv')
+    log.action('saving', outPath)
     fs.writeFileSync(outPath, report.contents, 'utf8')
   }
-}
+}

+ 21 - 20
tools/formatters/raw-json.js

@@ -1,15 +1,17 @@
+const log = require('../util/log')
+
 module.exports.format = function (data, options) {
   var output = JSON.stringify(data)
 
-  if(options.program) {
+  if (options.program) {
     // If reporting output is defined, ignore console log here.
-    if (options.program.reportOutput === undefined) {
-      console.log(output)
+    if (options.program.output === undefined) {
+      log.raw(output)
     } else {
-        return data
+      return data
     }
   } else {
-    console.log(output)
+    log.raw(output)
   }
 
   return data
@@ -18,38 +20,37 @@ module.exports.format = function (data, options) {
 const fs = require('fs-extra')
 const path = require('path')
 
-module.exports.finalReport = async function(reports, program) {
-  if (program.reportOutput === undefined) {
+module.exports.finalReport = async function (reports, program) {
+  if (program.output === undefined) {
     return
   }
 
   if (program.joinReports) {
     var out = {}
 
-    for(var report of reports) {
-      console.log('saving report', report.name)
+    for (var report of reports) {
+      log.action('compiling', report.name)
       out[report.name] = report.contents
     }
 
-    if (program.reportOutput == '-') {
-      console.log(JSON.stringify(out, null, 2))
+    if (program.output === '-') {
+      log.raw(JSON.stringify(out, null, 2))
     } else {
-      // fs.ensureDirSync(path.dirname(program.reportOutput))
-      //fs.copySync(sourceFile, outDir)
-      var outPath = program.reportOutput + '.json'
-      console.log('writing joined to', outPath)
+      // fs.ensureDirSync(path.dirname(program.output))
+      // fs.copySync(sourceFile, outDir)
+      let outPath = program.output + '.json'
+      log.action('compiling', outPath)
       fs.writeFileSync(outPath, JSON.stringify(out), 'utf8')
     }
   } else {
     // Ensure the output directory exists.
-    fs.ensureDirSync(program.reportOutput)
+    fs.ensureDirSync(program.output)
 
     // Write each report to the disk
-    for(var report of reports) {
-      var outPath = path.join(program.reportOutput, report.name + '.json')
-      console.log('saving', outPath)
+    for (let report of reports) {
+      let outPath = path.join(program.output, report.name + '.json')
+      log.action('saving', outPath)
       fs.writeFileSync(outPath, JSON.stringify(report.contents), 'utf8')
     }
   }
 }
-

+ 55 - 57
tools/formatters/table.js

@@ -1,4 +1,5 @@
 const stripAnsi = require('strip-ansi')
+const log = require('../util/log')
 
 function normalizeCols (rows, max) {
   function padEnd (string, maxLength, fillString) {
@@ -12,17 +13,17 @@ function normalizeCols (rows, max) {
   var widths = []
   max = max || rows[0].length
 
-  for (var i = 0; i < rows.length; i++) {
-    for (var j = 0; j < rows[i].length && j < max; j++) {
+  for (let i = 0; i < rows.length; i++) {
+    for (let j = 0; j < rows[i].length && j < max; j++) {
       if (!widths[j] || widths[j] < (stripAnsi(rows[i][j]) + '').length) {
         widths[j] = (stripAnsi(rows[i][j] + '') + '').length
       }
     }
   }
 
-  for (var i = 0; i < rows.length; i++) {
-    for (var j = 0; j < rows[i].length && j < max; j++) {
-      if (rows[i][j] == '-') {
+  for (let i = 0; i < rows.length; i++) {
+    for (let j = 0; j < rows[i].length && j < max; j++) {
+      if (rows[i][j] === '-') {
         rows[i][j] = padEnd(rows[i][j], widths[j], '-')
       } else {
         rows[i][j] = padEnd(rows[i][j], widths[j], ' ')
@@ -33,68 +34,65 @@ function normalizeCols (rows, max) {
   return rows
 }
 
-function keyValueArray(columns, keys, obj) {
+function keyValueArray (columns, keys, obj) {
   return keys.map(el => columns[el](obj))
 }
 
 module.exports.format = function (data, options) {
-    // Keys for each column
-    var keys = []
-
-    // Separators for each column
-    var separators = []
-    
-    // Add to collection of keys
-    for(var key in options.columns) {
-      keys.push(key)
-      separators.push('-')
-    }
-
-    // Create the rows
-    var items = [
-        keys,
-        separators,
-        ...data.map(data => keyValueArray(options.columns, keys, data))
-    ]
-
-    // Normalize column widths.
-    items = normalizeCols(items).map(el => {
-      return el.join(' | ').replace(/\n/g, '')
-    }).join('\n')
-
-    
+  // Keys for each column
+  var keys = []
 
-    if(options.program) {
+  // Separators for each column
+  var separators = []
 
-      // Disable color output
-      if (!options.program.color) { items = stripAnsi(items) }
+  // Add to collection of keys
+  for (var key in options.columns) {
+    keys.push(key)
+    separators.push('-')
+  }
 
-      // If reporting output is defined, ignore console log here.
-      if (options.program.reportOutput === undefined) {
-        console.log(items)
-      }
-    } else {
-      console.log(items)
+  // Create the rows
+  var items = [
+    keys,
+    separators,
+    ...data.map(data => keyValueArray(options.columns, keys, data))
+  ]
+
+  // Normalize column widths.
+  items = normalizeCols(items).map(el => {
+    return el.join(' | ').replace(/\n/g, '')
+  }).join('\n')
+
+  if (options.program) {
+    // Disable color output
+    if (!options.program.color) { items = stripAnsi(items) }
+
+    // If reporting output is defined, ignore console log here.
+    if (options.program.output === undefined) {
+      log.raw(items)
     }
-    
-    return items
+  } else {
+    log.raw(items)
+  }
+
+  return items
 }
 
 const fs = require('fs-extra')
 const path = require('path')
 
-module.exports.finalReport = async function(reports, program) {
-    if (program.reportOutput === undefined) {
-      return
-    }
-    
-    // Ensure the output directory exists.
-    fs.ensureDirSync(program.reportOutput)
-
-    // Write each report to the disk
-    for(var report of reports) {
-      var outPath = path.join(program.reportOutput, report.name + '.txt')
-      console.log('saving', outPath)
-      fs.writeFileSync(outPath, report.contents, 'utf8')
-    }
-  }
+module.exports.finalReport = async function (reports, program) {
+  if (program.output === undefined) {
+    return
+  }
+
+  // Ensure the output directory exists.
+  fs.ensureDirSync(program.output)
+
+  // Write each report to the disk
+  for (var report of reports) {
+    var outPath = path.join(program.output, report.name + '.txt')
+    log.action('saving', outPath)
+    fs.writeFileSync(outPath, report.contents, 'utf8')
+  }
+}

+ 132 - 136
tools/index.js

@@ -5,6 +5,8 @@ const path = require('path')
 const chalk = require('chalk')
 const version = require('./util/version_compare')
 const iPhoneBackup = require('./util/iphone_backup.js').iPhoneBackup
+const log = require('./util/log')
+
 var base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/')
 
 var reportTypes = {
@@ -24,34 +26,34 @@ var reportTypes = {
   'webhistory': require('./reports/webhistory'),
   'calls_statistics': require('./reports/calls_statistics'),
   'wifi': require('./reports/wifi'),
-  'all': require('./reports/all'),
   'address_book': require('./reports/address_book')
 }
 
 var formatters = {
   'json': require('./formatters/json'),
   'table': require('./formatters/table'),
-  'raw':  require('./formatters/raw-json'),
+  'raw': require('./formatters/raw-json'),
   'raw-json': require('./formatters/raw-json'),
   'csv': require('./formatters/csv'),
-  'raw-csv': require('./formatters/raw-csv'),
+  'raw-csv': require('./formatters/raw-csv')
 }
 
 program
-.version('3.0.0')
-.option('-l, --list', 'List Backups')
-.option(`-b, --backup <backup>`, 'Backup ID')
-.option(`-d, --dir <directory>`, `Backup Directory (default: ${base})`)
-.option('-r, --report <report_type>', 'Select a report type. see below for a full list.')
-.option('-i, --id <id>', 'Specify an ID for filtering certain reports')
-.option('-f, --formatter <type>', 'Specify output format. default: table')
-.option(`-e, --extract <dir>`, 'Extract data for commands. supported by: voicemail-files, manifest')
-.option('-o, --report-output <path>', 'Specify an output directory for files to be written to.')
-.option(`-v, --verbose`, 'Verbose debugging output')
-.option(`    --filter <filter>`, 'Filter output for individual reports. See the README for usage.')
-.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"')
+  .version('3.0.0')
+  .option('-l, --list', 'List Backups')
+  .option(`-b, --backup <backup>`, 'Backup ID')
+  .option(`-d, --dir <directory>`, `Backup Directory (default: ${base})`)
+  .option('-r, --report <report_type>', 'Select a report type. see below for a full list.')
+  .option('-i, --id <id>', 'Specify an ID for filtering certain reports')
+  .option('-f, --formatter <type>', 'Specify output format. default: table')
+  .option(`-e, --extract <dir>`, 'Extract data for commands. supported by: voicemail-files, manifest')
+  .option('-o, --output <path>', 'Specify an output directory for files to be written to.')
+  .option(`-v, --verbose`, 'Verbose debugging output')
+  .option(`    --filter <filter>`, 'Filter output fo r individual reports. See the README for usage.')
+  .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')
 
 program.on('--help', function () {
   console.log('')
@@ -60,11 +62,7 @@ program.on('--help', function () {
   // Generate a list of report types.
   for (var i in reportTypes) {
     var r = reportTypes[i]
-    if (program.isTTY) {
-      console.log('  ', r.name, (r.supportedVersions ? '(iOS ' + r.supportedVersions + ')' : ' ') + '-', r.description)
-    } else {
-      console.log('  ', chalk.green(r.name), (r.supportedVersions ? chalk.gray('(iOS ' + r.supportedVersions + ') ') : '') + '-', r.description)
-    }
+    console.log('  ', chalk.green(r.name), (r.supportedVersions ? chalk.gray('(iOS ' + r.supportedVersions + ') ') : '') + '-', r.description)
   }
   console.log('')
   console.log("If you're interested to know how this works, check out my post:")
@@ -75,12 +73,15 @@ program.on('--help', function () {
   console.log('')
 })
 
+process.on('unhandledRejection', (e) => {
+  console.log('unhandled', e)
+  process.exit(1)
+})
+
 // Parse argv.
 program.parse(process.argv)
 
-// Global verbose output flag.
-// This is bad
-global.verbose = program.verbose
+log.setVerbose(program.quiet ? 0 : (program.verbose ? 2 : 1))
 
 // Save the formatter
 program.formatter = formatters[program.formatter] || formatters.table
@@ -96,28 +97,28 @@ if (!process.stdout.isTTY) { program.color = false }
 // Find the base
 base = program.dir || base
 
-if (program.verbose) console.log('Using source:', base)
+log.verbose('Using source:', base)
 
 // Run the main function
 main()
 
-async function main() {
+async function main () {
   if (program.list) {
     // Run the list report standalone
-    let result = await new Promise((resolve, reject) => {
+    await new Promise((resolve, reject) => {
       reportTypes.list.func(program, base, resolve, reject)
     })
   } else if (program.report) {
-    var reportContents = [] 
+    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 != '')
+      .filter(el => el !== '')
 
     // Add all types if type is 'all'
-    if (program.report == 'all') {
+    if (program.report === 'all') {
       selectedTypes = []
 
       for (var key in reportTypes) {
@@ -125,50 +126,57 @@ async function main() {
           continue
         }
 
-        selectedTypes.push(key)  
+        selectedTypes.push(key)
       }
     }
-    
-    for(var reportName of selectedTypes) {
+
+    for (var reportName of selectedTypes) {
       // If the report is valid
       if (reportTypes[reportName]) {
-        
         var report = reportTypes[reportName]
 
-        if(selectedTypes.length > 1 && !report.usesPromises) {
-          console.log('Warning: report that does not utilize promises in multi-request.')
-          console.log('Warning: this may not work.')
+        if (selectedTypes.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) {
-            console.log('use -b or --backup <id> to specify backup.')
+            log.error('use -b or --backup <id> to specify backup.')
             process.exit(1)
           }
         }
-        
-        if (report.func) {
-          var report = await runSingleReport(report, program)
-
-          reportContents.push({ 
-            name: reportName, 
-            contents: report
-          })
-        } else if (report.functions) {
-          var report = await runSwitchedReport(report, program)
-
-          reportContents.push({ 
-            name: reportName, 
-            contents: report
-          })
+        try {
+          if (report.func) {
+            let contents = await runSingleReport(report, program)
+            if (contents == null) { log.end(); continue }
+
+            reportContents.push({
+              name: reportName,
+              contents: contents
+            })
+          } else if (report.functions) {
+            let contents = await runSwitchedReport(report, program)
+            if (contents == null) { log.end(); continue }
+
+            reportContents.push({
+              name: reportName,
+              contents: contents
+            })
+          }
+        } catch (e) {
+          log.error(`Couldn't run '${report.name}'.`)
+          log.error(e)
         }
+
+        log.end()
       } else {
-        console.log('')
-        console.log('  [!] Unknown Option type:', reportName)
-        console.log('  [!] It\'s possible this tool is out-of date.')
-        console.log('  https://github.com/richinfante/iphonebackuptools/issues')
-        console.log('')
+        log.error('Unknown report type:', reportName)
+        log.error(`It's possible this tool is out-of date.`)
+        log.error(`https://github.com/richinfante/iphonebackuptools/issues`)
         program.outputHelp()
       }
     }
@@ -179,57 +187,63 @@ async function main() {
   }
 }
 
-async function runSwitchedReport(report, program) {
-  try {
-    // New type of reports
-    var backup = iPhoneBackup.fromID(program.backup, base)
-          
-    var flag = false
-    var value
-    // Check for a compatible reporting tool.
-    for (var key in report.functions) {
-      if (version.versionCheck(backup.iOSVersion, key)) {
-        if(!report.usesPromises) {
-          if(program.verbose) console.log('using synchronous call.')
-            
-          value = report.functions[key](program, backup)
-        } else {
-          // Create a promise to resolve this function
-          async function createPromise() {
-            if(program.verbose) console.log('resolving using promises.')
-            
-            return new Promise((resolve, reject) => {
-              report.functions[key](program, backup, resolve, reject)
-            })
-          }
+async function runSwitchedReport (report, program) {
+  async function createPromise (key, program, backup) {
+    log.verbose('resolving using promises.')
 
-          // Use promises to resolve synchronously
-          value = await createPromise()
-        }
-        flag = true
-        break
+    return new Promise((resolve, reject) => {
+      report.functions[key](program, backup, resolve, reject)
+    })
+  }
+
+  // New type of reports
+  var backup = iPhoneBackup.fromID(program.backup, base)
+
+  var flag = false
+  var value
+  // Check for a compatible reporting tool.
+  for (var key in report.functions) {
+    if (version.versionCheck(backup.iOSVersion, key)) {
+      if (!report.usesPromises) {
+        log.verbose('using synchronous call.')
+
+        value = report.functions[key](program, backup)
+      } else {
+        // Use promises to resolve synchronously
+        value = await createPromise(key, program, backup)
       }
+      flag = true
+      break
     }
-    
-    if (!flag) {
-      console.log('[!] The report generator "', program.report,'" does not support iOS', backup.iOSVersion)
-      console.log('')
-      console.log('    If you think it should, file an issue here:')
-      console.log('    https://github.com/richinfante/iphonebackuptools/issues')
-      console.log('')
-      process.exit(1)
-    }
+  }
 
-    return value
-  } catch (e) {
-    console.log('[!] Encountered an error', e)
+  if (!flag) {
+    log.error(`Couldn't run '${report.name}'.`)
+    log.error(`The report generator '${report.name}' does not support iOS`, backup.iOSVersion)
+    log.error(`If you think it should, file an issue here:`)
+    log.error(`https://github.com/richinfante/iphonebackuptools/issues`)
+    return null
   }
+
+  return value
 }
 
-async function runSingleReport(report, program) {
-  async function runReport(backup, base) {
-    if(!report.usesPromises) {
-      if(program.verbose) console.log('using synchronous call.')
+async function runSingleReport (report, program) {
+  async function createPromise (program, backup, base) {
+    log.verbose('resolving using promises.')
+
+    return new Promise((resolve, reject) => {
+      if (report.requiresBackup) {
+        report.func(program, backup, resolve, reject)
+      } else {
+        report.func(program, base, resolve, reject)
+      }
+    })
+  }
+
+  async function runReport (backup, base) {
+    if (!report.usesPromises) {
+      log.verbose('using synchronous call.')
 
       // Old-style non-promise based report.
       if (report.requiresBackup) {
@@ -239,43 +253,25 @@ async function runSingleReport(report, program) {
       }
     } else {
       // Create a promise to resolve this function
-      async  function createPromise() {
-        if(program.verbose) console.log('resolving using promises.')
-        
-        return new Promise((resolve, reject) => {
-          if (report.requiresBackup) {
-            report.func(program, backup, resolve, reject)
-          } else {
-            report.func(program, base, resolve, reject)
-          }
-        })
-      }
-
       // Use promises to resolve synchronously
-      return await createPromise()
+      return createPromise(program, backup, base)
     }
   }
 
+  // New type of reports
+  var backup = iPhoneBackup.fromID(program.backup, base)
 
-  try {
-    // New type of reports
-    var backup = iPhoneBackup.fromID(program.backup, base)
-    
-    if (report.supportedVersions !== undefined) {
-      if (version.versionCheck(backup.iOSVersion, report.supportedVersions)) {
-        return await runReport(backup, base)
-      } else {
-        console.log('[!] The report generator "' + program.report + '" does not support iOS', backup.iOSVersion)
-        console.log('')
-        console.log('    If you think it should, file an issue here:')
-        console.log('    https://github.com/richinfante/iphonebackuptools/issues')
-        console.log('')
-        process.exit(1)
-      }
+  if (report.supportedVersions !== undefined) {
+    if (version.versionCheck(backup.iOSVersion, report.supportedVersions)) {
+      return runReport(backup, base)
     } else {
-      return await runReport(backup, base)
+      log.error(`Couldn't run '${report.name}'.`)
+      log.error(`The report generator '${report.name}' does not support iOS`, backup.iOSVersion)
+      log.error(`If you think it should, file an issue here:`)
+      log.error(`https://github.com/richinfante/iphonebackuptools/issues`)
+      return null
     }
-  } catch (e) {
-    console.log('[!] Encountered an error', e)
+  } else {
+    return runReport(backup, base)
   }
-}
+}

+ 42 - 19
tools/reports/_example.js

@@ -1,45 +1,68 @@
-// Name of the module.
+// First, give it a name!
 module.exports.name = 'example module'
 
-// Description of the module
+// Provide a description.
 module.exports.description = 'An example module to show how it works'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
+// Most reporting types should use this.
 module.exports.requiresBackup = true
 
 // Should this report be skipped in automated reports?
 // This is used when the 'all' report type is specified, and all possible reports are generated.
 // with this set to true, the report WILL NOT run when report type = 'all'
+// Most reporting types shouldn't need this.
 module.exports.requiresInteractivity = true
 
 // Specify this reporter supports the promises API for allowing chaining of reports.
+// All modules should use this.
 module.exports.usesPromises = true
 
 // Specify this only works for iOS 10+
+// If it is iOS-version specific, you can specify version information here.
+// You may provide a comma separated string such as ">=6.0,<11.0" to indicate ranges.
 module.exports.supportedVersions = '>=10.0'
 
-// Reporting function (for usesPromises = false)
-module.exports.func = function (program, backup) {
-  // This function will be called with the `commander` program, and the iPhoneBackup instance as arguments
-  // This is deprecated.
-}
-
+// Most reports will use this pattern.
 // Reporting function (for usesPromises = true)
 module.exports.func = function (program, backup, resolve, reject) {
-    // This function will be called with the `commander` program, and the iPhoneBackup instance as arguments
-    // It MUST resolve() the final result, or reject() if there's an error
+  // This function will be called with the `commander` program, and the iPhoneBackup instance as arguments
+  // It MUST resolve() the final result, or reject() if there's an error
+
+  // First, fetch some data. This variable should be an array of objects representing each row in a report.
+  // This would be replaced with a function from the backup object.
+  let data = backup.getData()
+
+  // Next, pass it to the user-selected formatter.
+  var result = program.formatter.format(data, {
+    // Provide the program options
+    program: program,
+
+    // A dictionary of items to be displayed as formatted data.
+    // The key is the column name, the value is a function that returns the value given an object representing a row.
+    columns: {
+      'Column-Name': row => row.ROWID
+    }
+  })
+
+  // Resolve the promise with the result.
+  // This ensures proper file output and multi-reporting.
+  resolve(result)
 }
 
 // --- OR ---
 
 // You can also provide an array of functions instead of using `module.exports.func`.
-// These functions *should* be independent ranges to ensure reliable execution
+// These functions *should* be independent ranges to ensure reliable execution.
 module.exports.functions = {
-    '>=10.0': function(program,backup) {
-        // This function would be called for iOS 10+
-    }, 
-    '>=9.0,<10.0': function(program,backup) {
-        // This function would be called for all iOS 9.
-    }
-}
+  '>=10.0': function (program, backup, resolve, reject) {
+    // This function would be called for iOS 10+.
+    // format and resolve() in the same manner as the example above.
+
+  },
+  '>=9.0,<10.0': function (program, backup) {
+    // This function would be called for all iOS 9.
+    // format and resolve() in the same manner as the example above.
+  }
+}

+ 21 - 22
tools/reports/address_book.js

@@ -1,7 +1,7 @@
 module.exports.name = 'address_book'
 module.exports.description = 'List all address book records contained in the backup.'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -10,29 +10,28 @@ module.exports.usesPromises = true
 
 module.exports.func = function (program, backup, resolve, reject) {
   backup.getAddressBook()
-  .then((items) => {
-
+    .then((items) => {
     // Use the configured formatter to print the rows.
-    const result = program.formatter.format(items, {
+      const result = program.formatter.format(items, {
       // Color formatting?
-      program: program,
+        program: program,
 
-      // Columns to be displayed in human-readable printouts.
-      // Some formatters, like raw or CSV, ignore these.
-      columns: {
-        'ID': el => el.ROWID,
-        'First': el => el.First ? el.First.substring(0,10) + '' : '',
-        'Last': el => el.Last ? el.Last.substring(0,10) + '' : '',
-        'Organization': el => el.organization ? el.organization.substring(0,10) + '' : '',
-        'Phone Work': el => el.phone_work ? el.phone_work.substring(0,14) + '' : '',
-        'Phone Mobile': el => el.phone_mobile ? el.phone_mobile.substring(0,14) + '' : '',
-        'Phone Home': el => el.phone_home ? el.phone_home.substring(0,14) + '' : '',
-        'Email': el => el.email ? el.email.substring(0,28) + '' : ''
-      }
-    })
+        // Columns to be displayed in human-readable printouts.
+        // Some formatters, like raw or CSV, ignore these.
+        columns: {
+          'ID': el => el.ROWID,
+          'First': el => el.First ? el.First.substring(0, 10) + '' : '',
+          'Last': el => el.Last ? el.Last.substring(0, 10) + '' : '',
+          'Organization': el => el.organization ? el.organization.substring(0, 10) + '' : '',
+          'Phone Work': el => el.phone_work ? el.phone_work.substring(0, 14) + '' : '',
+          'Phone Mobile': el => el.phone_mobile ? el.phone_mobile.substring(0, 14) + '' : '',
+          'Phone Home': el => el.phone_home ? el.phone_home.substring(0, 14) + '' : '',
+          'Email': el => el.email ? el.email.substring(0, 28) + '' : ''
+        }
+      })
 
-    // If using promises, we must call resolve()
-    resolve(result)
-  })
-  .catch(reject)
+      // If using promises, we must call resolve()
+      resolve(result)
+    })
+    .catch(reject)
 }

+ 0 - 2
tools/reports/all.js

@@ -1,2 +0,0 @@
-module.exports.name = 'all'
-module.exports.description = 'Runs all reports at once'

+ 3 - 5
tools/reports/apps.js

@@ -1,9 +1,7 @@
-const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
-
 module.exports.name = 'apps'
 module.exports.description = 'List all installed applications and container IDs.'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -11,7 +9,7 @@ module.exports.requiresBackup = true
 module.exports.usesPromises = true
 
 module.exports.func = function (program, backup, resolve, reject) {
-
+  // Fail if manifest does not exist
   if (!backup.manifest) return reject(new Error('Manifest does not exist in this version'))
 
   // Enumerate the apps in the backup
@@ -19,7 +17,7 @@ module.exports.func = function (program, backup, resolve, reject) {
   for (var key in backup.manifest.Applications) {
     var app = backup.manifest.Applications[key]
 
-    apps.push({ bundleID: app.CFBundleIdentifier, path:  app.Path})
+    apps.push({ bundleID: app.CFBundleIdentifier, path: app.Path })
   }
 
   var result = program.formatter.format(apps, {

+ 24 - 26
tools/reports/calls.js

@@ -1,7 +1,7 @@
 module.exports.name = 'calls'
 module.exports.description = 'List all call records contained in the backup.'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -12,33 +12,31 @@ module.exports.usesPromises = true
 module.exports.supportedVersions = '>=10.0'
 
 module.exports.func = function (program, backup, resolve, reject) {
-
   backup.getCallsList()
-  .then((items) => {
-
+    .then((items) => {
     // Use the configured formatter to print the rows.
-    const result = program.formatter.format(items, {
+      const result = program.formatter.format(items, {
       // Color formatting?
-      program: program,
-
-      // Columns to be displayed in human-readable printouts.
-      // Some formatters, like raw or CSV, ignore these.
-      columns: {
-        'ID': el => el.Z_PK,
-        'Date': el => el.XFORMATTEDDATESTRING,
-        'Answered': el => el.ZANSWERED + '',
-        'Originated': el => el.ZORIGINATED + '',
-        'Call Type': el => el.ZCALLTYPE + '',
-        'Duration': el => el.ZDURATION + '',
-        'Location': el => el.ZLOCATION + '',
-        'Country': el => el.ZISO_COUNTRY_CODE + '',
-        'Service': el => el.ZSERVICE_PROVIDER + '',
-        'Address': el => (el.ZADDRESS || '').toString()
-      }
+        program: program,
+
+        // Columns to be displayed in human-readable printouts.
+        // Some formatters, like raw or CSV, ignore these.
+        columns: {
+          'ID': el => el.Z_PK,
+          'Date': el => el.XFORMATTEDDATESTRING,
+          'Answered': el => el.ZANSWERED + '',
+          'Originated': el => el.ZORIGINATED + '',
+          'Call Type': el => el.ZCALLTYPE + '',
+          'Duration': el => el.ZDURATION + '',
+          'Location': el => el.ZLOCATION + '',
+          'Country': el => el.ZISO_COUNTRY_CODE + '',
+          'Service': el => el.ZSERVICE_PROVIDER + '',
+          'Address': el => (el.ZADDRESS || '').toString()
+        }
+      })
+
+      // If using promises, we must call resolve()
+      resolve(result)
     })
-
-    // If using promises, we must call resolve()
-    resolve(result)
-  })
-  .catch(reject)
+    .catch(reject)
 }

+ 27 - 31
tools/reports/calls_statistics.js

@@ -1,7 +1,7 @@
 module.exports.name = 'calls_statistics'
 module.exports.description = 'Get statistics about all calls'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -11,40 +11,36 @@ module.exports.usesPromises = true
 // You can also provide an array of functions instead of using `module.exports.func`.
 // These functions *should* be independent ranges to ensure reliable execution
 module.exports.functions = {
-  '>=9.0': function(program,backup,resolve,reject) {
+  '>=9.0': function (program, backup, resolve, reject) {
     // This function would be called for iOS 10+
     backup.getCallsStatistics()
-    .then((items) => {
-      
-      var result = program.formatter.format(Object.entries(items[0]), {
-        program: program,
-        columns: {
-          'Key': el => el[0] + '',
-          'Value': el => el[1] + ''
-        }
+      .then((items) => {
+        var result = program.formatter.format(Object.entries(items[0]), {
+          program: program,
+          columns: {
+            'Key': el => el[0] + '',
+            'Value': el => el[1] + ''
+          }
+        })
+
+        resolve(result)
       })
-
-      resolve(result)
-
-    })
-    .catch(reject)
-  }, 
-  '>=1.0,<9.0': function(program,backup,resolve,reject) {
+      .catch(reject)
+  },
+  '>=1.0,<9.0': function (program, backup, resolve, reject) {
     // This function would be called for all iOS 9.
     backup.getCallsStatisticsiOS7()
-    .then((items) => {
-
-      var result = program.formatter.format(items, {
-        program: program,
-        columns: {
-          'Key': el => el.key + '',
-          'Value': el => el.value + ''
-        }
+      .then((items) => {
+        var result = program.formatter.format(items, {
+          program: program,
+          columns: {
+            'Key': el => el.key + '',
+            'Value': el => el.value + ''
+          }
+        })
+
+        resolve(result)
       })
-
-      resolve(result)
-
-    })
-    .catch(reject)
+      .catch(reject)
   }
-}
+}

+ 2 - 8
tools/reports/conversations.js

@@ -1,12 +1,7 @@
-const stripAnsi = require('strip-ansi')
-const chalk = require('chalk')
-const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
-const normalizeCols = require('../util/normalize.js')
-
 module.exports.name = 'conversations'
 module.exports.description = 'List all SMS and iMessage conversations'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -16,7 +11,6 @@ module.exports.usesPromises = true
 module.exports.func = function (program, backup, resolve, reject) {
   backup.getConversations()
     .then((items) => {
-
       var result = program.formatter.format(items, {
         program: program,
         columns: {
@@ -24,7 +18,7 @@ module.exports.func = function (program, backup, resolve, reject) {
           'Date': el => el.XFORMATTEDDATESTRING || '??',
           'Service': el => el.service_name + '',
           'Chat Name': el => el.chat_identifier + '',
-          'Display Name': el => el.display_name + '',
+          'Display Name': el => el.display_name + ''
         }
       })
 

+ 23 - 47
tools/reports/conversations_full.js

@@ -1,7 +1,7 @@
 module.exports.name = 'conversations_full'
 module.exports.description = 'List all SMS and iMessage conversations and their messages (dump only)'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -9,52 +9,28 @@ module.exports.requiresBackup = true
 module.exports.usesPromises = true
 
 module.exports.func = async function (program, backup, resolve, reject) {
-  //if (program.dump) {
-    //return new Promise(async (resolve, reject) => {
-    let conversations = await backup.getConversations();
-    for (let el of conversations) {
-      el.messages = await backup.getMessages(el.ROWID, true);
+  // if (program.dump) {
+  // return new Promise(async (resolve, reject) => {
+  let conversations = await backup.getConversations()
+  for (let el of conversations) {
+    el.messages = await backup.getMessages(el.ROWID, true)
+  }
+
+  // Use the configured formatter to print the rows.
+  const result = program.formatter.format(conversations, {
+    // Color formatting?
+    program: program,
+
+    // Columns to be displayed in human-readable printouts.
+    // Some formatters, like raw or CSV, ignore these.
+    columns: {
+      'ID': el => el.ROWID,
+      'Date': el => el.XFORMATTEDDATESTRING || '??',
+      'Service': el => el.service_name + '',
+      'Chat Name': el => el.chat_identifier + '',
+      'Display Name': el => el.display_name + ''
     }
+  })
 
-      // Use the configured formatter to print the rows.
-    const result = program.formatter.format(conversations, {
-      // Color formatting?
-      program: program,
-
-      // Columns to be displayed in human-readable printouts.
-      // Some formatters, like raw or CSV, ignore these.
-      columns: {
-        'ID': el => el.ROWID,
-        'Date': el => el.XFORMATTEDDATESTRING || '??',
-        'Service': el => el.service_name + '',
-        'Chat Name': el => el.chat_identifier + '',
-        'Display Name': el => el.display_name + '',
-      }
-    })
-      
-    resolve(conversations);
-    //});
-  /*} 
-  else {
-    backup.getConversations(program.dump)
-      .then((items) => {
-        items = items.map(el => [
-          el.ROWID + '',
-          chalk.gray(el.XFORMATTEDDATESTRING || '??'),
-          el.service_name + '', 
-          el.chat_identifier + '',
-          el.display_name + ''
-        ])
-
-        items = [['ID', 'DATE', 'Service', 'Chat Name', 'Display Name'], ['-', '-', '-', '-', '-'], ...items]
-        items = normalizeCols(items).map(el => el.join(' | ')).join('\n')
-
-        if (!program.color) { items = stripAnsi(items) }
-
-        console.log(items)
-      })
-      .catch((e) => {
-        console.log('[!] Encountered an Error:', e)
-      })
-  }*/
+  resolve(result)
 }

+ 22 - 15
tools/reports/cookies.js

@@ -1,31 +1,38 @@
 module.exports.name = 'cookies'
 module.exports.description = 'List all iOS cookies'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
 // Specify this reporter supports the promises API for allowing chaining of reports.
 module.exports.usesPromises = true
 
-module.exports.func = function (program, backup, resolve, reject) {
+// Specify this only works for iOS 9+
+module.exports.supportedVersions = '>=10.0'
 
+module.exports.func = function (program, backup, resolve, reject) {
   backup.getCookies()
-  .then((items) => {
+    .then((items) => {
     // Use the configured formatter to print the rows.
-    const result = program.formatter.format(items, {
+      const result = program.formatter.format(items, {
       // Color formatting?
-      program: program,
+        program: program,
 
-      // Columns to be displayed in human-readable printouts.
-      // Some formatters, like raw or CSV, ignore these.
-      columns: {
-        'domain': el => el.domain,
-        'url': el => el.cookie.url
-      }
-    })
+        // Columns to be displayed in human-readable printouts.
+        // Some formatters, like raw or CSV, ignore these.
+        columns: {
+          'domain': el => el.domain,
+          'url': el => el.cookie.url,
+          'path': el => el.cookie.name,
+          'value': el => el.cookie.value,
+          'creation': el => el.cookie.creation,
+          'expiration': el => el.cookie.expiration,
+          'flags': el => el.cookie.flags
+        }
+      })
 
-    resolve(result)
-  })
-  .catch(reject)
+      resolve(result)
+    })
+    .catch(reject)
 }

+ 2 - 5
tools/reports/list.js

@@ -1,14 +1,11 @@
-const stripAnsi = require('strip-ansi')
 const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
-const normalizeCols = require('../util/normalize.js')
-const chalk = require('chalk')
 const fs = require('fs-extra')
+const chalk = require('chalk')
 
 module.exports.name = 'list'
 module.exports.description = 'List of all backups. alias for -l'
 
-
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = false
 

+ 89 - 88
tools/reports/manifest.js

@@ -1,13 +1,10 @@
-const stripAnsi = require('strip-ansi')
-const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
-const normalizeCols = require('../util/normalize.js')
 const fs = require('fs-extra')
-const chalk = require('chalk')
+const log = require('../util/log')
 const path = require('path')
 module.exports.name = 'manifest'
 module.exports.description = 'List all the files contained in the backup (iOS 5+)'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -17,133 +14,137 @@ module.exports.usesPromises = true
 // Specify this only works for iOS 5+
 module.exports.supportedVersions = '>=5.0'
 
+function isIncludedByFilter (program, item) {
+  return program.filter === 'all' ||
+    program.filter === undefined ||
+    (program.filter && item.domain.indexOf(program.filter) > -1)
+}
+
 // You can also provide an array of functions instead of using `module.exports.func`.
 // These functions *should* be independent ranges to ensure reliable execution
 module.exports.functions = {
-  '>=10.0': function(program,backup,resolve,reject) {
+
+  '>=10.0': function (program, backup, resolve, reject) {
     // This function would be called for iOS 10+
-      
-    backup.getFileManifest()
-    .then((items) => {
 
+    backup.getFileManifest()
+      .then((items) => {
       // Extract items for analysis on-disk.
-      if (program.extract) {
-        for (var item of items) {
+        if (program.extract) {
+          for (var item of items) {
           // Filter by the domain.
           // Simple "Contains" Search
-          if (program.filter === 'all' || (program.filter && item.domain.indexOf(program.filter) > -1)) {
+            if (isIncludedByFilter(program, item)) {
             // Do nothing, we'll process later.
-          } else {
+            } else {
             // Skip to the next iteration of the loop.
-            console.log(chalk.yellow('skipped'), item.relativePath)
-            continue
-          }
+              log.action('skipped', item.relativePath)
+              continue
+            }
 
-          try {
-            var sourceFile = backup.getFileName(item.fileID)
-            var stat = fs.lstatSync(sourceFile)
+            try {
+              var sourceFile = backup.getFileName(item.fileID)
+              var stat = fs.lstatSync(sourceFile)
 
-            // Only process files that exist.
-            if (stat.isFile() && fs.existsSync(sourceFile)) {
-              console.log(chalk.green('export'), item.relativePath)
+              // Only process files that exist.
+              if (stat.isFile() && fs.existsSync(sourceFile)) {
+                log.action('export', item.relativePath)
 
                 // Calculate the output dir.
-              var outDir = path.join(program.extract, item.domain, item.relativePath)
+                var outDir = path.join(program.extract, item.domain, item.relativePath)
 
                 // Create the directory and copy
-              fs.ensureDirSync(path.dirname(outDir))
-              fs.copySync(sourceFile, outDir)
+                fs.ensureDirSync(path.dirname(outDir))
+                fs.copySync(sourceFile, outDir)
 
                 // Save output info to the data item.
-              item.output_dir = outDir
-            } else if (stat.isDirectory()) {
+                item.output_dir = outDir
+              } else if (stat.isDirectory()) {
               // Do nothing..
-            } else {
-              console.log(chalk.blue('not found'), item.relativePath)
+              } else {
+                log.error('not found', item.relativePath)
+              }
+            } catch (e) {
+              log.error(item.relativePath, e.toString())
             }
-          } catch (e) {
-            console.log(chalk.red('fail'), item.relativePath, e.toString())
           }
-        }
-
-        resolve(result)
-      } else {
 
-        var result = program.formatter.format(items, {
-          program: program,
-          columns: {
-            'ID': el => el.fileID,
-            'Domain/Path': el => el.domain + ': ' + el.relativePath
-          }
-        })
+          resolve([])
+        } else {
+          var result = program.formatter.format(items, {
+            program: program,
+            columns: {
+              'ID': el => el.fileID,
+              'Domain/Path': el => el.domain + ': ' + el.relativePath
+            }
+          })
 
-        resolve(result)
-      }
-    })
-    .catch((e) => {
+          resolve(result)
+        }
+      })
+      .catch((e) => {
         console.log('[!] Encountered an Error:', e)
-    })
-  }, 
+      })
+  },
 
-  '>=5.0,<10.0': function(program,backup,resolve,reject) {
+  '>=5.0,<10.0': function (program, backup, resolve, reject) {
     // This function would be called for all iOS 5 up to iOS 9.x.
     backup.getOldFileManifest()
-    .then((items) => {
-
+      .then((items) => {
       // Extract items for analysis on-disk.
-      if (program.extract) {
-        for (var item of items) {
+        if (program.extract) {
+          for (var item of items) {
           // Filter by the domain.
           // Simple "Contains" Search
-          if (program.filter === 'all' || (program.filter && item.domain.indexOf(program.filter) > -1)) {
+            if (isIncludedByFilter(program, item)) {
             // Do nothing, we'll process later.
-          } else {
+            } else {
             // Skip to the next iteration of the loop.
-            console.log(chalk.yellow('skipped'), item.filename)
-            continue
-          }
+              log.action('skipped', item.filename)
+              continue
+            }
 
-          try {
-            var sourceFile = backup.getFileName(item.fileID)
-            var stat = fs.lstatSync(sourceFile)
+            try {
+              var sourceFile = backup.getFileName(item.fileID)
+              var stat = fs.lstatSync(sourceFile)
 
-            // Only process files that exist.
-            if (stat.isFile() && fs.existsSync(sourceFile)) {
-              console.log(chalk.green('export'), item.filename)
+              // Only process files that exist.
+              if (stat.isFile() && fs.existsSync(sourceFile)) {
+                log.action(item.filename)
 
                 // Calculate the output dir.
-              var outDir = path.join(program.extract, item.domain, item.filename)
+                var outDir = path.join(program.extract, item.domain, item.filename)
 
                 // Create the directory and copy
-              fs.ensureDirSync(path.dirname(outDir))
-              fs.copySync(sourceFile, outDir)
+                fs.ensureDirSync(path.dirname(outDir))
+                fs.copySync(sourceFile, outDir)
 
                 // Save output info to the data item.
-              item.output_dir = outDir
-            } else if (stat.isDirectory()) {
+                item.output_dir = outDir
+              } else if (stat.isDirectory()) {
               // Do nothing..
-            } else {
-              console.log(chalk.blue('not found'), item.filename)
+              } else {
+                log.error('not found', item.filename)
+              }
+            } catch (e) {
+              log.error(item.filename, e.toString())
             }
-          } catch (e) {
-            console.log(chalk.red('fail'), item.filename, e.toString())
           }
-        }
 
-        resolve(result)
-      } else {
-        var result = program.formatter.format(items, {
-          program: program,
-          columns: {
-            'ID': el => el.fileID,
-            'Domain/Path': el => (el.domain + ': ' + el.filename).substr(0,70),
-            'Size': el => el.filelen
-          }
-        })
+          resolve([])
+        } else {
+          var result = program.formatter.format(items, {
+            program: program,
+            columns: {
+              'ID': el => el.fileID,
+              'Domain/Path': el => (el.domain + ': ' + el.filename).substr(0, 70),
+              'Size': el => el.filelen
+            }
+          })
 
-        resolve(result)
-      }
-    })
-    .catch(reject)
+          resolve(result)
+        }
+      })
+      .catch(reject)
   }
 }

+ 4 - 8
tools/reports/messages.js

@@ -1,12 +1,9 @@
-const stripAnsi = require('strip-ansi')
-const chalk = require('chalk')
-const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
-const normalizeCols = require('../util/normalize.js')
+const log = require('../util/log')
 
 module.exports.name = 'messages'
 module.exports.description = 'List all SMS and iMessage messages in a conversation'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -20,17 +17,16 @@ module.exports.requiresInteractivity = true
 
 module.exports.func = function (program, backup, resolve, reject) {
   if (!program.id) {
-    console.log('use -i or --id <id> to specify conversation ID.')
+    log.error('use -i or --id <id> to specify conversation ID.')
     process.exit(1)
   }
 
   backup.getMessages(program.id)
     .then((items) => {
-  
       var result = program.formatter.format(items, {
         program: program,
         columns: {
-          'ID' : el => el.ROWID,
+          'ID': el => el.ROWID,
           'Date': el => el.XFORMATTEDDATESTRING,
           'Sender': el => el.x_sender,
           'Text': el => (el.text || '').trim()

+ 3 - 7
tools/reports/notes.js

@@ -1,14 +1,10 @@
-const stripAnsi = require('strip-ansi')
-const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
-const normalizeCols = require('../util/normalize.js')
-
 module.exports.name = 'notes'
 module.exports.description = 'List all iOS notes'
 
-// Specify this only works for iOS 10+
+// Specify this only works for iOS 9+
 module.exports.supportedVersions = '>=9.0'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -32,4 +28,4 @@ module.exports.func = function (program, backup, resolve, reject) {
       resolve(result)
     })
     .catch(reject)
-}
+}

+ 1 - 7
tools/reports/oldnotes.js

@@ -1,11 +1,7 @@
-const stripAnsi = require('strip-ansi')
-const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
-const normalizeCols = require('../util/normalize.js')
-
 module.exports.name = 'oldnotes'
 module.exports.description = 'List all iOS notes (from older unused database)'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -13,10 +9,8 @@ module.exports.requiresBackup = true
 module.exports.usesPromises = true
 
 module.exports.func = function (program, backup, resolve, reject) {
-
   backup.getOldNotes(program.dump)
     .then((items) => {
-
       var result = program.formatter.format(items, {
         program: program,
         columns: {

+ 3 - 3
tools/reports/photolocations.js

@@ -1,7 +1,7 @@
 module.exports.name = 'photolocations'
 module.exports.description = 'List all geolocation information for iOS photos (iOS 10+)'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -22,11 +22,11 @@ module.exports.func = function (program, backup, resolve, reject) {
           'Time': el => el.XFORMATTEDDATESTRING,
           'Latitude': el => el.ZLATITUDE,
           'Longitude': el => el.ZLONGITUDE,
-          'File': el => el.ZFILENAME,
+          'File': el => el.ZFILENAME
         }
       })
 
       resolve(output)
     })
     .catch(reject)
-}
+}

+ 28 - 33
tools/reports/voicemail-files.js

@@ -1,13 +1,11 @@
-const stripAnsi = require('strip-ansi')
-const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
-const normalizeCols = require('../util/normalize.js')
+const log = require('../util/log')
 const path = require('path')
 const fs = require('fs-extra')
 
 module.exports.name = 'voicemail-files'
 module.exports.description = 'List all or extract voicemail files (iOS 10+)'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -15,36 +13,33 @@ module.exports.requiresBackup = true
 module.exports.usesPromises = true
 
 module.exports.func = function (program, backup, resolve, reject) {
-
-backup.getVoicemailFileList()
-  .then((list) => {
-
-    // Extract to the specified location
-    if (program.extract) {
-      for (var item of list) {
-        try {
-          var outDir = path.join(program.extract, path.basename(item.relativePath))
-          fs.ensureDirSync(path.dirname(outDir))
-          fs.createReadStream(backup.getFileName(item.fileID)).pipe(fs.createWriteStream(outDir))
-          item.output_dir = outDir
-        } catch (e) {
-          console.log(`Couldn't Export: ${item.relativePath}`, e)
+  backup.getVoicemailFileList()
+    .then((list) => {
+      // Extract to the specified location
+      if (program.extract) {
+        for (var item of list) {
+          try {
+            var outDir = path.join(program.extract, path.basename(item.relativePath))
+            fs.ensureDirSync(path.dirname(outDir))
+            fs.createReadStream(backup.getFileName(item.fileID)).pipe(fs.createWriteStream(outDir))
+            item.output_dir = outDir
+          } catch (e) {
+            log.error(`Couldn't Export: ${item.relativePath}`, e)
+          }
         }
       }
-    }
-    // Generate report.
-    var result = program.formatter.format(list, {
-      program: program,
-      columns: {
-        'ID': el => el.fileID,
-        'Path': el => el.relativePath,
-        'Export Path': el => el.output_dir || '<not exported>'
-      }
-    })
 
-    resolve(result)
-  })
-  .catch((e) => {
-    console.log('[!] Encountered an Error:', e)
-  })
+      // Generate report.
+      var result = program.formatter.format(list, {
+        program: program,
+        columns: {
+          'ID': el => el.fileID,
+          'Path': el => el.relativePath,
+          'Export Path': el => el.output_dir || '<not exported>'
+        }
+      })
+
+      resolve(result)
+    })
+    .catch(reject)
 }

+ 2 - 5
tools/reports/voicemail.js

@@ -1,21 +1,19 @@
 module.exports.name = 'voicemail'
 module.exports.description = 'List all or extract voicemails on device'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
 // Specify this reporter supports the promises API for allowing chaining of reports.
 module.exports.usesPromises = true
 
-// Specify this only works for iOS 10+
+// Specify this only works for iOS 9+
 module.exports.supportedVersions = '>=9.0'
 
 module.exports.func = function (program, backup, resolve, reject) {
-
   backup.getVoicemailsList()
     .then((items) => {
-
       var result = program.formatter.format(items, {
         program: program,
         columns: {
@@ -25,7 +23,6 @@ module.exports.func = function (program, backup, resolve, reject) {
           'Token': el => el.token,
           'Duration': el => el.duration,
           'Expiration': el => el.expiration,
-          'Duration': el => el.duration,
           'Trashed': el => el.trashed_date,
           'Flags': el => el.flags
         }

+ 2 - 7
tools/reports/webhistory.js

@@ -1,26 +1,21 @@
-const stripAnsi = require('strip-ansi')
 const { URL } = require('url')
-const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
-const normalizeCols = require('../util/normalize.js')
 
 module.exports.name = 'webhistory'
 module.exports.description = 'List all web history'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
 // Specify this reporter supports the promises API for allowing chaining of reports.
 module.exports.usesPromises = true
 
-// Specify this only works for iOS 6+
+// Specify this only works for iOS 9+
 module.exports.supportedVersions = '>=9.0'
 
 module.exports.func = function (program, backup, resolve, reject) {
-
   backup.getWebHistory(program.dump)
     .then((history) => {
-      
       var result = program.formatter.format(history, {
         program: program,
         columns: {

+ 1 - 7
tools/reports/wifi.js

@@ -1,11 +1,7 @@
-const stripAnsi = require('strip-ansi')
-const iPhoneBackup = require('../util/iphone_backup.js').iPhoneBackup
-const normalizeCols = require('../util/normalize.js')
-
 module.exports.name = 'wifi'
 module.exports.description = 'List associated wifi networks and their usage information'
 
-// Specify this reporter requires a backup. 
+// Specify this reporter requires a backup.
 // The second parameter to func() is now a backup instead of the path to one.
 module.exports.requiresBackup = true
 
@@ -13,10 +9,8 @@ module.exports.requiresBackup = true
 module.exports.usesPromises = true
 
 module.exports.func = function (program, backup, resolve, reject) {
-
   backup.getWifiList()
     .then((items) => {
-
       var result = program.formatter.format(items['List of known networks'], {
         program: program,
         columns: {

+ 9 - 0
tools/util/backup_filehash.js

@@ -0,0 +1,9 @@
+const crypto = require('crypto')
+
+/// Derive the name of the file inside of the backup from it's domain and file name.
+module.exports = function fileHash (file, domain) {
+  domain = domain || 'HomeDomain'
+  let shasum = crypto.createHash('sha1')
+  shasum.update(`${domain}-${file}`)
+  return shasum.digest('hex')
+}

+ 139 - 0
tools/util/cookies.js

@@ -0,0 +1,139 @@
+const fs = require('fs')
+const log = require('./log')
+
+// Format from here:
+// http://www.securitylearn.net/2012/10/27/cookies-binarycookies-reader/
+
+// January 1st, 2001, 00:00:00 UTC
+const APPLE_2001_EPOCH = 978307200
+
+function parseCookie (cookieBuff) {
+  // Read a null-terminated string from the buffer.
+  function readNullTerminatedString (fromIndex) {
+    let string = ''
+
+    for (var i = fromIndex; cookieBuff.readInt8(i) !== 0 && i < size; i++) {
+      string += String.fromCharCode(cookieBuff.readInt8(i))
+    }
+
+    return string
+  }
+
+  let size = cookieBuff.readInt32LE(0)
+  // 4 unknown bytes
+  let flagInt = cookieBuff.readInt32LE(8)
+  // 4 unknown bytes
+  let urlOffset = cookieBuff.readInt32LE(16)
+  let nameOffset = cookieBuff.readInt32LE(20)
+  let pathOffset = cookieBuff.readInt32LE(24)
+  let valueOffset = cookieBuff.readInt32LE(28)
+  // END OF COOKIE 8 bytes = 0x0
+  let expirationEpoch = cookieBuff.readDoubleLE(40) + APPLE_2001_EPOCH
+  let creationEpoch = cookieBuff.readDoubleLE(48) + APPLE_2001_EPOCH
+
+  // Dictionary of flag strings.
+  let flagDict = {
+    0: 'none',
+    1: 'secure',
+    4: 'httpOnly',
+    5: 'secure,httpOnly'
+  }
+
+  let flags = flagDict[flagInt]
+  let url = readNullTerminatedString(urlOffset)
+  let name = readNullTerminatedString(nameOffset)
+  let path = readNullTerminatedString(pathOffset)
+  let value = readNullTerminatedString(valueOffset)
+  let expiration = new Date(expirationEpoch * 1000)
+  let creation = new Date(creationEpoch * 1000)
+
+  return { url, name, value, path, flags, creation, expiration }
+}
+
+function parsePage (page) {
+  function checkPageHeader (page) {
+    return page.readInt32BE(0) === 0x00000100
+  }
+
+  // Header check fails page parse. return nothing.
+  if (!checkPageHeader(page)) {
+    return []
+  }
+
+  // Get the count of cookies on this page.
+  const cookieCount = page.readInt32LE(4)
+
+  // Store the cookies.
+  let cookies = []
+
+  for (let i = 0; i < cookieCount; i++) {
+    // Read offset and size.
+    const cookieOffset = page.readInt32LE(8 + i * 4)
+    const cookieSize = page.readInt32LE(cookieOffset)
+
+    // Slice buff
+    const cookieBuff = page.slice(cookieOffset, cookieOffset + cookieSize)
+
+    // Parse cookie
+    let cookie = parseCookie(cookieBuff)
+
+    // Add the cookie if parsing succeded.
+    if (cookie) {
+      cookies.push(cookie)
+    }
+  }
+
+  return cookies
+}
+
+function parseBase (buff) {
+  function checkHeader (buff) {
+    return buff.readInt8(0) === 'c'.charCodeAt(0) &&
+      buff.readInt8(1) === 'o'.charCodeAt(0) &&
+      buff.readInt8(2) === 'o'.charCodeAt(0) &&
+      buff.readInt8(3) === 'k'.charCodeAt(0)
+  }
+
+  // Header check fails. Return nothing.
+  if (!checkHeader(buff)) {
+    return []
+  }
+
+  let pageCount = buff.readInt32BE(4)
+  let dataStart = (pageCount * 4) + 8
+  let cursor = dataStart
+
+  let cookies = []
+
+  for (let i = 0; i < pageCount; i++) {
+    // Find the page size, and grab the slice from the buffer.
+    let pageSize = buff.readInt32BE(8 + i * 4)
+    let page = buff.slice(cursor, cursor + pageSize)
+
+    cookies = [...cookies, ...parsePage(page)]
+
+    // Advance the cursor to the next page's tart index.
+    cursor += pageSize
+  }
+
+  return cookies
+}
+
+// This parser works on best-effort, to allow for maximum data retrival.
+// If parsing fails, we return nothing, or as much as we can.
+// errors are only raised for out-of-bounds errors, etc.
+module.exports.parse = function (filePath) {
+  return new Promise((resolve, reject) => {
+
+    log.verbose('parse', filePath)
+
+    try {
+      let buff = fs.readFileSync(filePath)
+      let result = parseBase(buff)
+      // console.log(result)
+      resolve(result)
+    } catch (e) {
+      return reject(e)
+    }
+  })
+}

+ 163 - 116
tools/util/iphone_backup.js

@@ -1,34 +1,43 @@
+const log = require('./log')
 const path = require('path')
 const sqlite3 = require('sqlite3')
 const bplist = require('bplist-parser')
 const fs = require('fs')
 const plist = require('plist')
-const mac_address_parse = require('./mac_address_parse')
-const cookieParser = require('binary-cookies')()
-const tz_offset = 5
-const manifest_mbdb_parse = require('./manifest_mbdb_parse')
+
+// Cookie Parser
+const cookieParser = require('./cookies.js')
+
+// Normalize mac addresses in wifi output
+const macParse = require('./mac_address_parse')
+
+// Derive filenames based on domain + file path
+const fileHash = require('./backup_filehash')
+
+// Manifest.mbdb parser
+const manifestMBDBParse = require('./manifest_mbdb_parse')
 
 const databases = {
-  SMS: '3d0d7e5fb2ce288813306e4d4636395e047a3d28',
-  AddressBook: '31bb7ba8914766d4ba40d6dfb6113c8b614be442',
-  Contacts: '31bb7ba8914766d4ba40d6dfb6113c8b614be442',
-  Calendar: '2041457d5fe04d39d0ab481178355df6781e6858',
+  SMS: fileHash('Library/SMS/sms.db'),
+  Contacts: fileHash('Library/AddressBook/AddressBook.sqlitedb'),
+  Calendar: fileHash('Library/Calendar/Calendar.sqlitedb'),
+  Reminders: fileHash('Library/Calendar/Calendar.sqlitedb'),
+  Notes: fileHash('Library/Notes/notes.sqlite'),
+  Notes2: fileHash('NoteStore.sqlite', 'AppDomainGroup-group.com.apple.notes'),
+  AddressBook: fileHash('Library/AddressBook/AddressBook.sqlitedb'),
   'Cookies.binarycookies': '69b1865768101bacde5b77ccc44445cea9ce1261',
-  Reminders: '2041457d5fe04d39d0ab481178355df6781e6858',
-  Notes: 'ca3bc056d4da0bbf88b5fb3be254f3b7147e639c',
-  Notes2: '4f98687d8ab0d6d1a371110e6b7300f6e465bef2',
   Calls: '2b2b0084a1bc3a5ac8c27afdf14afb42c61a19ca',
-  Calls2: '5a4935c78a5255723f707230a451d79c540d2741',
-  Locations: '4096c9ec676f2847dc283405900e284a7c815836',
-  WebHistory: 'e74113c185fd8297e140cfcf9c99436c5cc06b57',
-  Photos: '12b144c0bd44f2b3dffd9186d3f9c05b917cee25',
-  WiFi: 'ade0340f576ee14793c607073bd7e8e409af07a8',
-  Voicemail: '992df473bbb9e132f4b3b6e4d33f72171e97bc7a'
+  Calls2: fileHash('Library/CallHistoryDB/CallHistory.storedata'),
+  Locations: fileHash('Library/Caches/locationd/consolidated.db', 'RootDomain'),
+  WebHistory: fileHash('Library/Safari/History.db', 'AppDomain-com.apple.mobilesafari'),
+  Photos: fileHash('Media/PhotoData/Photos.sqlite', 'CameraRollDomain'),
+  WiFi: fileHash('SystemConfiguration/com.apple.wifi.plist', 'SystemPreferencesDomain'),
+  Voicemail: fileHash('Library/Voicemail/voicemail.db')
 }
 
 var cache = {}
 
-class iPhoneBackup {
+class IPhoneBackup {
   constructor (id, status, info, manifest, base) {
     this.id = id
     this.status = status
@@ -49,25 +58,25 @@ class iPhoneBackup {
 
     // Parse manifest bplist files
     try {
-      if (global.verbose) console.log('parsing status', base)
+      log.verbose('parsing status', base)
       var status = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Status.plist')))[0]
     } catch (e) {
-      console.log('Cannot open Status.plist', e)
+      log.error('Cannot open Status.plist', e)
     }
     try {
-      if (global.verbose) console.log('parsing manifest', base)
+      log.verbose('parsing manifest', base)
       var manifest = bplist.parseBuffer(fs.readFileSync(path.join(base, 'Manifest.plist')))[0]
     } catch (e) {
-      console.log('Cannot open Manifest.plist', e)
+      log.error('Cannot open Manifest.plist', e)
     }
     try {
-      if (global.verbose) console.log('parsing status', base)
+      log.verbose('parsing status', base)
       var info = plist.parse(fs.readFileSync(path.join(base, 'Info.plist'), 'utf8'))
     } catch (e) {
-      console.log('Cannot open Info.plist', e)
+      log.error('Cannot open Info.plist', e)
     }
 
-    return new iPhoneBackup(id, status, info, manifest, base)
+    return new IPhoneBackup(id, status, info, manifest, base)
   }
 
   get iOSVersion () {
@@ -77,7 +86,7 @@ class iPhoneBackup {
   getFileName (fileID, isAbsoulte) {
     isAbsoulte = isAbsoulte || false
 
-    //const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', this.id)
+    // const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/', this.id)
     // Return v2 filename
     if (this.status.Version < 3 || isAbsoulte) {
       return path.join(this.base, fileID)
@@ -86,16 +95,55 @@ class iPhoneBackup {
       return path.join(this.base, fileID.substr(0, 2), fileID)
     }
   }
+
+  openDatabase (fileID, isAbsoulte) {
+    return new Promise((resolve, reject) => {
+      isAbsoulte = isAbsoulte || false
+
+      // Get the backup folder
+      // Return v2 filename
+      if (this.status.Version < 3 || isAbsoulte) {
+        let db = new sqlite3.Database(path.join(this.base, fileID), sqlite3.OPEN_READONLY, (err) => {
+          if (err) {
+            return reject(err)
+          }
+
+          resolve(db)
+        })
+      } else {
+        // v3 has folders
+        let db = new sqlite3.Database(path.join(this.base, fileID.substr(0, 2), fileID), sqlite3.OPEN_READONLY, (err) => {
+          if (err) {
+            return reject(err)
+          }
+
+          resolve(db)
+        })
+      }
+    })
+  }
+
+  /// This is deprecated. Use openDatabase Instead.
   getDatabase (fileID, isAbsoulte) {
     isAbsoulte = isAbsoulte || false
 
     // Get the backup folder
     // Return v2 filename
     if (this.status.Version < 3 || isAbsoulte) {
-      return new sqlite3.Database(path.join(this.base, fileID), sqlite3.OPEN_READONLY)
+      return new sqlite3.Database(path.join(this.base, fileID), sqlite3.OPEN_READONLY, (err) => {
+        if (err) {
+          log.error('PANIC::', err)
+          process.exit(1)
+        }
+      })
     } else {
       // v3 has folders
-      return new sqlite3.Database(path.join(this.base, fileID.substr(0, 2), fileID), sqlite3.OPEN_READONLY)
+      return new sqlite3.Database(path.join(this.base, fileID.substr(0, 2), fileID), sqlite3.OPEN_READONLY, (err) => {
+        if (err) {
+          log.error('PANIC::', err)
+          process.exit(1)
+        }
+      })
     }
   }
 
@@ -114,7 +162,7 @@ class iPhoneBackup {
     return new Promise((resolve, reject) => {
       if (messageDest.indexOf('@') === -1) {
         messageDest = messageDest.replace(/[\s+\-()]*/g, '')
-        if (messageDest.length == 11 && messageDest[0] == '1') {
+        if (messageDest.length === 11 && messageDest[0] === '1') {
           messageDest = messageDest.substring(1)
         }
       }
@@ -164,27 +212,27 @@ class iPhoneBackup {
         INNER JOIN handle
           ON handle.rowid = message.handle_id
         WHERE chat_message_join.chat_id = ?`, [parseInt(chatId)],
-     async function (err, chats) {
-       if (err) return reject(err)
+      async function (err, chats) {
+        if (err) return reject(err)
 
-       chats = chats || []
+        chats = chats || []
 
         // Compute the user's name
-       for (var i in chats) {
-         var el = chats[i]
-         el.x_sender = el.is_from_me ? 'Me' : el.sender_name
+        for (var i in chats) {
+          var el = chats[i]
+          el.x_sender = el.is_from_me ? 'Me' : el.sender_name
 
-         if (!el.is_from_me) {
-           var contact = await backup.getName(el.sender_name)
+          if (!el.is_from_me) {
+            var contact = await backup.getName(el.sender_name)
 
-           if (contact) {
-             el.x_sender = `${contact.name} <${contact.query}>`
-           }
-         }
-       }
+            if (contact) {
+              el.x_sender = `${contact.name} <${contact.query}>`
+            }
+          }
+        }
 
-       resolve(chats)
-     })
+        resolve(chats)
+      })
     })
   }
 
@@ -204,27 +252,27 @@ class iPhoneBackup {
         INNER JOIN handle
           ON handle.rowid = message.handle_id
         WHERE chat_message_join.chat_id = ?`, [parseInt(chatId)],
-     async function (err, chats) {
-       if (err) return reject(err)
+      async function (err, chats) {
+        if (err) return reject(err)
 
-       chats = chats || []
+        chats = chats || []
 
         // Compute the user's name
-       for (var i in chats) {
-         var el = chats[i]
-         el.x_sender = el.is_from_me ? 'Me' : el.sender_name
+        for (var i in chats) {
+          var el = chats[i]
+          el.x_sender = el.is_from_me ? 'Me' : el.sender_name
 
-         if (!el.is_from_me) {
-           var contact = await backup.getName(el.sender_name)
+          if (!el.is_from_me) {
+            var contact = await backup.getName(el.sender_name)
 
-           if (contact) {
-             el.x_sender = `${contact.name} <${contact.query}>`
-           }
-         }
-       }
+            if (contact) {
+              el.x_sender = `${contact.name} <${contact.query}>`
+            }
+          }
+        }
 
-       resolve(chats)
-     })
+        resolve(chats)
+      })
     })
   }
 
@@ -322,7 +370,7 @@ class iPhoneBackup {
   getOldFileManifest () {
     return new Promise((resolve, reject) => {
       let mbdbPath = this.getFileName('Manifest.mbdb', true)
-      manifest_mbdb_parse.process(mbdbPath, resolve, reject)
+      manifestMBDBParse.process(mbdbPath, resolve, reject)
     })
   }
 
@@ -428,7 +476,7 @@ class iPhoneBackup {
       // May work for earlier but I haven't tested it
       return this.getCallsListiOS7()
     } else {
-      return this.getCallsList()
+      return this.getCallsListLater()
     }
   }
 
@@ -451,9 +499,7 @@ class iPhoneBackup {
     })
   }
 
-
-
-  getCallsList () {
+  getCallsListLater () {
     return new Promise((resolve, reject) => {
       var messagedb = this.getDatabase(databases.Calls2)
       messagedb.all(`SELECT *, datetime(ZDATE + 978307200, 'unixepoch') AS XFORMATTEDDATESTRING from ZCALLRECORD ORDER BY ZDATE ASC`, async function (err, rows) {
@@ -476,12 +522,15 @@ class iPhoneBackup {
 
   getVoicemailFileList () {
     return new Promise((resolve, reject) => {
-      var messagedb = this.getDatabase('Manifest.db', true)
-      messagedb.all(`SELECT * from FILES where relativePath like 'Library/Voicemail/%.amr'`, async function (err, rows) {
-        if (err) reject(err)
+      this.openDatabase('Manifest.db', true)
+        .then(manifestdb => {
+          manifestdb.all(`SELECT * from FILES where relativePath like 'Library/Voicemail/%.amr'`, async (err, rows) => {
+            if (err) reject(err)
 
-        resolve(rows)
-      })
+            resolve(rows)
+          })
+        })
+        .catch(reject)
     })
   }
 
@@ -490,14 +539,15 @@ class iPhoneBackup {
       var filename = this.getFileName(databases.WiFi)
 
       try {
-        let wifiList = bplist.parseBuffer(fs.readFileSync(filename))[0];
-         wifiList['List of known networks'] = wifiList['List of known networks']
-           .map(el => {
-              if (el.BSSID)
-                el.BSSID = mac_address_parse.pad_zeros(el.BSSID) + ''
-              return el;
-           });
-         resolve(wifiList);
+        let wifiList = bplist.parseBuffer(fs.readFileSync(filename))[0]
+        wifiList['List of known networks'] = wifiList['List of known networks']
+          .map(el => {
+            if (el.BSSID) {
+              el.BSSID = macParse.pad_zeros(el.BSSID) + ''
+            }
+            return el
+          })
+        resolve(wifiList)
       } catch (e) {
         reject(e)
       }
@@ -505,50 +555,44 @@ class iPhoneBackup {
   }
 
   getCookies () {
-    return new Promise((resolve, reject) => {
-      const self = this
-      var manifestdb = this.getDatabase('Manifest.db', true)
-      manifestdb.all(`SELECT fileID,domain,relativePath from FILES where relativePath like 'Library/Cookies/Cookies.binarycookies'`, async function (err, rows) {
-        if (err) reject(err)
-        let cookiesResult = [];
-        const iterateElements = (elements, index, callback) => {
-          if (index == elements.length)
-            return callback();
-          // do parse call with element
-          var ele = elements[index];
-          cookieParser.parse(self.getFileName(ele.fileID), function(err, cookies) {
-            //console.log(ele.domain, ':', cookies)
-            
-            if (err) {
-              cookiesResult.push({
-                domain: ele.domain,
-                error: err
-              })
-            } else {
-              cookies.forEach(cookie => {
-                cookie.url = cookie.url.replace(/\0/g, '')
-                cookie.name = cookie.name.replace(/\0/g, '')
-                cookie.path = cookie.path.replace(/\0/g, '')
-                cookie.value = cookie.value.replace(/\0/g, '')
-                cookiesResult.push({
-                  domain: ele.domain,
-                  cookie: cookie
+    return new Promise(async (resolve, reject) => {
+      this.openDatabase('Manifest.db', true)
+        .then(manifestdb => {
+          manifestdb.all(`SELECT fileID,domain,relativePath from FILES where relativePath like 'Library/Cookies/Cookies.binarycookies'`, async (err, rows) => {
+            if (err) return reject(err)
+
+            let cookiesResult = []
+
+            const iterateElements = (elements, index, callback) => {
+              if (index === elements.length) { return callback() }
+              // do parse call with element
+              var ele = elements[index]
+
+              cookieParser.parse(this.getFileName(ele.fileID))
+                .then(cookies => {
+                  // Map to include domain
+                  let formatted = cookies.map(el => { return { domain: ele.domain, cookie: el } })
+
+                  // Append result
+                  cookiesResult = [...cookiesResult, ...formatted]
+
+                  // Next file.
+                  iterateElements(elements, index + 1, callback)
                 })
-              });
             }
-            iterateElements(elements, index+1, callback);
+
+            iterateElements(rows, 0, () => {
+              resolve(cookiesResult)
+            })
           })
-        }
-        iterateElements(rows, 0, () => {
-          resolve(cookiesResult)
         })
-      })
+        .catch(reject)
     })
   }
 
   getAddressBook () {
     return new Promise((resolve, reject) => {
-      var addressbookdb = this.getDatabase(databases.AddressBook);
+      var addressbookdb = this.getDatabase(databases.AddressBook)
       try {
         const query = `
         select ABPerson.ROWID
@@ -573,7 +617,7 @@ class iPhoneBackup {
         `
         addressbookdb.all(query, async function (err, rows) {
           if (err) reject(err)
-  
+
           resolve(rows)
         })
       } catch (e) {
@@ -586,9 +630,12 @@ class iPhoneBackup {
 module.exports.availableBackups = function () {
   const base = path.join(process.env.HOME, '/Library/Application Support/MobileSync/Backup/')
   return new Promise((resolve, reject) => {
-    resolve(fs.readdirSync(base, { encoding: 'utf8' })
-      .map(file => iPhoneBackup.fromID(file)))
+    resolve(fs.readdirSync(base, {
+      encoding: 'utf8'
+    })
+      .map(file => IPhoneBackup.fromID(file)))
   })
 }
 
-module.exports.iPhoneBackup = iPhoneBackup
+module.exports.iPhoneBackup = IPhoneBackup
+module.exports.IPhoneBackup = IPhoneBackup

+ 137 - 0
tools/util/log.js

@@ -0,0 +1,137 @@
+const chalk = require('chalk')
+
+const VERBOSE_KEY = Symbol('verbose_key')
+
+var lvl = 0
+var wasRaw = false
+
+module.exports.setVerbose = function (value) {
+  global[VERBOSE_KEY] = value
+}
+
+function isVerbose (i) {
+  return global[VERBOSE_KEY] >= i
+}
+
+function indent () {
+  var indent = ''
+
+  for (var i = 0; i < lvl; i++) {
+    indent += '  '
+  }
+
+  return indent
+}
+
+/**
+ * Print an error to the screen.
+ * These will only be output if log level >= 0
+ * Args is a description of the error.
+ * @param {*} args - string description
+ */
+module.exports.error = function (...args) {
+  if (!isVerbose(0)) { return }
+
+  if (wasRaw) {
+    console.log('')
+    wasRaw = false
+  }
+
+  console.log(indent() + chalk.red('ERROR!'), ...args)
+}
+
+/**
+ * Print to the screen that an action was taken
+ * These will only be output if log level >= 1
+ * @param {string} action - Action that was taken
+ * @param {*} args - string description
+ */
+module.exports.action = function (action, ...args) {
+  if (!isVerbose(1)) { return }
+  if (wasRaw) {
+    console.log('')
+    wasRaw = false
+  }
+
+  console.log(indent() + chalk.green(action), ...args)
+}
+
+/**
+ * Print to screen that a group of actions happened
+ * These will only be output if log level >= 1
+ * @param {string} action - action
+ * @param {*} args - string description
+ */
+module.exports.begin = function (action, ...args) {
+  if (!isVerbose(1)) { return }
+  if (wasRaw) {
+    console.log('')
+    wasRaw = false
+  }
+
+  console.log(indent() + chalk.green(action), ...args)
+  lvl += 1
+}
+
+/**
+ * Exit indent group
+ * These will only be output if log level >= 1
+ */
+module.exports.end = function () {
+  if (!isVerbose(1)) { return }
+
+  if (wasRaw) {
+    console.log('')
+    wasRaw = false
+  }
+
+  lvl -= 1
+}
+
+/**
+ * Print a warning
+ * * These will only be output if log level >= 0
+ * @param {*} args - String description of the warning
+ */
+module.exports.warning = function (...args) {
+  if (!isVerbose(0)) { return }
+
+  if (wasRaw) {
+    console.log('')
+    wasRaw = false
+  }
+
+  console.log(indent() + chalk.yellow('WARNING!'), ...args)
+}
+
+/**
+ * Verbose logging drop-in for console.log
+ * These will only be output if log level >= 2
+ * @param {*} args - print output
+ */
+module.exports.verbose = function (...args) {
+  if (!isVerbose(2)) { return }
+  if (wasRaw) {
+    console.log('')
+    wasRaw = false
+  }
+
+  console.log(indent() + chalk.blue('verbose'), ...args)
+}
+
+/**
+ * Raw logging drop-in for console.log
+ * These lines will NOT be formatted.
+ * These will only be output if log level >= 0
+ * @param {*} args - print output
+ */
+module.exports.raw = function (...args) {
+  if (!isVerbose(0)) { return }
+
+  if (!wasRaw) {
+    console.log('')
+    wasRaw = true
+  }
+
+  console.log(...args)
+}

+ 4 - 5
tools/util/mac_address_parse.js

@@ -1,9 +1,8 @@
 const zpad = require('zpad')
 
 module.exports = {
-  pad_zeros: (mac_address) => {
-    if (mac_address)
-      return mac_address.split(':').map((hex) => zpad(hex)).join(':')
-    return mac_address
+  pad_zeros: (macAddress) => {
+    if (macAddress) { return macAddress.split(':').map((hex) => zpad(hex)).join(':') }
+    return macAddress
   }
-}
+}

+ 5 - 5
tools/util/normalize.js

@@ -12,17 +12,17 @@ module.exports = function normalizeOutput (rows, max) {
   var widths = []
   max = max || rows[0].length
 
-  for (var i = 0; i < rows.length; i++) {
-    for (var j = 0; j < rows[i].length && j < max; j++) {
+  for (let i = 0; i < rows.length; i++) {
+    for (let j = 0; j < rows[i].length && j < max; j++) {
       if (!widths[j] || widths[j] < stripAnsi(rows[i][j]).length) {
         widths[j] = stripAnsi(rows[i][j]).length
       }
     }
   }
 
-  for (var i = 0; i < rows.length; i++) {
-    for (var j = 0; j < rows[i].length && j < max; j++) {
-      if (rows[i][j] == '-') {
+  for (let i = 0; i < rows.length; i++) {
+    for (let j = 0; j < rows[i].length && j < max; j++) {
+      if (rows[i][j] === '-') {
         rows[i][j] = padEnd(rows[i][j], widths[j], '-')
       } else {
         rows[i][j] = padEnd(rows[i][j], widths[j], ' ')

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels