在 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
、公共秘钥
、指纹
等具体信息。并提供下载统计等丰富功能。