Skip to content

Commit 6e0f50e

Browse files
committed
Refactor app-info-parser: migrate utility functions to TypeScript, remove deprecated utils.js, and enhance AAB and APK parsing with improved error handling and resource mapping.
1 parent e492c5e commit 6e0f50e

File tree

12 files changed

+947
-887
lines changed

12 files changed

+947
-887
lines changed

src/utils/app-info-parser/aab.ts

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import fs from 'fs-extra';
2-
import path from 'path';
1+
import { spawn } from 'child_process';
32
import os from 'os';
3+
import path from 'path';
4+
import fs from 'fs-extra';
45
import { open as openZipFile } from 'yauzl';
5-
import { Zip } from './zip';
66
import { t } from '../i18n';
7+
import { ResourceFinder } from './resource-finder';
8+
import { mapInfoResource } from './utils';
9+
import { ManifestParser } from './xml-parser/manifest';
10+
import { Zip } from './zip';
711

812
export class AabParser extends Zip {
913
file: string | File;
@@ -16,20 +20,40 @@ export class AabParser extends Zip {
1620
async extractApk(
1721
outputPath: string,
1822
{
19-
includeAllSplits = true,
23+
includeAllSplits,
2024
splits,
2125
}: { includeAllSplits?: boolean; splits?: string[] | null },
2226
) {
23-
const { exec } = require('child_process');
24-
const util = require('util');
25-
const execAsync = util.promisify(exec);
2627
const normalizedSplits = Array.isArray(splits)
2728
? splits.map((item) => item.trim()).filter(Boolean)
2829
: [];
2930
const modules = includeAllSplits
3031
? null
3132
: Array.from(new Set(['base', ...normalizedSplits]));
32-
const modulesArg = modules ? ` --modules="${modules.join(',')}"` : '';
33+
const modulesArgs = modules ? [`--modules=${modules.join(',')}`] : [];
34+
35+
const runCommand = (command: string, args: string[]) =>
36+
new Promise<void>((resolve, reject) => {
37+
const child = spawn(command, args, {
38+
stdio: ['ignore', 'pipe', 'pipe'],
39+
});
40+
let stderr = '';
41+
child.stderr?.on('data', (chunk) => {
42+
stderr += chunk.toString();
43+
});
44+
child.on('error', reject);
45+
child.on('close', (code) => {
46+
if (code === 0) {
47+
resolve();
48+
return;
49+
}
50+
reject(
51+
new Error(
52+
stderr.trim() || `Command failed: ${command} (code ${code})`,
53+
),
54+
);
55+
});
56+
});
3357

3458
// Create a temp file for the .apks output
3559
const tempDir = os.tmpdir();
@@ -41,14 +65,28 @@ export class AabParser extends Zip {
4165
// User might need keystore to sign it properly but for simple extraction we stick to default debug key if possible or unsigned?
4266
// actually bundletool build-apks signs with debug key by default if no keystore provided.
4367

44-
let cmd = `bundletool build-apks --mode=universal --bundle="${this.file}" --output="${tempApksPath}" --overwrite${modulesArg}`;
4568
try {
46-
await execAsync(cmd);
69+
await runCommand('bundletool', [
70+
'build-apks',
71+
'--mode=universal',
72+
`--bundle=${this.file}`,
73+
`--output=${tempApksPath}`,
74+
'--overwrite',
75+
...modulesArgs,
76+
]);
4777
} catch (e) {
4878
// Fallback to npx node-bundletool if bundletool is not in PATH
4979
// We use -y to avoid interactive prompt for installation
50-
cmd = `npx -y node-bundletool build-apks --mode=universal --bundle="${this.file}" --output="${tempApksPath}" --overwrite${modulesArg}`;
51-
await execAsync(cmd);
80+
await runCommand('npx', [
81+
'-y',
82+
'node-bundletool',
83+
'build-apks',
84+
'--mode=universal',
85+
`--bundle=${this.file}`,
86+
`--output=${tempApksPath}`,
87+
'--overwrite',
88+
...modulesArgs,
89+
]);
5290
}
5391

5492
// 2. Extract universal.apk from the .apks (zip) file
@@ -119,7 +157,6 @@ export class AabParser extends Zip {
119157
const resourceBuffer = await this.getEntry(ResourceName);
120158
if (resourceBuffer) {
121159
const resourceMap = this._parseResourceMap(resourceBuffer as Buffer);
122-
const { mapInfoResource } = require('./utils');
123160
apkInfo = mapInfoResource(apkInfo, resourceMap);
124161
}
125162
} catch (e: any) {
@@ -137,8 +174,7 @@ export class AabParser extends Zip {
137174
*/
138175
private _parseManifest(buffer: Buffer) {
139176
try {
140-
const ManifestXmlParser = require('./xml-parser/manifest');
141-
const parser = new ManifestXmlParser(buffer, {
177+
const parser = new ManifestParser(buffer, {
142178
ignore: [
143179
'application.activity',
144180
'application.service',
@@ -159,7 +195,6 @@ export class AabParser extends Zip {
159195
*/
160196
private _parseResourceMap(buffer: Buffer) {
161197
try {
162-
const ResourceFinder = require('./resource-finder');
163198
return new ResourceFinder().processResourceTable(buffer);
164199
} catch (e: any) {
165200
throw new Error(t('aabParseResourcesError', { error: e?.message ?? e }));

src/utils/app-info-parser/apk.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1+
import { ResourceFinder } from './resource-finder';
12
import { findApkIconPath, getBase64FromBuffer, mapInfoResource } from './utils';
3+
import { ManifestParser } from './xml-parser/manifest';
24
import { Zip } from './zip';
35

46
const ManifestName = /^androidmanifest\.xml$/;
57
const ResourceName = /^resources\.arsc$/;
68

7-
const ManifestXmlParser = require('./xml-parser/manifest');
8-
const ResourceFinder = require('./resource-finder');
9-
109
export class ApkParser extends Zip {
1110
parse(): Promise<any> {
1211
return new Promise((resolve, reject) => {
@@ -61,7 +60,7 @@ export class ApkParser extends Zip {
6160
*/
6261
private _parseManifest(buffer: Buffer) {
6362
try {
64-
const parser = new ManifestXmlParser(buffer, {
63+
const parser = new ManifestParser(buffer, {
6564
ignore: [
6665
'application.activity',
6766
'application.service',

src/utils/app-info-parser/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1+
import { AabParser } from './aab';
12
import { ApkParser } from './apk';
2-
const IpaParser = require('./ipa');
33
import { AppParser } from './app';
4-
import { AabParser } from './aab';
4+
import { IpaParser } from './ipa';
55
const supportFileTypes = ['ipa', 'apk', 'app', 'aab'];
66

77
class AppInfoParser {
Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,37 @@ const parsePlist = require('plist').parse;
22
const parseBplist = require('bplist-parser').parseBuffer;
33
const cgbiToPng = require('cgbi-to-png');
44

5+
import { findIpaIconPath, getBase64FromBuffer, isBrowser } from './utils';
56
import { Zip } from './zip';
6-
const { findIpaIconPath, getBase64FromBuffer, isBrowser } = require('./utils');
77

88
const PlistName = /payload\/[^\/]+?.app\/info.plist$/i;
99
const ProvisionName = /payload\/.+?\.app\/embedded.mobileprovision/;
1010

11-
class IpaParser extends Zip {
12-
parse() {
11+
export class IpaParser extends Zip {
12+
parse(): Promise<any> {
1313
return new Promise((resolve, reject) => {
1414
this.getEntries([PlistName, ProvisionName])
15-
.then((buffers) => {
16-
if (!buffers[PlistName]) {
15+
.then((buffers: any) => {
16+
if (!buffers[PlistName as any]) {
1717
throw new Error("Info.plist can't be found.");
1818
}
19-
const plistInfo = this._parsePlist(buffers[PlistName]);
20-
// parse mobile provision
21-
const provisionInfo = this._parseProvision(buffers[ProvisionName]);
19+
const plistInfo = this._parsePlist(buffers[PlistName as any]);
20+
const provisionInfo = this._parseProvision(
21+
buffers[ProvisionName as any],
22+
);
2223
plistInfo.mobileProvision = provisionInfo;
2324

24-
// find icon path and parse icon
2525
const iconRegex = new RegExp(
2626
findIpaIconPath(plistInfo).toLowerCase(),
2727
);
2828
this.getEntry(iconRegex)
29-
.then((iconBuffer) => {
29+
.then((iconBuffer: any) => {
3030
try {
31-
// In general, the ipa file's icon has been specially processed, should be converted
3231
plistInfo.icon = iconBuffer
3332
? getBase64FromBuffer(cgbiToPng.revert(iconBuffer))
3433
: null;
3534
} catch (err) {
3635
if (isBrowser()) {
37-
// Normal conversion in other cases
3836
plistInfo.icon = iconBuffer
3937
? getBase64FromBuffer(
4038
window.btoa(String.fromCharCode(...iconBuffer)),
@@ -47,11 +45,11 @@ class IpaParser extends Zip {
4745
}
4846
resolve(plistInfo);
4947
})
50-
.catch((e) => {
48+
.catch((e: any) => {
5149
reject(e);
5250
});
5351
})
54-
.catch((e) => {
52+
.catch((e: any) => {
5553
reject(e);
5654
});
5755
});
@@ -60,8 +58,8 @@ class IpaParser extends Zip {
6058
* Parse plist
6159
* @param {Buffer} buffer // plist file's buffer
6260
*/
63-
_parsePlist(buffer) {
64-
let result;
61+
private _parsePlist(buffer: Buffer) {
62+
let result: any;
6563
const bufferType = buffer[0];
6664
if (bufferType === 60 || bufferType === '<' || bufferType === 239) {
6765
result = parsePlist(buffer.toString());
@@ -76,8 +74,8 @@ class IpaParser extends Zip {
7674
* parse provision
7775
* @param {Buffer} buffer // provision file's buffer
7876
*/
79-
_parseProvision(buffer) {
80-
let info = {};
77+
private _parseProvision(buffer: Buffer | undefined) {
78+
let info: Record<string, any> = {};
8179
if (buffer) {
8280
let content = buffer.toString('utf-8');
8381
const firstIndex = content.indexOf('<?xml');
@@ -90,5 +88,3 @@ class IpaParser extends Zip {
9088
return info;
9189
}
9290
}
93-
94-
module.exports = IpaParser;

0 commit comments

Comments
 (0)