mirror of
				https://gitcode.com/github-mirrors/react-native-update-cli.git
				synced 2025-10-31 23:03:11 +08:00 
			
		
		
		
	v1.20.6
This commit is contained in:
		| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "react-native-update-cli", | ||||
|   "version": "1.20.5", | ||||
|   "version": "1.20.6", | ||||
|   "description": "Command tools for javaScript updater with `pushy` service for react native apps.", | ||||
|   "main": "index.js", | ||||
|   "bin": { | ||||
| @@ -31,7 +31,6 @@ | ||||
|   }, | ||||
|   "homepage": "https://github.com/reactnativecn/react-native-pushy/tree/master/react-native-pushy-cli", | ||||
|   "dependencies": { | ||||
|     "app-info-parser": "github:sunnylqm/app-info-parser#fix/support-ascii-versionname", | ||||
|     "cli-arguments": "^0.2.1", | ||||
|     "filesize-parser": "^1.5.0", | ||||
|     "fs-extra": "8", | ||||
|   | ||||
							
								
								
									
										117
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										117
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @@ -5,9 +5,6 @@ settings: | ||||
|   excludeLinksFromLockfile: false | ||||
|  | ||||
| dependencies: | ||||
|   app-info-parser: | ||||
|     specifier: github:sunnylqm/app-info-parser#fix/support-ascii-versionname | ||||
|     version: github.com/sunnylqm/app-info-parser/f7748ce60278fb16f6f280d908d35025a241eb57 | ||||
|   cli-arguments: | ||||
|     specifier: ^0.2.1 | ||||
|     version: 0.2.1 | ||||
| @@ -437,10 +434,6 @@ packages: | ||||
|     resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} | ||||
|     dev: true | ||||
|  | ||||
|   /base64-js@1.5.1: | ||||
|     resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} | ||||
|     dev: false | ||||
|  | ||||
|   /base@0.11.2: | ||||
|     resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} | ||||
|     engines: {node: '>=0.10.0'} | ||||
| @@ -462,11 +455,6 @@ packages: | ||||
|       tweetnacl: 0.14.5 | ||||
|     dev: false | ||||
|  | ||||
|   /big-integer@1.6.50: | ||||
|     resolution: {integrity: sha512-+O2uoQWFRo8ysZNo/rjtri2jIwjr3XfeAgRjAUADRqGG+ZITvyn8J1kvXLTaKVr3hhGXk+f23tKfdzmklVM9vQ==} | ||||
|     engines: {node: '>=0.6'} | ||||
|     dev: false | ||||
|  | ||||
|   /binary-extensions@1.13.1: | ||||
|     resolution: {integrity: sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==} | ||||
|     engines: {node: '>=0.10.0'} | ||||
| @@ -496,13 +484,6 @@ packages: | ||||
|       wrap-ansi: 7.0.0 | ||||
|     dev: false | ||||
|  | ||||
|   /bplist-parser@0.2.0: | ||||
|     resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} | ||||
|     engines: {node: '>= 5.10.0'} | ||||
|     dependencies: | ||||
|       big-integer: 1.6.50 | ||||
|     dev: false | ||||
|  | ||||
|   /brace-expansion@1.1.11: | ||||
|     resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} | ||||
|     dependencies: | ||||
| @@ -551,24 +532,6 @@ packages: | ||||
|     resolution: {integrity: sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=} | ||||
|     dev: false | ||||
|  | ||||
|   /buffer@5.7.1: | ||||
|     resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} | ||||
|     dependencies: | ||||
|       base64-js: 1.5.1 | ||||
|       ieee754: 1.2.1 | ||||
|     dev: false | ||||
|  | ||||
|   /bufferpack@0.0.6: | ||||
|     resolution: {integrity: sha512-MTWvLHElqczrIVhge9qHtqgNigJFyh0+tCDId5yCbFAfuekHWIG+uAgvoHVflwrDPuY/e47JE1ki5qcM7w4uLg==} | ||||
|     dev: false | ||||
|  | ||||
|   /bytebuffer@5.0.1: | ||||
|     resolution: {integrity: sha512-IuzSdmADppkZ6DlpycMkm8l9zeEq16fWtLvunEwFiYciR/BHo4E8/xs5piFquG+Za8OWmMqHF8zuRviz2LHvRQ==} | ||||
|     engines: {node: '>=0.8'} | ||||
|     dependencies: | ||||
|       long: 3.2.0 | ||||
|     dev: false | ||||
|  | ||||
|   /cache-base@1.0.1: | ||||
|     resolution: {integrity: sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==} | ||||
|     engines: {node: '>=0.10.0'} | ||||
| @@ -620,15 +583,6 @@ packages: | ||||
|     resolution: {integrity: sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=} | ||||
|     dev: false | ||||
|  | ||||
|   /cgbi-to-png@1.0.7: | ||||
|     resolution: {integrity: sha512-YR80kxTPuq9oRpZUdQmNEQWrmTKLINk1cfLVfyrV7Rfr9KLtLJdcockPKbreIr4JYAq+DhHBR7w+WA/tF5VDaQ==} | ||||
|     dependencies: | ||||
|       bufferpack: 0.0.6 | ||||
|       crc: 3.8.0 | ||||
|       stream-to-buffer: 0.1.0 | ||||
|       streamifier: 0.1.1 | ||||
|     dev: false | ||||
|  | ||||
|   /chalk@1.1.3: | ||||
|     resolution: {integrity: sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=} | ||||
|     engines: {node: '>=0.10.0'} | ||||
| @@ -753,11 +707,6 @@ packages: | ||||
|     resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} | ||||
|     dev: true | ||||
|  | ||||
|   /commander@7.2.0: | ||||
|     resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} | ||||
|     engines: {node: '>= 10'} | ||||
|     dev: false | ||||
|  | ||||
|   /component-emitter@1.3.0: | ||||
|     resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==} | ||||
|     requiresBuild: true | ||||
| @@ -809,12 +758,6 @@ packages: | ||||
|     dev: true | ||||
|     optional: true | ||||
|  | ||||
|   /crc@3.8.0: | ||||
|     resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} | ||||
|     dependencies: | ||||
|       buffer: 5.7.1 | ||||
|     dev: false | ||||
|  | ||||
|   /crypto-random-string@2.0.0: | ||||
|     resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} | ||||
|     engines: {node: '>=8'} | ||||
| @@ -1485,10 +1428,6 @@ packages: | ||||
|       sshpk: 1.16.1 | ||||
|     dev: false | ||||
|  | ||||
|   /ieee754@1.2.1: | ||||
|     resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} | ||||
|     dev: false | ||||
|  | ||||
|   /import-lazy@2.1.0: | ||||
|     resolution: {integrity: sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==} | ||||
|     engines: {node: '>=4'} | ||||
| @@ -1850,13 +1789,6 @@ packages: | ||||
|     dev: true | ||||
|     optional: true | ||||
|  | ||||
|   /isomorphic-unzip@1.1.5: | ||||
|     resolution: {integrity: sha512-2McA51lWhmO3Kk438jxVcYeh6L+AOqVnl9XdX1yI7GlLA9RwEyTBgGem1rNuRIU2abAmOiv+IagThdUxASY4IA==} | ||||
|     dependencies: | ||||
|       buffer: 5.7.1 | ||||
|       yauzl: 2.10.0 | ||||
|     dev: false | ||||
|  | ||||
|   /isstream@0.1.2: | ||||
|     resolution: {integrity: sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=} | ||||
|     dev: false | ||||
| @@ -1980,11 +1912,6 @@ packages: | ||||
|     resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} | ||||
|     dev: true | ||||
|  | ||||
|   /long@3.2.0: | ||||
|     resolution: {integrity: sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg==} | ||||
|     engines: {node: '>=0.6'} | ||||
|     dev: false | ||||
|  | ||||
|   /loose-envify@1.4.0: | ||||
|     resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} | ||||
|     hasBin: true | ||||
| @@ -2347,14 +2274,6 @@ packages: | ||||
|     resolution: {integrity: sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=} | ||||
|     dev: false | ||||
|  | ||||
|   /plist@3.0.4: | ||||
|     resolution: {integrity: sha512-ksrr8y9+nXOxQB2osVNqrgvX/XQPOXaU4BQMKjYq8PvaY1U18mo+fKgBSwzK+luSyinOuPae956lSVcBwxlAMg==} | ||||
|     engines: {node: '>=6'} | ||||
|     dependencies: | ||||
|       base64-js: 1.5.1 | ||||
|       xmlbuilder: 9.0.7 | ||||
|     dev: false | ||||
|  | ||||
|   /posix-character-classes@0.1.1: | ||||
|     resolution: {integrity: sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=} | ||||
|     engines: {node: '>=0.10.0'} | ||||
| @@ -2785,29 +2704,12 @@ packages: | ||||
|     dev: true | ||||
|     optional: true | ||||
|  | ||||
|   /stream-to-buffer@0.1.0: | ||||
|     resolution: {integrity: sha512-Da4WoKaZyu3nf+bIdIifh7IPkFjARBnBK+pYqn0EUJqksjV9afojjaCCHUemH30Jmu7T2qcKvlZm2ykN38uzaw==} | ||||
|     engines: {node: '>= 0.8'} | ||||
|     dependencies: | ||||
|       stream-to: 0.2.2 | ||||
|     dev: false | ||||
|  | ||||
|   /stream-to@0.2.2: | ||||
|     resolution: {integrity: sha512-Kg1BSDTwgGiVMtTCJNlo7kk/xzL33ZuZveEBRt6rXw+f1WLK/8kmz2NVCT/Qnv0JkV85JOHcLhD82mnXsR3kPw==} | ||||
|     engines: {node: '>= 0.10.0'} | ||||
|     dev: false | ||||
|  | ||||
|   /stream-transform@2.1.3: | ||||
|     resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==} | ||||
|     dependencies: | ||||
|       mixme: 0.5.4 | ||||
|     dev: false | ||||
|  | ||||
|   /streamifier@0.1.1: | ||||
|     resolution: {integrity: sha512-zDgl+muIlWzXNsXeyUfOk9dChMjlpkq0DRsxujtYPgyJ676yQ8jEm6zzaaWHFDg5BNcLuif0eD2MTyJdZqXpdg==} | ||||
|     engines: {node: '>=0.10'} | ||||
|     dev: false | ||||
|  | ||||
|   /string-width@4.2.3: | ||||
|     resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} | ||||
|     engines: {node: '>=8'} | ||||
| @@ -3164,11 +3066,6 @@ packages: | ||||
|     engines: {node: '>=8'} | ||||
|     dev: false | ||||
|  | ||||
|   /xmlbuilder@9.0.7: | ||||
|     resolution: {integrity: sha512-7YXTQc3P2l9+0rjaUbLwMKRhtmwg1M1eDf6nag7urC7pIPYLD9W/jmzQ4ptRSUbodw5S0jfoGTflLemQibSpeQ==} | ||||
|     engines: {node: '>=4.0'} | ||||
|     dev: false | ||||
|  | ||||
|   /y18n@4.0.3: | ||||
|     resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} | ||||
|     dev: false | ||||
| @@ -3237,17 +3134,3 @@ packages: | ||||
|     dependencies: | ||||
|       buffer-crc32: 0.2.13 | ||||
|     dev: false | ||||
|  | ||||
|   github.com/sunnylqm/app-info-parser/f7748ce60278fb16f6f280d908d35025a241eb57: | ||||
|     resolution: {tarball: https://codeload.github.com/sunnylqm/app-info-parser/tar.gz/f7748ce60278fb16f6f280d908d35025a241eb57} | ||||
|     name: app-info-parser | ||||
|     version: 1.1.6 | ||||
|     hasBin: true | ||||
|     dependencies: | ||||
|       bplist-parser: 0.2.0 | ||||
|       bytebuffer: 5.0.1 | ||||
|       cgbi-to-png: 1.0.7 | ||||
|       commander: 7.2.0 | ||||
|       isomorphic-unzip: 1.1.5 | ||||
|       plist: 3.0.4 | ||||
|     dev: false | ||||
|   | ||||
							
								
								
									
										90
									
								
								src/utils/app-info-parser/apk.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/utils/app-info-parser/apk.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| const Zip = require('./zip') | ||||
| const { mapInfoResource, findApkIconPath, getBase64FromBuffer } = require('./utils') | ||||
| const ManifestName = /^androidmanifest\.xml$/ | ||||
| const ResourceName = /^resources\.arsc$/ | ||||
|  | ||||
| const ManifestXmlParser = require('./xml-parser/manifest') | ||||
| const ResourceFinder = require('./resource-finder') | ||||
|  | ||||
| class ApkParser extends Zip { | ||||
|   /** | ||||
|    * parser for parsing .apk file | ||||
|    * @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser | ||||
|    */ | ||||
|   constructor (file) { | ||||
|     super(file) | ||||
|     if (!(this instanceof ApkParser)) { | ||||
|       return new ApkParser(file) | ||||
|     } | ||||
|   } | ||||
|   parse () { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       this.getEntries([ManifestName, ResourceName]).then(buffers => { | ||||
|         if (!buffers[ManifestName]) { | ||||
|           throw new Error('AndroidManifest.xml can\'t be found.') | ||||
|         } | ||||
|         let apkInfo = this._parseManifest(buffers[ManifestName]) | ||||
|         let resourceMap | ||||
|         if (!buffers[ResourceName]) { | ||||
|           resolve(apkInfo) | ||||
|         } else { | ||||
|           // parse resourceMap | ||||
|           resourceMap = this._parseResourceMap(buffers[ResourceName]) | ||||
|           // update apkInfo with resourceMap | ||||
|           apkInfo = mapInfoResource(apkInfo, resourceMap) | ||||
|  | ||||
|           // find icon path and parse icon | ||||
|           const iconPath = findApkIconPath(apkInfo) | ||||
|           if (iconPath) { | ||||
|             this.getEntry(iconPath).then(iconBuffer => { | ||||
|               apkInfo.icon = iconBuffer ? getBase64FromBuffer(iconBuffer) : null | ||||
|               resolve(apkInfo) | ||||
|             }).catch(e => { | ||||
|               apkInfo.icon = null | ||||
|               resolve(apkInfo) | ||||
|               console.warn('[Warning] failed to parse icon: ', e) | ||||
|             }) | ||||
|           } else { | ||||
|             apkInfo.icon = null | ||||
|             resolve(apkInfo) | ||||
|           } | ||||
|         } | ||||
|       }).catch(e => { | ||||
|         reject(e) | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
|   /** | ||||
|    * Parse manifest | ||||
|    * @param {Buffer} buffer // manifest file's buffer | ||||
|    */ | ||||
|   _parseManifest (buffer) { | ||||
|     try { | ||||
|       const parser = new ManifestXmlParser(buffer, { | ||||
|         ignore: [ | ||||
|           'application.activity', | ||||
|           'application.service', | ||||
|           'application.receiver', | ||||
|           'application.provider', | ||||
|           'permission-group' | ||||
|         ] | ||||
|       }) | ||||
|       return parser.parse() | ||||
|     } catch (e) { | ||||
|       throw new Error('Parse AndroidManifest.xml error: ', e) | ||||
|     } | ||||
|   } | ||||
|   /** | ||||
|    * Parse resourceMap | ||||
|    * @param {Buffer} buffer // resourceMap file's buffer | ||||
|    */ | ||||
|   _parseResourceMap (buffer) { | ||||
|     try { | ||||
|       return new ResourceFinder().processResourceTable(buffer) | ||||
|     } catch (e) { | ||||
|       throw new Error('Parser resources.arsc error: ' + e) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| module.exports = ApkParser | ||||
							
								
								
									
										35
									
								
								src/utils/app-info-parser/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/utils/app-info-parser/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| const ApkParser = require('./apk') | ||||
| const IpaParser = require('./ipa') | ||||
| const supportFileTypes = ['ipa', 'apk'] | ||||
|  | ||||
| class AppInfoParser { | ||||
|   /** | ||||
|    * parser for parsing .ipa or .apk file | ||||
|    * @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser | ||||
|    */ | ||||
|   constructor (file) { | ||||
|     if (!file) { | ||||
|       throw new Error('Param miss: file(file\'s path in Node, instance of File or Blob in browser).') | ||||
|     } | ||||
|     const splits = (file.name || file).split('.') | ||||
|     const fileType = splits[splits.length - 1].toLowerCase() | ||||
|     if (!supportFileTypes.includes(fileType)) { | ||||
|       throw new Error('Unsupported file type, only support .ipa or .apk file.') | ||||
|     } | ||||
|     this.file = file | ||||
|  | ||||
|     switch (fileType) { | ||||
|       case 'ipa': | ||||
|         this.parser = new IpaParser(this.file) | ||||
|         break | ||||
|       case 'apk': | ||||
|         this.parser = new ApkParser(this.file) | ||||
|         break | ||||
|     } | ||||
|   } | ||||
|   parse () { | ||||
|     return this.parser.parse() | ||||
|   } | ||||
| } | ||||
|  | ||||
| module.exports = AppInfoParser | ||||
							
								
								
									
										92
									
								
								src/utils/app-info-parser/ipa.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/utils/app-info-parser/ipa.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | ||||
| const parsePlist = require('plist').parse | ||||
| const parseBplist = require('bplist-parser').parseBuffer | ||||
| const cgbiToPng = require('cgbi-to-png') | ||||
|  | ||||
| const Zip = require('./zip') | ||||
| const { findIpaIconPath, getBase64FromBuffer, isBrowser } = require('./utils') | ||||
|  | ||||
| const PlistName = new RegExp('payload/[^/]+?.app/info.plist$', 'i') | ||||
| const ProvisionName = /payload\/.+?\.app\/embedded.mobileprovision/ | ||||
|  | ||||
| class IpaParser extends Zip { | ||||
|   /** | ||||
|    * parser for parsing .ipa file | ||||
|    * @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser | ||||
|    */ | ||||
|   constructor (file) { | ||||
|     super(file) | ||||
|     if (!(this instanceof IpaParser)) { | ||||
|       return new IpaParser(file) | ||||
|     } | ||||
|   } | ||||
|   parse () { | ||||
|     return new Promise((resolve, reject) => { | ||||
|       this.getEntries([PlistName, ProvisionName]).then(buffers => { | ||||
|         if (!buffers[PlistName]) { | ||||
|           throw new Error('Info.plist can\'t be found.') | ||||
|         } | ||||
|         const plistInfo = this._parsePlist(buffers[PlistName]) | ||||
|         // parse mobile provision | ||||
|         const provisionInfo = this._parseProvision(buffers[ProvisionName]) | ||||
|         plistInfo.mobileProvision = provisionInfo | ||||
|  | ||||
|         // find icon path and parse icon | ||||
|         const iconRegex = new RegExp(findIpaIconPath(plistInfo).toLowerCase()) | ||||
|         this.getEntry(iconRegex).then(iconBuffer => { | ||||
|           try { | ||||
|             // In general, the ipa file's icon has been specially processed, should be converted | ||||
|             plistInfo.icon = iconBuffer ? getBase64FromBuffer(cgbiToPng.revert(iconBuffer)) : null | ||||
|           } catch (err) { | ||||
|             if (isBrowser()) { | ||||
|               // Normal conversion in other cases | ||||
|               plistInfo.icon = iconBuffer ? getBase64FromBuffer(window.btoa(String.fromCharCode(...iconBuffer))) : null | ||||
|             } else { | ||||
|               plistInfo.icon = null | ||||
|               console.warn('[Warning] failed to parse icon: ', err) | ||||
|             } | ||||
|           } | ||||
|           resolve(plistInfo) | ||||
|         }).catch(e => { | ||||
|           reject(e) | ||||
|         }) | ||||
|       }).catch(e => { | ||||
|         reject(e) | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
|   /** | ||||
|    * Parse plist | ||||
|    * @param {Buffer} buffer // plist file's buffer | ||||
|    */ | ||||
|   _parsePlist (buffer) { | ||||
|     let result | ||||
|     const bufferType = buffer[0] | ||||
|     if (bufferType === 60 || bufferType === '<' || bufferType === 239) { | ||||
|       result = parsePlist(buffer.toString()) | ||||
|     } else if (bufferType === 98) { | ||||
|       result = parseBplist(buffer)[0] | ||||
|     } else { | ||||
|       throw new Error('Unknown plist buffer type.') | ||||
|     } | ||||
|     return result | ||||
|   } | ||||
|   /** | ||||
|    * parse provision | ||||
|    * @param {Buffer} buffer // provision file's buffer | ||||
|    */ | ||||
|   _parseProvision (buffer) { | ||||
|     let info = {} | ||||
|     if (buffer) { | ||||
|       let content = buffer.toString('utf-8') | ||||
|       const firstIndex = content.indexOf('<?xml') | ||||
|       const endIndex = content.indexOf('</plist>') | ||||
|       content = content.slice(firstIndex, endIndex + 8) | ||||
|       if (content) { | ||||
|         info = parsePlist(content) | ||||
|       } | ||||
|     } | ||||
|     return info | ||||
|   } | ||||
| } | ||||
|  | ||||
| module.exports = IpaParser | ||||
							
								
								
									
										499
									
								
								src/utils/app-info-parser/resource-finder.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										499
									
								
								src/utils/app-info-parser/resource-finder.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,499 @@ | ||||
| /** | ||||
|  * Code translated from a C# project https://github.com/hylander0/Iteedee.ApkReader/blob/master/Iteedee.ApkReader/ApkResourceFinder.cs | ||||
|  * | ||||
|  * Decode binary file `resources.arsc` from a .apk file to a JavaScript Object. | ||||
|  */ | ||||
|  | ||||
| var ByteBuffer = require("bytebuffer"); | ||||
|  | ||||
| var DEBUG = false; | ||||
|  | ||||
| var RES_STRING_POOL_TYPE = 0x0001; | ||||
| var RES_TABLE_TYPE = 0x0002; | ||||
| var RES_TABLE_PACKAGE_TYPE = 0x0200; | ||||
| var RES_TABLE_TYPE_TYPE = 0x0201; | ||||
| var RES_TABLE_TYPE_SPEC_TYPE = 0x0202; | ||||
|  | ||||
| // The 'data' holds a ResTable_ref, a reference to another resource | ||||
| // table entry. | ||||
| var TYPE_REFERENCE = 0x01; | ||||
| // The 'data' holds an index into the containing resource table's | ||||
| // global value string pool. | ||||
| var TYPE_STRING = 0x03; | ||||
|  | ||||
| function ResourceFinder() { | ||||
|   this.valueStringPool = null; | ||||
|   this.typeStringPool = null; | ||||
|   this.keyStringPool = null; | ||||
|  | ||||
|   this.package_id = 0; | ||||
|  | ||||
|   this.responseMap = {}; | ||||
|   this.entryMap = {}; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Same to C# BinaryReader.readBytes | ||||
|  * | ||||
|  * @param bb ByteBuffer | ||||
|  * @param len length | ||||
|  * @returns {Buffer} | ||||
|  */ | ||||
| ResourceFinder.readBytes = function(bb, len) { | ||||
|   var uint8Array = new Uint8Array(len); | ||||
|   for (var i = 0; i < len; i++) { | ||||
|     uint8Array[i] = bb.readUint8(); | ||||
|   } | ||||
|  | ||||
|   return ByteBuffer.wrap(uint8Array, "binary", true); | ||||
| }; | ||||
|  | ||||
| // | ||||
| /** | ||||
|  * | ||||
|  * @param {ByteBuffer} bb | ||||
|  * @return {Map<String, Set<String>>} | ||||
|  */ | ||||
| ResourceFinder.prototype.processResourceTable = function(resourceBuffer) { | ||||
|   const bb = ByteBuffer.wrap(resourceBuffer, "binary", true); | ||||
|  | ||||
|   // Resource table structure | ||||
|   var type = bb.readShort(), | ||||
|     headerSize = bb.readShort(), | ||||
|     size = bb.readInt(), | ||||
|     packageCount = bb.readInt(), | ||||
|     buffer, | ||||
|     bb2; | ||||
|   if (type != RES_TABLE_TYPE) { | ||||
|     throw new Error("No RES_TABLE_TYPE found!"); | ||||
|   } | ||||
|   if (size != bb.limit) { | ||||
|     throw new Error("The buffer size not matches to the resource table size."); | ||||
|   } | ||||
|   bb.offset = headerSize; | ||||
|  | ||||
|   var realStringPoolCount = 0, | ||||
|     realPackageCount = 0; | ||||
|  | ||||
|   while (true) { | ||||
|     var pos, t, hs, s; | ||||
|     try { | ||||
|       pos = bb.offset; | ||||
|       t = bb.readShort(); | ||||
|       hs = bb.readShort(); | ||||
|       s = bb.readInt(); | ||||
|     } catch (e) { | ||||
|       break; | ||||
|     } | ||||
|     if (t == RES_STRING_POOL_TYPE) { | ||||
|       // Process the string pool | ||||
|       if (realStringPoolCount == 0) { | ||||
|         // Only the first string pool is processed. | ||||
|         if (DEBUG) { | ||||
|           console.log("Processing the string pool ..."); | ||||
|         } | ||||
|  | ||||
|         buffer = new ByteBuffer(s); | ||||
|         bb.offset = pos; | ||||
|         bb.prependTo(buffer); | ||||
|  | ||||
|         bb2 = ByteBuffer.wrap(buffer, "binary", true); | ||||
|  | ||||
|         bb2.LE(); | ||||
|         this.valueStringPool = this.processStringPool(bb2); | ||||
|       } | ||||
|       realStringPoolCount++; | ||||
|     } else if (t == RES_TABLE_PACKAGE_TYPE) { | ||||
|       // Process the package | ||||
|       if (DEBUG) { | ||||
|         console.log("Processing the package " + realPackageCount + " ..."); | ||||
|       } | ||||
|  | ||||
|       buffer = new ByteBuffer(s); | ||||
|       bb.offset = pos; | ||||
|       bb.prependTo(buffer); | ||||
|  | ||||
|       bb2 = ByteBuffer.wrap(buffer, "binary", true); | ||||
|       bb2.LE(); | ||||
|       this.processPackage(bb2); | ||||
|  | ||||
|       realPackageCount++; | ||||
|     } else { | ||||
|       throw new Error("Unsupported type"); | ||||
|     } | ||||
|     bb.offset = pos + s; | ||||
|     if (!bb.remaining()) break; | ||||
|   } | ||||
|  | ||||
|   if (realStringPoolCount != 1) { | ||||
|     throw new Error("More than 1 string pool found!"); | ||||
|   } | ||||
|   if (realPackageCount != packageCount) { | ||||
|     throw new Error("Real package count not equals the declared count."); | ||||
|   } | ||||
|  | ||||
|   return this.responseMap; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * | ||||
|  * @param {ByteBuffer} bb | ||||
|  */ | ||||
| ResourceFinder.prototype.processPackage = function(bb) { | ||||
|   // Package structure | ||||
|   var type = bb.readShort(), | ||||
|     headerSize = bb.readShort(), | ||||
|     size = bb.readInt(), | ||||
|     id = bb.readInt(); | ||||
|  | ||||
|   this.package_id = id; | ||||
|  | ||||
|   for (var i = 0; i < 256; ++i) { | ||||
|     bb.readUint8(); | ||||
|   } | ||||
|  | ||||
|   var typeStrings = bb.readInt(), | ||||
|     lastPublicType = bb.readInt(), | ||||
|     keyStrings = bb.readInt(), | ||||
|     lastPublicKey = bb.readInt(); | ||||
|  | ||||
|   if (typeStrings != headerSize) { | ||||
|     throw new Error( | ||||
|       "TypeStrings must immediately following the package structure header." | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   if (DEBUG) { | ||||
|     console.log("Type strings:"); | ||||
|   } | ||||
|  | ||||
|   var lastPosition = bb.offset; | ||||
|   bb.offset = typeStrings; | ||||
|   var bbTypeStrings = ResourceFinder.readBytes(bb, bb.limit - bb.offset); | ||||
|   bb.offset = lastPosition; | ||||
|   this.typeStringPool = this.processStringPool(bbTypeStrings); | ||||
|  | ||||
|   // Key strings | ||||
|   if (DEBUG) { | ||||
|     console.log("Key strings:"); | ||||
|   } | ||||
|  | ||||
|   bb.offset = keyStrings; | ||||
|   var key_type = bb.readShort(), | ||||
|     key_headerSize = bb.readShort(), | ||||
|     key_size = bb.readInt(); | ||||
|  | ||||
|   lastPosition = bb.offset; | ||||
|   bb.offset = keyStrings; | ||||
|   var bbKeyStrings = ResourceFinder.readBytes(bb, bb.limit - bb.offset); | ||||
|   bb.offset = lastPosition; | ||||
|   this.keyStringPool = this.processStringPool(bbKeyStrings); | ||||
|  | ||||
|   // Iterate through all chunks | ||||
|   var typeSpecCount = 0; | ||||
|   var typeCount = 0; | ||||
|  | ||||
|   bb.offset = keyStrings + key_size; | ||||
|  | ||||
|   var bb2; | ||||
|  | ||||
|   while (true) { | ||||
|     var pos = bb.offset; | ||||
|     try { | ||||
|       var t = bb.readShort(); | ||||
|       var hs = bb.readShort(); | ||||
|       var s = bb.readInt(); | ||||
|     } catch (e) { | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     if (t == RES_TABLE_TYPE_SPEC_TYPE) { | ||||
|       bb.offset = pos; | ||||
|       bb2 = ResourceFinder.readBytes(bb, s); | ||||
|       this.processTypeSpec(bb2); | ||||
|  | ||||
|       typeSpecCount++; | ||||
|     } else if (t == RES_TABLE_TYPE_TYPE) { | ||||
|       bb.offset = pos; | ||||
|       bb2 = ResourceFinder.readBytes(bb, s); | ||||
|       this.processType(bb2); | ||||
|  | ||||
|       typeCount++; | ||||
|     } | ||||
|  | ||||
|     if (s == 0) { | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     bb.offset = pos + s; | ||||
|  | ||||
|     if (!bb.remaining()) { | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * | ||||
|  * @param {ByteBuffer} bb | ||||
|  */ | ||||
| ResourceFinder.prototype.processType = function(bb) { | ||||
|   var type = bb.readShort(), | ||||
|     headerSize = bb.readShort(), | ||||
|     size = bb.readInt(), | ||||
|     id = bb.readByte(), | ||||
|     res0 = bb.readByte(), | ||||
|     res1 = bb.readShort(), | ||||
|     entryCount = bb.readInt(), | ||||
|     entriesStart = bb.readInt(); | ||||
|  | ||||
|   var refKeys = {}; | ||||
|  | ||||
|   var config_size = bb.readInt(); | ||||
|  | ||||
|   // Skip the config data | ||||
|   bb.offset = headerSize; | ||||
|  | ||||
|   if (headerSize + entryCount * 4 != entriesStart) { | ||||
|     throw new Error("HeaderSize, entryCount and entriesStart are not valid."); | ||||
|   } | ||||
|  | ||||
|   // Start to get entry indices | ||||
|   var entryIndices = new Array(entryCount); | ||||
|   for (var i = 0; i < entryCount; ++i) { | ||||
|     entryIndices[i] = bb.readInt(); | ||||
|   } | ||||
|  | ||||
|   // Get entries | ||||
|   for (var i = 0; i < entryCount; ++i) { | ||||
|     if (entryIndices[i] == -1) continue; | ||||
|  | ||||
|     var resource_id = (this.package_id << 24) | (id << 16) | i; | ||||
|  | ||||
|     var pos = bb.offset, | ||||
|       entry_size, | ||||
|       entry_flag, | ||||
|       entry_key, | ||||
|       value_size, | ||||
|       value_res0, | ||||
|       value_dataType, | ||||
|       value_data; | ||||
|     try { | ||||
|       entry_size = bb.readShort() | ||||
|       entry_flag = bb.readShort() | ||||
|       entry_key = bb.readInt() | ||||
|     } catch (e) { | ||||
|       break | ||||
|     } | ||||
|  | ||||
|     // Get the value (simple) or map (complex) | ||||
|  | ||||
|     var FLAG_COMPLEX = 0x0001; | ||||
|     if ((entry_flag & FLAG_COMPLEX) == 0) { | ||||
|       // Simple case | ||||
|       value_size = bb.readShort(); | ||||
|       value_res0 = bb.readByte(); | ||||
|       value_dataType = bb.readByte(); | ||||
|       value_data = bb.readInt(); | ||||
|  | ||||
|       var idStr = Number(resource_id).toString(16); | ||||
|       var keyStr = this.keyStringPool[entry_key]; | ||||
|  | ||||
|       var data = null; | ||||
|  | ||||
|       if (DEBUG) { | ||||
|         console.log( | ||||
|           "Entry 0x" + idStr + ", key: " + keyStr + ", simple value type: " | ||||
|         ); | ||||
|       } | ||||
|  | ||||
|       var key = parseInt(idStr, 16); | ||||
|  | ||||
|       var entryArr = this.entryMap[key]; | ||||
|       if (entryArr == null) { | ||||
|         entryArr = []; | ||||
|       } | ||||
|       entryArr.push(keyStr); | ||||
|  | ||||
|       this.entryMap[key] = entryArr; | ||||
|  | ||||
|       if (value_dataType == TYPE_STRING) { | ||||
|         data = this.valueStringPool[value_data]; | ||||
|  | ||||
|         if (DEBUG) { | ||||
|           console.log(", data: " + this.valueStringPool[value_data] + ""); | ||||
|         } | ||||
|       } else if (value_dataType == TYPE_REFERENCE) { | ||||
|         var hexIndex = Number(value_data).toString(16); | ||||
|  | ||||
|         refKeys[idStr] = value_data; | ||||
|       } else { | ||||
|         data = "" + value_data; | ||||
|         if (DEBUG) { | ||||
|           console.log(", data: " + value_data + ""); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       this.putIntoMap("@" + idStr, data); | ||||
|     } else { | ||||
|       // Complex case | ||||
|       var entry_parent = bb.readInt(); | ||||
|       var entry_count = bb.readInt(); | ||||
|  | ||||
|       for (var j = 0; j < entry_count; ++j) { | ||||
|         var ref_name = bb.readInt(); | ||||
|         value_size = bb.readShort(); | ||||
|         value_res0 = bb.readByte(); | ||||
|         value_dataType = bb.readByte(); | ||||
|         value_data = bb.readInt(); | ||||
|       } | ||||
|  | ||||
|       if (DEBUG) { | ||||
|         console.log( | ||||
|           "Entry 0x" + | ||||
|             Number(resource_id).toString(16) + | ||||
|             ", key: " + | ||||
|             this.keyStringPool[entry_key] + | ||||
|             ", complex value, not printed." | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   for (var refK in refKeys) { | ||||
|     var values = this.responseMap[ | ||||
|       "@" + | ||||
|         Number(refKeys[refK]) | ||||
|           .toString(16) | ||||
|           .toUpperCase() | ||||
|     ]; | ||||
|     if (values != null && Object.keys(values).length < 1000) { | ||||
|       for (var value in values) { | ||||
|         this.putIntoMap("@" + refK, values[value]); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * | ||||
|  * @param {ByteBuffer} bb | ||||
|  * @return {Array} | ||||
|  */ | ||||
| ResourceFinder.prototype.processStringPool = function(bb) { | ||||
|   // String pool structure | ||||
|   // | ||||
|   var type = bb.readShort(), | ||||
|     headerSize = bb.readShort(), | ||||
|     size = bb.readInt(), | ||||
|     stringCount = bb.readInt(), | ||||
|     styleCount = bb.readInt(), | ||||
|     flags = bb.readInt(), | ||||
|     stringsStart = bb.readInt(), | ||||
|     stylesStart = bb.readInt(), | ||||
|     u16len, | ||||
|     buffer; | ||||
|  | ||||
|   var isUTF_8 = (flags & 256) != 0; | ||||
|  | ||||
|   var offsets = new Array(stringCount); | ||||
|   for (var i = 0; i < stringCount; ++i) { | ||||
|     offsets[i] = bb.readInt(); | ||||
|   } | ||||
|  | ||||
|   var strings = new Array(stringCount); | ||||
|  | ||||
|   for (var i = 0; i < stringCount; ++i) { | ||||
|     var pos = stringsStart + offsets[i]; | ||||
|     bb.offset = pos; | ||||
|  | ||||
|     strings[i] = ""; | ||||
|  | ||||
|     if (isUTF_8) { | ||||
|       u16len = bb.readUint8(); | ||||
|  | ||||
|       if ((u16len & 0x80) != 0) { | ||||
|         u16len = ((u16len & 0x7f) << 8) + bb.readUint8(); | ||||
|       } | ||||
|  | ||||
|       var u8len = bb.readUint8(); | ||||
|       if ((u8len & 0x80) != 0) { | ||||
|         u8len = ((u8len & 0x7f) << 8) + bb.readUint8(); | ||||
|       } | ||||
|  | ||||
|       if (u8len > 0) { | ||||
|         buffer = ResourceFinder.readBytes(bb, u8len); | ||||
|         try { | ||||
|           strings[i] = ByteBuffer.wrap(buffer, "utf8", true).toString("utf8"); | ||||
|         } catch (e) { | ||||
|           if (DEBUG) { | ||||
|             console.error(e); | ||||
|             console.log("Error when turning buffer to utf-8 string."); | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         strings[i] = ""; | ||||
|       } | ||||
|     } else { | ||||
|       u16len = bb.readUint16(); | ||||
|       if ((u16len & 0x8000) != 0) { | ||||
|         // larger than 32768 | ||||
|         u16len = ((u16len & 0x7fff) << 16) + bb.readUint16(); | ||||
|       } | ||||
|  | ||||
|       if (u16len > 0) { | ||||
|         var len = u16len * 2; | ||||
|         buffer = ResourceFinder.readBytes(bb, len); | ||||
|         try { | ||||
|           strings[i] = ByteBuffer.wrap(buffer, "utf8", true).toString("utf8"); | ||||
|         } catch (e) { | ||||
|           if (DEBUG) { | ||||
|             console.error(e); | ||||
|             console.log("Error when turning buffer to utf-8 string."); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (DEBUG) { | ||||
|       console.log("Parsed value: {0}", strings[i]); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return strings; | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * | ||||
|  * @param {ByteBuffer} bb | ||||
|  */ | ||||
| ResourceFinder.prototype.processTypeSpec = function(bb) { | ||||
|   var type = bb.readShort(), | ||||
|     headerSize = bb.readShort(), | ||||
|     size = bb.readInt(), | ||||
|     id = bb.readByte(), | ||||
|     res0 = bb.readByte(), | ||||
|     res1 = bb.readShort(), | ||||
|     entryCount = bb.readInt(); | ||||
|  | ||||
|   if (DEBUG) { | ||||
|     console.log("Processing type spec " + this.typeStringPool[id - 1] + "..."); | ||||
|   } | ||||
|  | ||||
|   var flags = new Array(entryCount); | ||||
|  | ||||
|   for (var i = 0; i < entryCount; ++i) { | ||||
|     flags[i] = bb.readInt(); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| ResourceFinder.prototype.putIntoMap = function(resId, value) { | ||||
|   if (this.responseMap[resId.toUpperCase()] == null) { | ||||
|     this.responseMap[resId.toUpperCase()] = [] | ||||
|   } | ||||
|   if(value){ | ||||
|     this.responseMap[resId.toUpperCase()].push(value) | ||||
|   } | ||||
| }; | ||||
|  | ||||
| module.exports = ResourceFinder; | ||||
							
								
								
									
										167
									
								
								src/utils/app-info-parser/utils.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								src/utils/app-info-parser/utils.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,167 @@ | ||||
| function objectType (o) { | ||||
|   return Object.prototype.toString.call(o).slice(8, -1).toLowerCase() | ||||
| } | ||||
|  | ||||
| function isArray (o) { | ||||
|   return objectType(o) === 'array' | ||||
| } | ||||
|  | ||||
| function isObject (o) { | ||||
|   return objectType(o) === 'object' | ||||
| } | ||||
|  | ||||
| function isPrimitive (o) { | ||||
|   return o === null || ['boolean', 'number', 'string', 'undefined'].includes(objectType(o)) | ||||
| } | ||||
|  | ||||
| function isBrowser () { | ||||
|   return ( | ||||
|     typeof process === 'undefined' || | ||||
|     Object.prototype.toString.call(process) !== '[object process]' | ||||
|   ) | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * map file place with resourceMap | ||||
|  * @param {Object} apkInfo // json info parsed from .apk file | ||||
|  * @param {Object} resourceMap // resourceMap | ||||
|  */ | ||||
| function mapInfoResource (apkInfo, resourceMap) { | ||||
|   iteratorObj(apkInfo) | ||||
|   return apkInfo | ||||
|   function iteratorObj (obj) { | ||||
|     for (var i in obj) { | ||||
|       if (isArray(obj[i])) { | ||||
|         iteratorArray(obj[i]) | ||||
|       } else if (isObject(obj[i])) { | ||||
|         iteratorObj(obj[i]) | ||||
|       } else if (isPrimitive(obj[i])) { | ||||
|         if (isResources(obj[i])) { | ||||
|           obj[i] = resourceMap[transKeyToMatchResourceMap(obj[i])] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function iteratorArray (array) { | ||||
|     const l = array.length | ||||
|     for (let i = 0; i < l; i++) { | ||||
|       if (isArray(array[i])) { | ||||
|         iteratorArray(array[i]) | ||||
|       } else if (isObject(array[i])) { | ||||
|         iteratorObj(array[i]) | ||||
|       } else if (isPrimitive(array[i])) { | ||||
|         if (isResources(array[i])) { | ||||
|           array[i] = resourceMap[transKeyToMatchResourceMap(array[i])] | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   function isResources (attrValue) { | ||||
|     if (!attrValue) return false | ||||
|     if (typeof attrValue !== 'string') { | ||||
|       attrValue = attrValue.toString() | ||||
|     } | ||||
|     return attrValue.indexOf('resourceId:') === 0 | ||||
|   } | ||||
|  | ||||
|   function transKeyToMatchResourceMap (resourceId) { | ||||
|     return '@' + resourceId.replace('resourceId:0x', '').toUpperCase() | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * find .apk file's icon path from json info | ||||
|  * @param info // json info parsed from .apk file | ||||
|  */ | ||||
| function findApkIconPath (info) { | ||||
|   if (!info.application.icon || !info.application.icon.splice) { | ||||
|     return '' | ||||
|   } | ||||
|   const rulesMap = { | ||||
|     mdpi: 48, | ||||
|     hdpi: 72, | ||||
|     xhdpi: 96, | ||||
|     xxdpi: 144, | ||||
|     xxxhdpi: 192 | ||||
|   } | ||||
|   const resultMap = {} | ||||
|   const maxDpiIcon = { dpi: 120, icon: '' } | ||||
|  | ||||
|   for (const i in rulesMap) { | ||||
|     info.application.icon.some((icon) => { | ||||
|       if (icon && icon.indexOf(i) !== -1) { | ||||
|         resultMap['application-icon-' + rulesMap[i]] = icon | ||||
|         return true | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|     // get the maximal size icon | ||||
|     if ( | ||||
|       resultMap['application-icon-' + rulesMap[i]] && | ||||
|       rulesMap[i] >= maxDpiIcon.dpi | ||||
|     ) { | ||||
|       maxDpiIcon.dpi = rulesMap[i] | ||||
|       maxDpiIcon.icon = resultMap['application-icon-' + rulesMap[i]] | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (Object.keys(resultMap).length === 0 || !maxDpiIcon.icon) { | ||||
|     maxDpiIcon.dpi = 120 | ||||
|     maxDpiIcon.icon = info.application.icon[0] || '' | ||||
|     resultMap['applicataion-icon-120'] = maxDpiIcon.icon | ||||
|   } | ||||
|   return maxDpiIcon.icon | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * find .ipa file's icon path from json info | ||||
|  * @param info // json info parsed from .ipa file | ||||
|  */ | ||||
| function findIpaIconPath (info) { | ||||
|   if ( | ||||
|     info.CFBundleIcons && | ||||
|     info.CFBundleIcons.CFBundlePrimaryIcon && | ||||
|     info.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles && | ||||
|     info.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles.length | ||||
|   ) { | ||||
|     return info.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles[info.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles.length - 1] | ||||
|   } else if (info.CFBundleIconFiles && info.CFBundleIconFiles.length) { | ||||
|     return info.CFBundleIconFiles[info.CFBundleIconFiles.length - 1] | ||||
|   } else { | ||||
|     return '.app/Icon.png' | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * transform buffer to base64 | ||||
|  * @param {Buffer} buffer | ||||
|  */ | ||||
| function getBase64FromBuffer (buffer) { | ||||
|   return 'data:image/png;base64,' + buffer.toString('base64') | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 去除unicode空字符 | ||||
|  * @param {String} str | ||||
|  */ | ||||
| function decodeNullUnicode (str) { | ||||
|   if (typeof str === 'string') { | ||||
|     // eslint-disable-next-line | ||||
|     str = str.replace(/\u0000/g, '') | ||||
|   } | ||||
|   return str | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   isArray, | ||||
|   isObject, | ||||
|   isPrimitive, | ||||
|   isBrowser, | ||||
|   mapInfoResource, | ||||
|   findApkIconPath, | ||||
|   findIpaIconPath, | ||||
|   getBase64FromBuffer, | ||||
|   decodeNullUnicode | ||||
| } | ||||
							
								
								
									
										674
									
								
								src/utils/app-info-parser/xml-parser/binary.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										674
									
								
								src/utils/app-info-parser/xml-parser/binary.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,674 @@ | ||||
| // From https://github.com/openstf/adbkit-apkreader | ||||
| const NodeType = { | ||||
|   ELEMENT_NODE: 1, | ||||
|   ATTRIBUTE_NODE: 2, | ||||
|   CDATA_SECTION_NODE: 4 | ||||
| } | ||||
|  | ||||
| const ChunkType = { | ||||
|   NULL: 0x0000, | ||||
|   STRING_POOL: 0x0001, | ||||
|   TABLE: 0x0002, | ||||
|   XML: 0x0003, | ||||
|   XML_FIRST_CHUNK: 0x0100, | ||||
|   XML_START_NAMESPACE: 0x0100, | ||||
|   XML_END_NAMESPACE: 0x0101, | ||||
|   XML_START_ELEMENT: 0x0102, | ||||
|   XML_END_ELEMENT: 0x0103, | ||||
|   XML_CDATA: 0x0104, | ||||
|   XML_LAST_CHUNK: 0x017f, | ||||
|   XML_RESOURCE_MAP: 0x0180, | ||||
|   TABLE_PACKAGE: 0x0200, | ||||
|   TABLE_TYPE: 0x0201, | ||||
|   TABLE_TYPE_SPEC: 0x0202 | ||||
| } | ||||
|  | ||||
| const StringFlags = { | ||||
|   SORTED: 1 << 0, | ||||
|   UTF8: 1 << 8 | ||||
| } | ||||
|  | ||||
| // Taken from android.util.TypedValue | ||||
| const TypedValue = { | ||||
|   COMPLEX_MANTISSA_MASK: 0x00ffffff, | ||||
|   COMPLEX_MANTISSA_SHIFT: 0x00000008, | ||||
|   COMPLEX_RADIX_0p23: 0x00000003, | ||||
|   COMPLEX_RADIX_16p7: 0x00000001, | ||||
|   COMPLEX_RADIX_23p0: 0x00000000, | ||||
|   COMPLEX_RADIX_8p15: 0x00000002, | ||||
|   COMPLEX_RADIX_MASK: 0x00000003, | ||||
|   COMPLEX_RADIX_SHIFT: 0x00000004, | ||||
|   COMPLEX_UNIT_DIP: 0x00000001, | ||||
|   COMPLEX_UNIT_FRACTION: 0x00000000, | ||||
|   COMPLEX_UNIT_FRACTION_PARENT: 0x00000001, | ||||
|   COMPLEX_UNIT_IN: 0x00000004, | ||||
|   COMPLEX_UNIT_MASK: 0x0000000f, | ||||
|   COMPLEX_UNIT_MM: 0x00000005, | ||||
|   COMPLEX_UNIT_PT: 0x00000003, | ||||
|   COMPLEX_UNIT_PX: 0x00000000, | ||||
|   COMPLEX_UNIT_SHIFT: 0x00000000, | ||||
|   COMPLEX_UNIT_SP: 0x00000002, | ||||
|   DENSITY_DEFAULT: 0x00000000, | ||||
|   DENSITY_NONE: 0x0000ffff, | ||||
|   TYPE_ATTRIBUTE: 0x00000002, | ||||
|   TYPE_DIMENSION: 0x00000005, | ||||
|   TYPE_FIRST_COLOR_INT: 0x0000001c, | ||||
|   TYPE_FIRST_INT: 0x00000010, | ||||
|   TYPE_FLOAT: 0x00000004, | ||||
|   TYPE_FRACTION: 0x00000006, | ||||
|   TYPE_INT_BOOLEAN: 0x00000012, | ||||
|   TYPE_INT_COLOR_ARGB4: 0x0000001e, | ||||
|   TYPE_INT_COLOR_ARGB8: 0x0000001c, | ||||
|   TYPE_INT_COLOR_RGB4: 0x0000001f, | ||||
|   TYPE_INT_COLOR_RGB8: 0x0000001d, | ||||
|   TYPE_INT_DEC: 0x00000010, | ||||
|   TYPE_INT_HEX: 0x00000011, | ||||
|   TYPE_LAST_COLOR_INT: 0x0000001f, | ||||
|   TYPE_LAST_INT: 0x0000001f, | ||||
|   TYPE_NULL: 0x00000000, | ||||
|   TYPE_REFERENCE: 0x00000001, | ||||
|   TYPE_STRING: 0x00000003 | ||||
| } | ||||
|  | ||||
| class BinaryXmlParser { | ||||
|   constructor (buffer, options = {}) { | ||||
|     this.buffer = buffer | ||||
|     this.cursor = 0 | ||||
|     this.strings = [] | ||||
|     this.resources = [] | ||||
|     this.document = null | ||||
|     this.parent = null | ||||
|     this.stack = [] | ||||
|     this.debug = options.debug || false | ||||
|   } | ||||
|  | ||||
|   readU8 () { | ||||
|     this.debug && console.group('readU8') | ||||
|     this.debug && console.debug('cursor:', this.cursor) | ||||
|     const val = this.buffer[this.cursor] | ||||
|     this.debug && console.debug('value:', val) | ||||
|     this.cursor += 1 | ||||
|     this.debug && console.groupEnd() | ||||
|     return val | ||||
|   } | ||||
|  | ||||
|   readU16 () { | ||||
|     this.debug && console.group('readU16') | ||||
|     this.debug && console.debug('cursor:', this.cursor) | ||||
|     const val = this.buffer.readUInt16LE(this.cursor) | ||||
|     this.debug && console.debug('value:', val) | ||||
|     this.cursor += 2 | ||||
|     this.debug && console.groupEnd() | ||||
|     return val | ||||
|   } | ||||
|  | ||||
|   readS32 () { | ||||
|     this.debug && console.group('readS32') | ||||
|     this.debug && console.debug('cursor:', this.cursor) | ||||
|     const val = this.buffer.readInt32LE(this.cursor) | ||||
|     this.debug && console.debug('value:', val) | ||||
|     this.cursor += 4 | ||||
|     this.debug && console.groupEnd() | ||||
|     return val | ||||
|   } | ||||
|  | ||||
|   readU32 () { | ||||
|     this.debug && console.group('readU32') | ||||
|     this.debug && console.debug('cursor:', this.cursor) | ||||
|     const val = this.buffer.readUInt32LE(this.cursor) | ||||
|     this.debug && console.debug('value:', val) | ||||
|     this.cursor += 4 | ||||
|     this.debug && console.groupEnd() | ||||
|     return val | ||||
|   } | ||||
|  | ||||
|   readLength8 () { | ||||
|     this.debug && console.group('readLength8') | ||||
|     let len = this.readU8() | ||||
|     if (len & 0x80) { | ||||
|       len = (len & 0x7f) << 8 | ||||
|       len += this.readU8() | ||||
|     } | ||||
|     this.debug && console.debug('length:', len) | ||||
|     this.debug && console.groupEnd() | ||||
|     return len | ||||
|   } | ||||
|  | ||||
|   readLength16 () { | ||||
|     this.debug && console.group('readLength16') | ||||
|     let len = this.readU16() | ||||
|     if (len & 0x8000) { | ||||
|       len = (len & 0x7fff) << 16 | ||||
|       len += this.readU16() | ||||
|     } | ||||
|     this.debug && console.debug('length:', len) | ||||
|     this.debug && console.groupEnd() | ||||
|     return len | ||||
|   } | ||||
|  | ||||
|   readDimension () { | ||||
|     this.debug && console.group('readDimension') | ||||
|  | ||||
|     const dimension = { | ||||
|       value: null, | ||||
|       unit: null, | ||||
|       rawUnit: null | ||||
|     } | ||||
|  | ||||
|     const value = this.readU32() | ||||
|     const unit = dimension.value & 0xff | ||||
|  | ||||
|     dimension.value = value >> 8 | ||||
|     dimension.rawUnit = unit | ||||
|  | ||||
|     switch (unit) { | ||||
|       case TypedValue.COMPLEX_UNIT_MM: | ||||
|         dimension.unit = 'mm' | ||||
|         break | ||||
|       case TypedValue.COMPLEX_UNIT_PX: | ||||
|         dimension.unit = 'px' | ||||
|         break | ||||
|       case TypedValue.COMPLEX_UNIT_DIP: | ||||
|         dimension.unit = 'dp' | ||||
|         break | ||||
|       case TypedValue.COMPLEX_UNIT_SP: | ||||
|         dimension.unit = 'sp' | ||||
|         break | ||||
|       case TypedValue.COMPLEX_UNIT_PT: | ||||
|         dimension.unit = 'pt' | ||||
|         break | ||||
|       case TypedValue.COMPLEX_UNIT_IN: | ||||
|         dimension.unit = 'in' | ||||
|         break | ||||
|     } | ||||
|  | ||||
|     this.debug && console.groupEnd() | ||||
|  | ||||
|     return dimension | ||||
|   } | ||||
|  | ||||
|   readFraction () { | ||||
|     this.debug && console.group('readFraction') | ||||
|  | ||||
|     const fraction = { | ||||
|       value: null, | ||||
|       type: null, | ||||
|       rawType: null | ||||
|     } | ||||
|  | ||||
|     const value = this.readU32() | ||||
|     const type = value & 0xf | ||||
|  | ||||
|     fraction.value = this.convertIntToFloat(value >> 4) | ||||
|     fraction.rawType = type | ||||
|  | ||||
|     switch (type) { | ||||
|       case TypedValue.COMPLEX_UNIT_FRACTION: | ||||
|         fraction.type = '%' | ||||
|         break | ||||
|       case TypedValue.COMPLEX_UNIT_FRACTION_PARENT: | ||||
|         fraction.type = '%p' | ||||
|         break | ||||
|     } | ||||
|  | ||||
|     this.debug && console.groupEnd() | ||||
|  | ||||
|     return fraction | ||||
|   } | ||||
|  | ||||
|   readHex24 () { | ||||
|     this.debug && console.group('readHex24') | ||||
|     var val = (this.readU32() & 0xffffff).toString(16) | ||||
|     this.debug && console.groupEnd() | ||||
|     return val | ||||
|   } | ||||
|  | ||||
|   readHex32 () { | ||||
|     this.debug && console.group('readHex32') | ||||
|     var val = this.readU32().toString(16) | ||||
|     this.debug && console.groupEnd() | ||||
|     return val | ||||
|   } | ||||
|  | ||||
|   readTypedValue () { | ||||
|     this.debug && console.group('readTypedValue') | ||||
|  | ||||
|     const typedValue = { | ||||
|       value: null, | ||||
|       type: null, | ||||
|       rawType: null | ||||
|     } | ||||
|  | ||||
|     const start = this.cursor | ||||
|  | ||||
|     let size = this.readU16() | ||||
|     /* const zero = */ this.readU8() | ||||
|     const dataType = this.readU8() | ||||
|  | ||||
|     // Yes, there has been a real world APK where the size is malformed. | ||||
|     if (size === 0) { | ||||
|       size = 8 | ||||
|     } | ||||
|  | ||||
|     typedValue.rawType = dataType | ||||
|  | ||||
|     switch (dataType) { | ||||
|       case TypedValue.TYPE_INT_DEC: | ||||
|         typedValue.value = this.readS32() | ||||
|         typedValue.type = 'int_dec' | ||||
|         break | ||||
|       case TypedValue.TYPE_INT_HEX: | ||||
|         typedValue.value = this.readS32() | ||||
|         typedValue.type = 'int_hex' | ||||
|         break | ||||
|       case TypedValue.TYPE_STRING: | ||||
|         var ref = this.readS32() | ||||
|         typedValue.value = ref > 0 ? this.strings[ref] : '' | ||||
|         typedValue.type = 'string' | ||||
|         break | ||||
|       case TypedValue.TYPE_REFERENCE: | ||||
|         var id = this.readU32() | ||||
|         typedValue.value = `resourceId:0x${id.toString(16)}` | ||||
|         typedValue.type = 'reference' | ||||
|         break | ||||
|       case TypedValue.TYPE_INT_BOOLEAN: | ||||
|         typedValue.value = this.readS32() !== 0 | ||||
|         typedValue.type = 'boolean' | ||||
|         break | ||||
|       case TypedValue.TYPE_NULL: | ||||
|         this.readU32() | ||||
|         typedValue.value = null | ||||
|         typedValue.type = 'null' | ||||
|         break | ||||
|       case TypedValue.TYPE_INT_COLOR_RGB8: | ||||
|         typedValue.value = this.readHex24() | ||||
|         typedValue.type = 'rgb8' | ||||
|         break | ||||
|       case TypedValue.TYPE_INT_COLOR_RGB4: | ||||
|         typedValue.value = this.readHex24() | ||||
|         typedValue.type = 'rgb4' | ||||
|         break | ||||
|       case TypedValue.TYPE_INT_COLOR_ARGB8: | ||||
|         typedValue.value = this.readHex32() | ||||
|         typedValue.type = 'argb8' | ||||
|         break | ||||
|       case TypedValue.TYPE_INT_COLOR_ARGB4: | ||||
|         typedValue.value = this.readHex32() | ||||
|         typedValue.type = 'argb4' | ||||
|         break | ||||
|       case TypedValue.TYPE_DIMENSION: | ||||
|         typedValue.value = this.readDimension() | ||||
|         typedValue.type = 'dimension' | ||||
|         break | ||||
|       case TypedValue.TYPE_FRACTION: | ||||
|         typedValue.value = this.readFraction() | ||||
|         typedValue.type = 'fraction' | ||||
|         break | ||||
|       default: { | ||||
|         const type = dataType.toString(16) | ||||
|         console.debug(`Not sure what to do with typed value of type 0x${type}, falling back to reading an uint32.`) | ||||
|         typedValue.value = this.readU32() | ||||
|         typedValue.type = 'unknown' | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // Ensure we consume the whole value | ||||
|     const end = start + size | ||||
|     if (this.cursor !== end) { | ||||
|       const type = dataType.toString(16) | ||||
|       const diff = end - this.cursor | ||||
|       console.debug(`Cursor is off by ${diff} bytes at ${this.cursor} at supposed end \ | ||||
| of typed value of type 0x${type}. The typed value started at offset ${start} \ | ||||
| and is supposed to end at offset ${end}. Ignoring the rest of the value.`) | ||||
|       this.cursor = end | ||||
|     } | ||||
|  | ||||
|     this.debug && console.groupEnd() | ||||
|  | ||||
|     return typedValue | ||||
|   } | ||||
|  | ||||
|   // https://twitter.com/kawasima/status/427730289201139712 | ||||
|   convertIntToFloat (int) { | ||||
|     const buf = new ArrayBuffer(4) | ||||
|     ;(new Int32Array(buf))[0] = int | ||||
|     return (new Float32Array(buf))[0] | ||||
|   } | ||||
|  | ||||
|   readString (encoding) { | ||||
|     this.debug && console.group('readString', encoding) | ||||
|     switch (encoding) { | ||||
|       case 'utf-8': | ||||
|         var stringLength = this.readLength8(encoding) | ||||
|         this.debug && console.debug('stringLength:', stringLength) | ||||
|         var byteLength = this.readLength8(encoding) | ||||
|         this.debug && console.debug('byteLength:', byteLength) | ||||
|         var value = this.buffer.toString(encoding, this.cursor, (this.cursor += byteLength)) | ||||
|         this.debug && console.debug('value:', value) | ||||
|         this.debug && console.groupEnd() | ||||
|         return value | ||||
|       case 'ucs2': | ||||
|         stringLength = this.readLength16(encoding) | ||||
|         this.debug && console.debug('stringLength:', stringLength) | ||||
|         byteLength = stringLength * 2 | ||||
|         this.debug && console.debug('byteLength:', byteLength) | ||||
|         value = this.buffer.toString(encoding, this.cursor, (this.cursor += byteLength)) | ||||
|         this.debug && console.debug('value:', value) | ||||
|         this.debug && console.groupEnd() | ||||
|         return value | ||||
|       default: | ||||
|         throw new Error(`Unsupported encoding '${encoding}'`) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   readChunkHeader () { | ||||
|     this.debug && console.group('readChunkHeader') | ||||
|     var header = { | ||||
|       startOffset: this.cursor, | ||||
|       chunkType: this.readU16(), | ||||
|       headerSize: this.readU16(), | ||||
|       chunkSize: this.readU32() | ||||
|     } | ||||
|     this.debug && console.debug('startOffset:', header.startOffset) | ||||
|     this.debug && console.debug('chunkType:', header.chunkType) | ||||
|     this.debug && console.debug('headerSize:', header.headerSize) | ||||
|     this.debug && console.debug('chunkSize:', header.chunkSize) | ||||
|     this.debug && console.groupEnd() | ||||
|     return header | ||||
|   } | ||||
|  | ||||
|   readStringPool (header) { | ||||
|     this.debug && console.group('readStringPool') | ||||
|  | ||||
|     header.stringCount = this.readU32() | ||||
|     this.debug && console.debug('stringCount:', header.stringCount) | ||||
|     header.styleCount = this.readU32() | ||||
|     this.debug && console.debug('styleCount:', header.styleCount) | ||||
|     header.flags = this.readU32() | ||||
|     this.debug && console.debug('flags:', header.flags) | ||||
|     header.stringsStart = this.readU32() | ||||
|     this.debug && console.debug('stringsStart:', header.stringsStart) | ||||
|     header.stylesStart = this.readU32() | ||||
|     this.debug && console.debug('stylesStart:', header.stylesStart) | ||||
|  | ||||
|     if (header.chunkType !== ChunkType.STRING_POOL) { | ||||
|       throw new Error('Invalid string pool header') | ||||
|     } | ||||
|  | ||||
|     const offsets = [] | ||||
|     for (let i = 0, l = header.stringCount; i < l; ++i) { | ||||
|       this.debug && console.debug('offset:', i) | ||||
|       offsets.push(this.readU32()) | ||||
|     } | ||||
|  | ||||
|     const sorted = (header.flags & StringFlags.SORTED) === StringFlags.SORTED | ||||
|     this.debug && console.debug('sorted:', sorted) | ||||
|     const encoding = (header.flags & StringFlags.UTF8) === StringFlags.UTF8 | ||||
|       ? 'utf-8' | ||||
|       : 'ucs2' | ||||
|     this.debug && console.debug('encoding:', encoding) | ||||
|  | ||||
|     const stringsStart = header.startOffset + header.stringsStart | ||||
|     this.cursor = stringsStart | ||||
|     for (let i = 0, l = header.stringCount; i < l; ++i) { | ||||
|       this.debug && console.debug('string:', i) | ||||
|       this.debug && console.debug('offset:', offsets[i]) | ||||
|       this.cursor = stringsStart + offsets[i] | ||||
|       this.strings.push(this.readString(encoding)) | ||||
|     } | ||||
|  | ||||
|     // Skip styles | ||||
|     this.cursor = header.startOffset + header.chunkSize | ||||
|  | ||||
|     this.debug && console.groupEnd() | ||||
|  | ||||
|     return null | ||||
|   } | ||||
|  | ||||
|   readResourceMap (header) { | ||||
|     this.debug && console.group('readResourceMap') | ||||
|     const count = Math.floor((header.chunkSize - header.headerSize) / 4) | ||||
|     for (let i = 0; i < count; ++i) { | ||||
|       this.resources.push(this.readU32()) | ||||
|     } | ||||
|     this.debug && console.groupEnd() | ||||
|     return null | ||||
|   } | ||||
|  | ||||
|   readXmlNamespaceStart (/* header */) { | ||||
|     this.debug && console.group('readXmlNamespaceStart') | ||||
|  | ||||
|     /* const line = */ this.readU32() | ||||
|     /* const commentRef = */ this.readU32() | ||||
|     /* const prefixRef = */ this.readS32() | ||||
|     /* const uriRef = */ this.readS32() | ||||
|  | ||||
|     // We don't currently care about the values, but they could | ||||
|     // be accessed like so: | ||||
|     // | ||||
|     // namespaceURI.prefix = this.strings[prefixRef] // if prefixRef > 0 | ||||
|     // namespaceURI.uri = this.strings[uriRef] // if uriRef > 0 | ||||
|  | ||||
|     this.debug && console.groupEnd() | ||||
|  | ||||
|     return null | ||||
|   } | ||||
|  | ||||
|   readXmlNamespaceEnd (/* header */) { | ||||
|     this.debug && console.group('readXmlNamespaceEnd') | ||||
|  | ||||
|     /* const line = */ this.readU32() | ||||
|     /* const commentRef = */ this.readU32() | ||||
|     /* const prefixRef = */ this.readS32() | ||||
|     /* const uriRef = */ this.readS32() | ||||
|  | ||||
|     // We don't currently care about the values, but they could | ||||
|     // be accessed like so: | ||||
|     // | ||||
|     // namespaceURI.prefix = this.strings[prefixRef] // if prefixRef > 0 | ||||
|     // namespaceURI.uri = this.strings[uriRef] // if uriRef > 0 | ||||
|  | ||||
|     this.debug && console.groupEnd() | ||||
|  | ||||
|     return null | ||||
|   } | ||||
|  | ||||
|   readXmlElementStart (/* header */) { | ||||
|     this.debug && console.group('readXmlElementStart') | ||||
|  | ||||
|     const node = { | ||||
|       namespaceURI: null, | ||||
|       nodeType: NodeType.ELEMENT_NODE, | ||||
|       nodeName: null, | ||||
|       attributes: [], | ||||
|       childNodes: [] | ||||
|     } | ||||
|  | ||||
|     /* const line = */ this.readU32() | ||||
|     /* const commentRef = */ this.readU32() | ||||
|     const nsRef = this.readS32() | ||||
|     const nameRef = this.readS32() | ||||
|  | ||||
|     if (nsRef > 0) { | ||||
|       node.namespaceURI = this.strings[nsRef] | ||||
|     } | ||||
|  | ||||
|     node.nodeName = this.strings[nameRef] | ||||
|  | ||||
|     /* const attrStart = */ this.readU16() | ||||
|     /* const attrSize = */ this.readU16() | ||||
|     const attrCount = this.readU16() | ||||
|     /* const idIndex = */ this.readU16() | ||||
|     /* const classIndex = */ this.readU16() | ||||
|     /* const styleIndex = */ this.readU16() | ||||
|  | ||||
|     for (let i = 0; i < attrCount; ++i) { | ||||
|       node.attributes.push(this.readXmlAttribute()) | ||||
|     } | ||||
|  | ||||
|     if (this.document) { | ||||
|       this.parent.childNodes.push(node) | ||||
|       this.parent = node | ||||
|     } else { | ||||
|       this.document = (this.parent = node) | ||||
|     } | ||||
|  | ||||
|     this.stack.push(node) | ||||
|  | ||||
|     this.debug && console.groupEnd() | ||||
|  | ||||
|     return node | ||||
|   } | ||||
|  | ||||
|   readXmlAttribute () { | ||||
|     this.debug && console.group('readXmlAttribute') | ||||
|  | ||||
|     const attr = { | ||||
|       namespaceURI: null, | ||||
|       nodeType: NodeType.ATTRIBUTE_NODE, | ||||
|       nodeName: null, | ||||
|       name: null, | ||||
|       value: null, | ||||
|       typedValue: null | ||||
|     } | ||||
|  | ||||
|     const nsRef = this.readS32() | ||||
|     const nameRef = this.readS32() | ||||
|     const valueRef = this.readS32() | ||||
|  | ||||
|     if (nsRef > 0) { | ||||
|       attr.namespaceURI = this.strings[nsRef] | ||||
|     } | ||||
|  | ||||
|     attr.nodeName = attr.name = this.strings[nameRef] | ||||
|  | ||||
|     if (valueRef > 0) { | ||||
|       // some apk have versionName with special characters | ||||
|       if (attr.name === 'versionName') { | ||||
|         // only keep printable characters | ||||
|         // https://www.ascii-code.com/characters/printable-characters | ||||
|         this.strings[valueRef] = this.strings[valueRef].replace(/[^\x21-\x7E]/g, '') | ||||
|       } | ||||
|       attr.value = this.strings[valueRef] | ||||
|     } | ||||
|  | ||||
|     attr.typedValue = this.readTypedValue() | ||||
|  | ||||
|     this.debug && console.groupEnd() | ||||
|  | ||||
|     return attr | ||||
|   } | ||||
|  | ||||
|   readXmlElementEnd (/* header */) { | ||||
|     this.debug && console.group('readXmlCData') | ||||
|  | ||||
|     /* const line = */ this.readU32() | ||||
|     /* const commentRef = */ this.readU32() | ||||
|     /* const nsRef = */ this.readS32() | ||||
|     /* const nameRef = */ this.readS32() | ||||
|  | ||||
|     this.stack.pop() | ||||
|     this.parent = this.stack[this.stack.length - 1] | ||||
|  | ||||
|     this.debug && console.groupEnd() | ||||
|  | ||||
|     return null | ||||
|   } | ||||
|  | ||||
|   readXmlCData (/* header */) { | ||||
|     this.debug && console.group('readXmlCData') | ||||
|  | ||||
|     const cdata = { | ||||
|       namespaceURI: null, | ||||
|       nodeType: NodeType.CDATA_SECTION_NODE, | ||||
|       nodeName: '#cdata', | ||||
|       data: null, | ||||
|       typedValue: null | ||||
|     } | ||||
|  | ||||
|     /* const line = */ this.readU32() | ||||
|     /* const commentRef = */ this.readU32() | ||||
|     const dataRef = this.readS32() | ||||
|  | ||||
|     if (dataRef > 0) { | ||||
|       cdata.data = this.strings[dataRef] | ||||
|     } | ||||
|  | ||||
|     cdata.typedValue = this.readTypedValue() | ||||
|  | ||||
|     this.parent.childNodes.push(cdata) | ||||
|  | ||||
|     this.debug && console.groupEnd() | ||||
|  | ||||
|     return cdata | ||||
|   } | ||||
|  | ||||
|   readNull (header) { | ||||
|     this.debug && console.group('readNull') | ||||
|     this.cursor += header.chunkSize - header.headerSize | ||||
|     this.debug && console.groupEnd() | ||||
|     return null | ||||
|   } | ||||
|  | ||||
|   parse () { | ||||
|     this.debug && console.group('BinaryXmlParser.parse') | ||||
|  | ||||
|     const xmlHeader = this.readChunkHeader() | ||||
|     if (xmlHeader.chunkType !== ChunkType.XML) { | ||||
|       throw new Error('Invalid XML header') | ||||
|     } | ||||
|  | ||||
|     while (this.cursor < this.buffer.length) { | ||||
|       this.debug && console.group('chunk') | ||||
|       const start = this.cursor | ||||
|       const header = this.readChunkHeader() | ||||
|       switch (header.chunkType) { | ||||
|         case ChunkType.STRING_POOL: | ||||
|           this.readStringPool(header) | ||||
|           break | ||||
|         case ChunkType.XML_RESOURCE_MAP: | ||||
|           this.readResourceMap(header) | ||||
|           break | ||||
|         case ChunkType.XML_START_NAMESPACE: | ||||
|           this.readXmlNamespaceStart(header) | ||||
|           break | ||||
|         case ChunkType.XML_END_NAMESPACE: | ||||
|           this.readXmlNamespaceEnd(header) | ||||
|           break | ||||
|         case ChunkType.XML_START_ELEMENT: | ||||
|           this.readXmlElementStart(header) | ||||
|           break | ||||
|         case ChunkType.XML_END_ELEMENT: | ||||
|           this.readXmlElementEnd(header) | ||||
|           break | ||||
|         case ChunkType.XML_CDATA: | ||||
|           this.readXmlCData(header) | ||||
|           break | ||||
|         case ChunkType.NULL: | ||||
|           this.readNull(header) | ||||
|           break | ||||
|         default: | ||||
|           throw new Error(`Unsupported chunk type '${header.chunkType}'`) | ||||
|       } | ||||
|  | ||||
|       // Ensure we consume the whole chunk | ||||
|       const end = start + header.chunkSize | ||||
|       if (this.cursor !== end) { | ||||
|         const diff = end - this.cursor | ||||
|         const type = header.chunkType.toString(16) | ||||
|         console.debug(`Cursor is off by ${diff} bytes at ${this.cursor} at supposed \ | ||||
| end of chunk of type 0x${type}. The chunk started at offset ${start} and is \ | ||||
| supposed to end at offset ${end}. Ignoring the rest of the chunk.`) | ||||
|         this.cursor = end | ||||
|       } | ||||
|  | ||||
|       this.debug && console.groupEnd() | ||||
|     } | ||||
|  | ||||
|     this.debug && console.groupEnd() | ||||
|  | ||||
|     return this.document | ||||
|   } | ||||
| } | ||||
|  | ||||
| module.exports = BinaryXmlParser | ||||
							
								
								
									
										216
									
								
								src/utils/app-info-parser/xml-parser/manifest.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								src/utils/app-info-parser/xml-parser/manifest.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,216 @@ | ||||
| // From https://github.com/openstf/adbkit-apkreader | ||||
| const BinaryXmlParser = require('./binary') | ||||
|  | ||||
| const INTENT_MAIN = 'android.intent.action.MAIN' | ||||
| const CATEGORY_LAUNCHER = 'android.intent.category.LAUNCHER' | ||||
|  | ||||
| class ManifestParser { | ||||
|   constructor (buffer, options = {}) { | ||||
|     this.buffer = buffer | ||||
|     this.xmlParser = new BinaryXmlParser(this.buffer, options) | ||||
|   } | ||||
|  | ||||
|   collapseAttributes (element) { | ||||
|     const collapsed = Object.create(null) | ||||
|     for (let attr of Array.from(element.attributes)) { | ||||
|       collapsed[attr.name] = attr.typedValue.value | ||||
|     } | ||||
|     return collapsed | ||||
|   } | ||||
|  | ||||
|   parseIntents (element, target) { | ||||
|     target.intentFilters = [] | ||||
|     target.metaData = [] | ||||
|  | ||||
|     return element.childNodes.forEach(element => { | ||||
|       switch (element.nodeName) { | ||||
|         case 'intent-filter': { | ||||
|           const intentFilter = this.collapseAttributes(element) | ||||
|  | ||||
|           intentFilter.actions = [] | ||||
|           intentFilter.categories = [] | ||||
|           intentFilter.data = [] | ||||
|  | ||||
|           element.childNodes.forEach(element => { | ||||
|             switch (element.nodeName) { | ||||
|               case 'action': | ||||
|                 intentFilter.actions.push(this.collapseAttributes(element)) | ||||
|                 break | ||||
|               case 'category': | ||||
|                 intentFilter.categories.push(this.collapseAttributes(element)) | ||||
|                 break | ||||
|               case 'data': | ||||
|                 intentFilter.data.push(this.collapseAttributes(element)) | ||||
|                 break | ||||
|             } | ||||
|           }) | ||||
|  | ||||
|           target.intentFilters.push(intentFilter) | ||||
|           break | ||||
|         } | ||||
|         case 'meta-data': | ||||
|           target.metaData.push(this.collapseAttributes(element)) | ||||
|           break | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   parseApplication (element) { | ||||
|     const app = this.collapseAttributes(element) | ||||
|  | ||||
|     app.activities = [] | ||||
|     app.activityAliases = [] | ||||
|     app.launcherActivities = [] | ||||
|     app.services = [] | ||||
|     app.receivers = [] | ||||
|     app.providers = [] | ||||
|     app.usesLibraries = [] | ||||
|     app.metaData = [] | ||||
|  | ||||
|     element.childNodes.forEach(element => { | ||||
|       switch (element.nodeName) { | ||||
|         case 'activity': { | ||||
|           const activity = this.collapseAttributes(element) | ||||
|           this.parseIntents(element, activity) | ||||
|           app.activities.push(activity) | ||||
|           if (this.isLauncherActivity(activity)) { | ||||
|             app.launcherActivities.push(activity) | ||||
|           } | ||||
|           break | ||||
|         } | ||||
|         case 'activity-alias': { | ||||
|           const activityAlias = this.collapseAttributes(element) | ||||
|           this.parseIntents(element, activityAlias) | ||||
|           app.activityAliases.push(activityAlias) | ||||
|           if (this.isLauncherActivity(activityAlias)) { | ||||
|             app.launcherActivities.push(activityAlias) | ||||
|           } | ||||
|           break | ||||
|         } | ||||
|         case 'service': { | ||||
|           const service = this.collapseAttributes(element) | ||||
|           this.parseIntents(element, service) | ||||
|           app.services.push(service) | ||||
|           break | ||||
|         } | ||||
|         case 'receiver': { | ||||
|           const receiver = this.collapseAttributes(element) | ||||
|           this.parseIntents(element, receiver) | ||||
|           app.receivers.push(receiver) | ||||
|           break | ||||
|         } | ||||
|         case 'provider': { | ||||
|           const provider = this.collapseAttributes(element) | ||||
|  | ||||
|           provider.grantUriPermissions = [] | ||||
|           provider.metaData = [] | ||||
|           provider.pathPermissions = [] | ||||
|  | ||||
|           element.childNodes.forEach(element => { | ||||
|             switch (element.nodeName) { | ||||
|               case 'grant-uri-permission': | ||||
|                 provider.grantUriPermissions.push(this.collapseAttributes(element)) | ||||
|                 break | ||||
|               case 'meta-data': | ||||
|                 provider.metaData.push(this.collapseAttributes(element)) | ||||
|                 break | ||||
|               case 'path-permission': | ||||
|                 provider.pathPermissions.push(this.collapseAttributes(element)) | ||||
|                 break | ||||
|             } | ||||
|           }) | ||||
|  | ||||
|           app.providers.push(provider) | ||||
|           break | ||||
|         } | ||||
|         case 'uses-library': | ||||
|           app.usesLibraries.push(this.collapseAttributes(element)) | ||||
|           break | ||||
|         case 'meta-data': | ||||
|           app.metaData.push(this.collapseAttributes(element)) | ||||
|           break | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|     return app | ||||
|   } | ||||
|  | ||||
|   isLauncherActivity (activity) { | ||||
|     return activity.intentFilters.some(function (filter) { | ||||
|       const hasMain = filter.actions.some(action => action.name === INTENT_MAIN) | ||||
|       if (!hasMain) { | ||||
|         return false | ||||
|       } | ||||
|       return filter.categories.some(category => category.name === CATEGORY_LAUNCHER) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   parse () { | ||||
|     const document = this.xmlParser.parse() | ||||
|     const manifest = this.collapseAttributes(document) | ||||
|  | ||||
|     manifest.usesPermissions = [] | ||||
|     manifest.usesPermissionsSDK23 = [] | ||||
|     manifest.permissions = [] | ||||
|     manifest.permissionTrees = [] | ||||
|     manifest.permissionGroups = [] | ||||
|     manifest.instrumentation = null | ||||
|     manifest.usesSdk = null | ||||
|     manifest.usesConfiguration = null | ||||
|     manifest.usesFeatures = [] | ||||
|     manifest.supportsScreens = null | ||||
|     manifest.compatibleScreens = [] | ||||
|     manifest.supportsGlTextures = [] | ||||
|     manifest.application = Object.create(null) | ||||
|  | ||||
|     document.childNodes.forEach(element => { | ||||
|       switch (element.nodeName) { | ||||
|         case 'uses-permission': | ||||
|           manifest.usesPermissions.push(this.collapseAttributes(element)) | ||||
|           break | ||||
|         case 'uses-permission-sdk-23': | ||||
|           manifest.usesPermissionsSDK23.push(this.collapseAttributes(element)) | ||||
|           break | ||||
|         case 'permission': | ||||
|           manifest.permissions.push(this.collapseAttributes(element)) | ||||
|           break | ||||
|         case 'permission-tree': | ||||
|           manifest.permissionTrees.push(this.collapseAttributes(element)) | ||||
|           break | ||||
|         case 'permission-group': | ||||
|           manifest.permissionGroups.push(this.collapseAttributes(element)) | ||||
|           break | ||||
|         case 'instrumentation': | ||||
|           manifest.instrumentation = this.collapseAttributes(element) | ||||
|           break | ||||
|         case 'uses-sdk': | ||||
|           manifest.usesSdk = this.collapseAttributes(element) | ||||
|           break | ||||
|         case 'uses-configuration': | ||||
|           manifest.usesConfiguration = this.collapseAttributes(element) | ||||
|           break | ||||
|         case 'uses-feature': | ||||
|           manifest.usesFeatures.push(this.collapseAttributes(element)) | ||||
|           break | ||||
|         case 'supports-screens': | ||||
|           manifest.supportsScreens = this.collapseAttributes(element) | ||||
|           break | ||||
|         case 'compatible-screens': | ||||
|           element.childNodes.forEach(screen => { | ||||
|             return manifest.compatibleScreens.push(this.collapseAttributes(screen)) | ||||
|           }) | ||||
|           break | ||||
|         case 'supports-gl-texture': | ||||
|           manifest.supportsGlTextures.push(this.collapseAttributes(element)) | ||||
|           break | ||||
|         case 'application': | ||||
|           manifest.application = this.parseApplication(element) | ||||
|           break | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|     return manifest | ||||
|   } | ||||
| } | ||||
|  | ||||
| module.exports = ManifestParser | ||||
							
								
								
									
										48
									
								
								src/utils/app-info-parser/zip.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/utils/app-info-parser/zip.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | ||||
| const Unzip = require('isomorphic-unzip') | ||||
| const { isBrowser, decodeNullUnicode } = require('./utils') | ||||
|  | ||||
| class Zip { | ||||
|   constructor (file) { | ||||
|     if (isBrowser()) { | ||||
|       if (!(file instanceof window.Blob || typeof file.size !== 'undefined')) { | ||||
|         throw new Error('Param error: [file] must be an instance of Blob or File in browser.') | ||||
|       } | ||||
|       this.file = file | ||||
|     } else { | ||||
|       if (typeof file !== 'string') { | ||||
|         throw new Error('Param error: [file] must be file path in Node.') | ||||
|       } | ||||
|       this.file = require('path').resolve(file) | ||||
|     } | ||||
|     this.unzip = new Unzip(this.file) | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * get entries by regexps, the return format is: { <filename>: <Buffer|Blob> } | ||||
|    * @param {Array} regexps // regexps for matching files | ||||
|    * @param {String} type // return type, can be buffer or blob, default buffer | ||||
|    */ | ||||
|   getEntries (regexps, type = 'buffer') { | ||||
|     regexps = regexps.map(regex => decodeNullUnicode(regex)) | ||||
|     return new Promise((resolve, reject) => { | ||||
|       this.unzip.getBuffer(regexps, { type }, (err, buffers) => { | ||||
|         err ? reject(err) : resolve(buffers) | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
|   /** | ||||
|    * get entry by regex, return an instance of Buffer or Blob | ||||
|    * @param {Regex} regex // regex for matching file | ||||
|    * @param {String} type // return type, can be buffer or blob, default buffer | ||||
|    */ | ||||
|   getEntry (regex, type = 'buffer') { | ||||
|     regex = decodeNullUnicode(regex) | ||||
|     return new Promise((resolve, reject) => { | ||||
|       this.unzip.getBuffer([regex], { type }, (err, buffers) => { | ||||
|         err ? reject(err) : resolve(buffers[regex]) | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|  | ||||
| module.exports = Zip | ||||
| @@ -2,7 +2,7 @@ import fs from 'fs-extra'; | ||||
| import os from 'os'; | ||||
| import path from 'path'; | ||||
| import pkg from '../../package.json'; | ||||
| import AppInfoParser from 'app-info-parser'; | ||||
| import AppInfoParser from './app-info-parser'; | ||||
|  | ||||
| import read from 'read'; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 sunnylqm
					sunnylqm