在 TS 中解析 ipa 文件

在 TS 中解析 ipa 文件

ipaXcode打包出来的APP的安装包,通过解析ipa中的文件,我们可以获得APPDisplayNameVersionBundleIdentifier等信息,
同时也可以获取到APP证书的相关信息,包括APP安装环境证书的有效期APP开通的功能可安装设备的UDID公共秘钥指纹等。

解析 ipa 可用的工具

NPM 官网上搜索 app、ipa、package 或 parser 等信息,
可直接使用的插件有:app-info-parser,但是其不支持 TS,经过询问,作者并不明确何时可以适配 TS。
js-app-parser是支持 TS 的解析插件,但是解析 ipa 时会出错,解析 apk 时是正确的(解析非标准打包的 apk 会出错,解决见下一篇)。
综合分析以上插件源码,找到 ipa 解析失败问题并解决,创建一个支持 TSts-package-parser
js-app-parser解析出错的地方是解析info.plist的时候出错了。

分析 ipa

解析 ipa,实际需要解析的文件是 info.plistAppIcon
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公共秘钥指纹等具体信息。并提供下载统计等丰富功能。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值