mirror of
				https://gitcode.com/github-mirrors/react-native-update-cli.git
				synced 2025-10-31 14:53:11 +08:00 
			
		
		
		
	v1.20.6
This commit is contained in:
		
							
								
								
									
										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 | ||||
		Reference in New Issue
	
	Block a user
	 sunnylqm
					sunnylqm