在 TS 中解析 ipa 文件
ipa
即Xcode
打包出來的APP的安裝包
,通過解析ipa
中的文件,我們可以獲得APP
的DisplayName
、Version
、BundleIdentifier
等信息,
同時也可以獲取到APP證書
的相關信息,包括APP
的安裝環境
、證書的有效期
、APP開通的功能
、可安裝設備的UDID
、公共秘鑰
、指紋
等。
解析 ipa 可用的工具
在NPM 官網上搜索 app、ipa、package 或 parser
等信息,
可直接使用的插件有:app-info-parser
,但是其不支持 TS,經過詢問,作者并不明確何時可以適配 TS。
js-app-parser
是支持 TS 的解析插件,但是解析 ipa
時會出錯,解析 apk
時是正確的(解析非標準打包的 apk 會出錯,解決見下一篇)。
綜合分析以上插件源碼,找到 ipa
解析失敗問題并解決,創建一個支持 TS
的 ts-package-parser
。
js-app-parser
解析出錯的地方是解析info.plist
的時候出錯了。
分析 ipa
解析 ipa
,實際需要解析的文件是 info.plist
、AppIcon
。
而 ipa
實際上就是壓縮包。只要將其解壓縮即可得到全部內容,再從中找到需要的文件,就可以得到 APP 的信息。
mobileprovision
文件中包含 APP 的證書等信息,此文件可由后端進行解析。
解析 ipa
解壓縮 ipa
主流的解壓縮工具為 jszip
,并支持TS
,使用 jszip
解壓縮 ipa
文件,得到ipa
里的所有文件。
解析過程中需要用到的方法
// 創建jszip對象this._jsZip = new JSZip();/*** 解壓文件* @param blob 文件內容* @returns 文件數據:JSZip*/unZipFile(blob: Blob | ArrayBuffer) {return new Promise((resolve, reject) => {this._jsZip.loadAsync(blob).then((zipObjc: JSZip) => {return resolve(zipObjc);}).catch((e) => {return reject('解析File失敗');});});}/*** 生成文件* @param path 要壓縮的文件路徑* @param type 文件類型:OutputType* @returns 生成的文件*/zipFilePathToNeedType<T extends OutputType>(path: string, type: T) {return new Promise((resolve, reject) => {this._jsZip.file(path).async(type).then((result) => {return resolve(result);}).catch(() => {return reject('生成文件失敗');});});}
通過以上解壓縮方法即可得到解壓后的 ipa 對象。
// 解壓ipathis.unZipFile(file).then((zipObjc: JSZip) => {const names = Object.getOwnPropertyNames(zipObjc.files);});// 通過zipObjc得到ipa的全部文件名稱和路徑const names = Object.getOwnPropertyNames(zipObjc.files);// 通過遍歷names得到info.plist文件const plistRegex = /^Payload\/(?:.*)\.app\/Info.plist$/;let plistPaht = '';for (let i = 0; i < names.length; i++) {if (plistRegex.test(names[i])) {plistPath = names[i];break;}}
解析 info.plist 文件
獲取到 info.plist 文件路徑后,讀取到文件內容,類型為:arraybuffer
使用 jszip 創建文件,將得到的文件,轉換為 buffer 文件。
import bufferLib from 'buffer';import { parse as PlistParse } from 'plist';import bplist from 'bplist-parser';this.zipFilePathToNeedType(plistPath, 'arraybuffer').then((arrBuffer: ArrayBuffer) => {// 創建buffer對象const buffer = bufferLib.Buffer.from(arrBuffer);// 根據buffer的第一個元素設置bufferTpeconst bufferType = buffer[0] as number | string;// 解析結果對象let result = null;if (bufferType == 60 || bufferType == '<' || bufferType == 239) {result = PlistParse(buffer.toString()) as any;} else if (bufferType == 98 || bufferType == 'b') {result = bplist.parseBuffer(buffer)[0];} else {throw new Error('Unknown plist buffer type.');}}
至此即可解析出 info.plist 的信息。
Info.name = result.CFBundleDisplayName || result.CFBundleName;
Info.versionName = result.CFBundleShortVersionString;
Info.versionCode = result.CFBundleVersion;
Info.ubndleId = result.CFBundleIdentifier;
Info.platform = 'ios';
// 設置icon信息,解析icon需要用到
if (result.CFBundleIcons) {const icons = result.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles;if (icons) {Info.icon = icons[icons.length - 1];}
}
解析 AppIcon
獲取 icon 路徑、名稱等信息,解析獲取到 png 文件。
// 使用 js-app-parser 解析icon的方法import { parsePNG } from 'js-app-parser/dist/ios/png-parse';// appIcon路徑const appIconRegex = /^Payload\/(?:.*)\.app\/AppIcon[0-9]{2}x[0-9]{2}@[2-3]x.png$/;/*** 解析ipa中的icon.png* @param zipObjc 已解析的ipa對象數據* @param bundleUploadInfo 已解析的info.plist數據對象* @returns ApplicationModel對象*/parserFileToPngIcon(zipObjc: JSZip, bundleUploadInfo: BundleUploadModel): Promise<BundleUploadModel> {return new Promise((resolve, reject) => {// 獲取ipa中的全部文件及路徑const names = Object.getOwnPropertyNames(zipObjc.files);// 解析info.plist時獲取的if (bundleUploadInfo.icon) {// icon的file對象let icon = void 0;for (let i = 0; i < names.length; i++) {if (names[i].indexOf(bundleUploadInfo.icon) >= 0) {// 將icon路徑生成file類型數據icon = zipObjc.files[names[i]];break;}}if (icon) {bundleUploadInfo.icon = icon.name;// 解析icon數據this.zipFilePathToNeedType(icon.name, 'uint8array').then((data: Uint8Array) => {const iconPng = parsePNG(data);bundleUploadInfo.iconSteam = iconPng;bundleUploadInfo.iconUrl = URL.createObjectURL(new Blob([iconPng]));resolve(bundleUploadInfo);}).catch((err) => {reject(err);});} else {resolve(bundleUploadInfo);}} else {resolve(bundleUploadInfo);}});}
壓縮生成文件
ipa 中其他重要的證書信息等存儲在 mobileprovision 中。同樣的通過 nams 獲取到 mobileprovision,
之后將 info.plist、mobileprovision 等文件壓縮為同一個文件。
// 描述文件路徑const provisonRegex = /^Payload\/(?:._)\.app\/(?:._).mobileprovision$/;/*** 壓縮解析后需要的文件* @param bundleUploadInfo 解析后的對象* @returns BundleUploadModel*/async unzipFilePathToIpaOrApk(bundleUploadInfo: BundleUploadModel): Promise<BundleUploadModel> {const jszip = new JSZip();const paths = [];paths.push(bundleUploadInfo.plistPath);paths.push(bundleUploadInfo.provisionPath);for (let i = 0; i < paths.length; i++) {const path = paths[i];const name = path.substring(path.indexOf('app/') + 4);await this.zipFilePathToNeedType(path, 'blob').then((result: Blob) => {jszip.folder(`Payload/${bundleUploadInfo.name}.app/`).file(name, result);});}return new Promise((resolve, reject) => {jszip.generateAsync({type: 'blob', // 壓縮類型compression: 'DEFLATE', // STORE:默認不壓縮 DEFLATE:需要壓縮compressionOptions: {level: 9, // 壓縮等級1~9 1壓縮速度最快,9最優壓縮方式},mimeType: 'bundleUploadInfo/iphone',}).then((fileZip) => {bundleUploadInfo.ipaZip = fileZip;resolve(bundleUploadInfo);}).catch((err) => {reject(err);});});}
總結
ipa 的解析主要是解析 info.plist、appIcon。
應用之家即采用了此種方式進行解析。
上傳 ipa 后并能解析到應用的安裝環境
、證書的有效期
、APP開通的功能
、可安裝設備的UDID
、公共秘鑰
、指紋
等具體信息。并提供下載統計等豐富功能。