diff --git a/README.md b/README.md index b2f984a..eeea5bd 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ If you use gradle (or Android Studio), you can build your app from the command-l This extension allows you to debug your App by creating a new Android configuration in `launch.json`. The following settings are used to configure the debugger: - +```jsonc { "version": "0.2.0", "configurations": [ @@ -50,9 +50,13 @@ The following settings are used to configure the debugger: // Launch behaviour if source files have been saved after the APK was built. One of: [ ignore warn stop ]. Default: warn "staleBuild": "warn", + + // Fully qualified path to the AndroidManifest.xml file compiled in the APK. Default: appSrcRoot/AndroidManifest.xml + "manifestFile": "${workspaceRoot}/app/src/main/AndroidManifest.xml" } ] } +``` ## Building your app automatically diff --git a/package-lock.json b/package-lock.json index d93ebb1..ac023b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -225,8 +225,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "bcrypt-pbkdf": { "version": "1.0.2", @@ -237,6 +236,20 @@ "tweetnacl": "^0.14.3" } }, + "big-integer": { + "version": "1.6.44", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.44.tgz", + "integrity": "sha512-7MzElZPTyJ2fNvBkPxtFQ2fWIkVmuzw41+BZHSzpEq3ymB2MfeKp1+yXl/tS75xCx+WnyV+yb0kp+K1C3UNwmQ==" + }, + "binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", + "requires": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -246,11 +259,15 @@ "inherits": "~2.0.0" } }, + "bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -280,6 +297,16 @@ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "dev": true }, + "buffer-indexof-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz", + "integrity": "sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8=" + }, + "buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" + }, "caller-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", @@ -301,6 +328,14 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", + "requires": { + "traverse": ">=0.3.0 <0.4" + } + }, "chalk": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", @@ -401,8 +436,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "convert-source-map": { "version": "1.6.0", @@ -416,8 +450,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cross-spawn": { "version": "6.0.5", @@ -501,6 +534,14 @@ "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "requires": { + "readable-stream": "^2.0.2" + } + }, "duplexify": { "version": "3.6.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.1.tgz", @@ -804,14 +845,12 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, "requires": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", @@ -844,7 +883,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -891,8 +929,7 @@ "graceful-fs": { "version": "4.1.15", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "dev": true + "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, "growl": { "version": "1.10.5", @@ -1169,7 +1206,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -1178,8 +1214,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "inquirer": { "version": "6.2.1", @@ -1331,8 +1366,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -1453,6 +1487,11 @@ "type-check": "~0.3.2" } }, + "listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=" + }, "lodash": { "version": "4.17.14", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", @@ -1495,7 +1534,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -1670,7 +1708,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -1722,8 +1759,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -1786,8 +1822,7 @@ "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" }, "progress": { "version": "2.0.2", @@ -1853,7 +1888,6 @@ "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -1976,7 +2010,6 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, "requires": { "glob": "^7.0.5" } @@ -2002,8 +2035,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safer-buffer": { "version": "2.1.2", @@ -2017,6 +2049,11 @@ "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", "dev": true }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -2147,7 +2184,6 @@ "version": "1.1.1", "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -2277,6 +2313,11 @@ } } }, + "traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=" + }, "tslib": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", @@ -2329,6 +2370,22 @@ "through2-filter": "^2.0.0" } }, + "unzipper": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.4.tgz", + "integrity": "sha512-7fmeZe+1J5D5fRMl4Zl4Chn+y+Os0bFgBrzc0Iuv3Xj+xFY0mrRUxzbrj8ZUO/lkyErJxtZJ5a13pLXc+KxvRg==", + "requires": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -2351,8 +2408,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { "version": "3.3.2", @@ -2636,8 +2692,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "0.2.1", diff --git a/package.json b/package.json index 2e1b03d..b3a287d 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,11 @@ "description": "Port number to use for the internal logcat websocket link. Changes to this value only apply when the extension is restarted. Default: 7038", "default": 7038 }, + "manifestFile": { + "type": "string", + "description": "Overrides the default location of AndroidManifest.xml", + "default": "${workspaceRoot}/app/src/main/AndroidManifest.xml" + }, "staleBuild": { "type": "string", "description": "Launch behaviour if source files have been saved after the APK was built. One of: [\"ignore\" \"warn\" \"stop\"]. Default: \"warn\"", @@ -136,6 +141,7 @@ }, "dependencies": { "long": "^4.0.0", + "unzipper": "^0.10.4", "uuid": "^3.3.2", "vscode-debugadapter": "^1.32.0", "vscode-debugprotocol": "^1.32.0", diff --git a/src/apkdecoder.js b/src/apkdecoder.js new file mode 100644 index 0000000..de2bd4c --- /dev/null +++ b/src/apkdecoder.js @@ -0,0 +1,281 @@ +const START_NAMESPACE_SPEC = { + hdr: '0x0100', + hdrsz: '0x0010', + sz: '0x00000018', + startline: 4, + commentId: 4, + nameId: 4, + valueId: 4, +} +const END_NAMESPACE_SPEC = { + hdr: '0x0101', + hdrsz: '0x0010', + sz: '0x00000018', + startline: 4, + commentId: 4, + nameId: 4, + valueId: 4, +} +const BEGIN_NODE_SPEC = { + hdr: '0x0102', + hdrsz: '0x0010', + sz: 4, + startline: 4, + commentId: 4, + namespaceId: 4, + nameId: 4, + attr: { + offset: 2, + size: 2, + count: 2, + }, + id_attr_offset: 2, + cls_attr_offset: 2, + style_attr_offset: 2, + attributes: [{ + length: main => main.attr.count, + element_spec: { + ns: 4, + nameId: 4, + commentId: 4, + sz: '0x0008', + zero: '0x00', + type: 1, + value: 4, + } + }] +} + +const END_NODE_SPEC = { + hdr: '0x0103', + hdrsz: '0x0010', + sz: 4, + startline: 4, + commentId: 4, + namespaceId: 4, + nameId: 4, +} + +function decode_spec_value(o, key, value, buf, idx, main) { + let byteLength; + switch (true) { + case typeof value === 'number': { + // raw integer value + byteLength = value; + o[key] = buf.readIntLE(idx, byteLength); + break; + } + case Array.isArray(value): { + // known-length array of values + const length = value[0].length(main); + byteLength = 0; + o[key] = new Array(length); + for (let i = 0; i < length; i++) { + const bytes = decode_spec_value(o[key], i, value[0].element_spec, buf, idx, main); + idx += bytes; + byteLength += bytes; + } + break; + } + case typeof value === 'object': { + // named sub-spec + o[key] = {}; + byteLength = decode_spec(buf, value, o[key], o, idx); + break + } + case /^0x[\da-fA-F]/.test(value): { + // exact integer value + byteLength = (value.length - 2) / 2; + o[key] = buf.readUIntLE(idx, byteLength); + if (parseInt(value) !== o[key]) { + throw new Error(`Bad value. Expected ${value}, got 0x${o[key].toString(16)}`); + } + break; + } + case value === 'length-utf16-null': { + // 2-byte length, utf16 chars, null char + const string_byte_length = buf.readUInt16LE(idx) * 2; // 1 char = 2 bytes + idx += 2; + o[key] = buf.slice(idx, idx + string_byte_length).toString('ucs2'); + idx += string_byte_length; + if (buf.readUInt16LE(idx) !== 0) { + throw new Error(`Bad value. Nul char expected but not found.`); + } + byteLength = 2 + string_byte_length + 2; + break; + } + case /^align:\d+$/.test(value): { + // used for arbitrary padding to a specified alignment + const align = parseInt(value.split(':')[1], 10); + byteLength = align - (idx % align); + o[key] = buf.slice(idx, idx + byteLength); + break; + } + default: throw new Error(`Unknown spec value definition: ${value}`); + } + return byteLength; +} + +function decode_spec(buf, spec, o = {}, main = o, idx = 0) { + + let byteLength = 0; + for (let key of Object.keys(spec)) { + const value = spec[key]; + const bytes = decode_spec_value(o, key, value, buf, idx, main); + idx += bytes; + byteLength += bytes; + } + + return byteLength; +} + +/** + * Converts a binary XML file back into a readable XML document + * @param {Buffer} buf binary XMl content + */ +function decode_binary_xml(buf) { + const xml_spec = { + header: '0x00080003', + headerSize: 4, + stringPool: { + header: '0x0001', + hdrsize: '0x001c', + sz: 4, + stringCount: 4, + styleCount: 4, + flags: 4, + stringStart: 4, + styleStart: 4, + stringOffsets: [{ + length: main => main.stringPool.stringCount, + element_spec: 4, + }], + strings: [{ + length: main => main.stringPool.stringCount, + element_spec: 'length-utf16-null', + }], + padding: 'align:4', + }, + resourceIDPool: { + hdr: '0x0180', + hdrsize: '0x0008', + sz: 4, + resIDs: [{ + length: main => (main.resourceIDPool.sz - main.resourceIDPool.hdrsize) / 4, + element_spec: 4, + }] + } + } + + const decoded = {}; + let idx = decode_spec(buf, xml_spec, decoded); + + // after we've extracted the string and id's, it should be time to parse the xml + const node_stack = [{ nodes: [] }]; + const namespaces = []; + while (idx < buf.byteLength) { + const id = buf.readUInt16LE(idx); + switch (id) { + case 0x0100: { + // start namespace + const node = {}; + idx += decode_spec(buf, START_NAMESPACE_SPEC, node, node, idx); + namespaces.push(node); + break; + } + case 0x0101: { + // end namespace + const node = {}; + idx += decode_spec(buf, END_NAMESPACE_SPEC, node, node, idx); + const i = namespaces.findIndex(ns => ns.nameId === node.nameId); + namespaces.splice(i, 1); + break; + } + case 0x0102: { + // begin node + const node = {}; + idx += decode_spec(buf, BEGIN_NODE_SPEC, node, node, idx); + node.namespaces = namespaces.slice(); + node.namespaces.forEach(ns => { + if (!ns.node) ns.node = node; + }); + node.nodes = []; + node_stack[0].nodes.push(node); + node_stack.unshift(node); + break; + } + case 0x0103: { + // end node + const spec = END_NODE_SPEC; + const node = {}; + idx += decode_spec(buf, spec, node, node, idx); + node_stack.shift(); + break; + } + default: throw new Error(`Unknown XML element ${id.toString(16)}`); + } + } + decoded.nodes = node_stack[0].nodes; + + const xml = toXMLDocument(decoded); + return xml; +} + +/** + * Convert the decoded binary XML to a readable XML document + * @param {*} decoded + */ +function toXMLDocument(decoded) { + const strings = decoded.stringPool.strings; + const format = { + nodes: (nodes, indent) => { + return nodes.map(node => format.node(node, indent)).join('\n'); + }, + node: (node, indent) => { + const parts = [indent, '<', strings[node.nameId]]; + for (let ns of node.namespaces.filter(ns => ns.node === node)) { + parts.push(' ', `xmlns:${strings[ns.nameId]}="${strings[ns.valueId]}"`); + } + const attr_indent = node.attributes.length > 1 ? `\n${indent} ` : ' '; + for (let attr of node.attributes) { + parts.push(attr_indent, format.attribute(attr, node.namespaces)); + } + if (node.nodes.length) { + parts.push('>\n', format.nodes(node.nodes, indent + ' '), '\n', indent, ''); + } else { + parts.push(' />'); + } + + return parts.join(''); + }, + attribute: (attr, namespaces) => { + let value = attr.value; + switch (attr.type) { + case 3: + value = strings[value]; + break; + case 16: + value |= 0; // convert to signed integer + break; + case 18: + value = value ? true : false; + break; + case 1: // resource id + case 17: // flags + default: + value = '0x' + value.toString(`16`); + break; + } + let ns = ''; + if (attr.ns >= 0) { + ns = `${strings[namespaces.find(ns => ns.valueId === attr.ns).nameId]}:`; + } + return `${ns}${strings[attr.nameId]}="${value}"`; + } + } + return '\n' + format.nodes(decoded.nodes, ''); +} + +module.exports = { + decode_binary_xml, +} diff --git a/src/debugMain.js b/src/debugMain.js index efa0593..c7eca47 100644 --- a/src/debugMain.js +++ b/src/debugMain.js @@ -10,10 +10,12 @@ const dom = require('xmldom').DOMParser; const fs = require('fs'); const os = require('os'); const path = require('path'); +const unzipper = require('unzipper'); const xpath = require('xpath'); // our stuff const { ADBClient } = require('./adbclient'); +const { decode_binary_xml } = require('./apkdecoder'); const { Debugger } = require('./debugger'); const $ = require('./jq-promise'); const { AndroidThread } = require('./threads'); @@ -45,6 +47,8 @@ class AndroidDebugSession extends DebugSession { this.src_packages = {}; // the device we are debugging this._device = null; + // the full file path name of the AndroidManifest.xml, taken from the manifestFile launch property + this.manifest_fpn = ''; // the threads (we know about from the last refreshThreads call) // this is implemented as both a hashmap and an array of AndroidThread objects @@ -240,6 +244,7 @@ class AndroidDebugSession extends DebugSession { // app_src_root must end in a path-separator for correct validation of sub-paths this.app_src_root = ensure_path_end_slash(args.appSrcRoot); this.apk_fpn = args.apkFile; + this.manifest_fpn = args.manifestFile; if (typeof args.callStackDisplaySize === 'number' && args.callStackDisplaySize >= 0) this.callStackDisplaySize = args.callStackDisplaySize|0; @@ -432,7 +437,7 @@ class AndroidDebugSession extends DebugSession { h.update(apk_file_data); done.result.content_hash = h.digest('hex'); // read the manifest - fs.readFile(path.join(this.app_src_root,'AndroidManifest.xml'), 'utf8', (err,manifest) => { + this.readAndroidManifest((err, manifest) => { if (err) return done.rejectWith(this, [new Error('Manifest read error. ' + err.message)]); done.result.manifest = manifest; try { @@ -460,6 +465,64 @@ class AndroidDebugSession extends DebugSession { }); return done; } + + readAndroidManifest(cb) { + // Because of manifest merging and build-injected properties, the manifest compiled inside + // the APK is frequently different from the AndroidManifest.xml source file. + // We try to extract the manifest from 3 sources (in priority order): + // 1. The 'manifestFile' launch configuration property + // 2. The decoded manifest from the APK + // 3. The AndroidManifest.xml file from the root of the source tree. + + const readAPKManifest = (cb) => { + D(`Reading APK Manifest`); + const apk_manifest_chunks = []; + function cb_once(err, manifest) { + cb && cb(err, manifest); + cb = null; + } + fs.createReadStream(this.apk_fpn) + .pipe(unzipper.ParseOne(/^AndroidManifest\.xml$/)) + .on('data', chunk => { + apk_manifest_chunks.push(chunk); + }) + .once('error', err => { + cb_once(err); + }) + .once('end', () => { + try { + const manifest = decode_binary_xml(Buffer.concat(apk_manifest_chunks)); + D(`APK manifest read complete`); + cb_once(null, manifest); + } catch (err) { + D(`APK manifest decode failed: ${err.message}`); + cb_once(err); + } + }); + } + + const readSourceManifest = (cb) => { + D(`Reading source manifest from ${this.app_src_root}`); + fs.readFile(path.join(this.app_src_root, 'AndroidManifest.xml'), 'utf8', cb); + } + + // a value from the manifestFile overrides the default manifest extraction + // note: there's no validation that the file is a valid AndroidManifest.xml file + if (this.manifest_fpn) { + D(`Reading manifest from ${this.manifest_fpn}`); + fs.readFile(this.manifest_fpn, 'utf8', cb); + return; + } + + readAPKManifest((err, manifest) => { + if (err) { + // if we fail to read the APK manifest, revert to the source manifest + readSourceManifest(cb) + return; + } + cb(err, manifest); + }); + } scanSourceSync(app_root) { try {