在 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 对象。
// 解压ipa
this.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的第一个元素设置bufferTpe
const 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
、公共秘钥
、指纹
等具体信息。并提供下载统计等丰富功能。