Skip to content

Commit 5ec02cb

Browse files
sunnylqmclaude
andcommitted
perf: read package info entries in a single zip scan
- zip.ts: add getEntriesFromHarmonyApp to read multiple entries (and do the nested .hap extraction) in one pass; getEntryFromHarmonyApp now delegates to it - utils/index.ts: getApkInfo (2 scans -> 1), getIpaInfo (4 -> 1) and getAppInfo (3 scans + 3 nested .hap extractions -> 1) batch their entry lookups instead of enumerating the archive once per entry - tests: cover batch harmony reads and case-sensitive regex entry match Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 05d451a commit 5ec02cb

3 files changed

Lines changed: 88 additions & 33 deletions

File tree

‎src/utils/app-info-parser/zip.ts‎

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,33 @@ export class Zip {
3737
async getEntryFromHarmonyApp(
3838
regex: RegExp,
3939
): Promise<Buffer | Blob | undefined> {
40+
return (await this.getEntriesFromHarmonyApp([regex]))[0];
41+
}
42+
43+
/**
44+
* Read multiple entries in a single pass over the archive (including
45+
* nested .hap extraction), instead of one full scan per entry.
46+
* Returns buffers in the same order as the given regexps.
47+
*/
48+
async getEntriesFromHarmonyApp(
49+
regexps: RegExp[],
50+
): Promise<(Buffer | undefined)[]> {
51+
const results: (Buffer | undefined)[] = new Array(regexps.length);
4052
try {
41-
let originSource: Buffer | Blob | undefined;
4253
if (typeof this.file !== 'string') {
4354
throw new Error('Param error: [file] must be file path in Node.');
4455
}
4556
await enumZipEntries(this.file, async (entry, zipFile) => {
46-
if (regex.test(entry.fileName)) {
47-
originSource = await readEntry(entry, zipFile);
57+
for (let i = 0; i < regexps.length; i++) {
58+
if (regexps[i].test(entry.fileName)) {
59+
results[i] = await readEntry(entry, zipFile);
60+
}
4861
}
4962
});
50-
return originSource;
5163
} catch (error) {
52-
console.error('Error in getEntryFromHarmonyApp:', error);
64+
console.error('Error in getEntriesFromHarmonyApp:', error);
5365
}
66+
return results;
5467
}
5568

5669
private async readEntries(

‎src/utils/index.ts‎

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,17 @@ export function translateOptions<T extends Record<string, unknown>>(
139139
return result as T;
140140
}
141141

142+
const ApkBundleFileName = /assets\/index.android.bundle/;
143+
const ApkUpdateJsonName = /res\/raw\/update.json/;
144+
142145
export async function getApkInfo(fn: string) {
143146
const appInfoParser = new AppInfoParser(fn);
144-
const bundleFile = await appInfoParser.parser.getEntry(
145-
/assets\/index.android.bundle/,
146-
);
147+
// read both entries in a single scan over the archive
148+
const entries = await appInfoParser.parser.getEntries([
149+
ApkBundleFileName,
150+
ApkUpdateJsonName,
151+
]);
152+
const bundleFile = entries[String(ApkBundleFileName)];
147153
if (!bundleFile) {
148154
throw new Error(
149155
t('bundleNotFound', {
@@ -152,9 +158,7 @@ export async function getApkInfo(fn: string) {
152158
}),
153159
);
154160
}
155-
const updateJsonFile = await appInfoParser.parser.getEntry(
156-
/res\/raw\/update.json/,
157-
);
161+
const updateJsonFile = entries[String(ApkUpdateJsonName)];
158162
let appCredential = {};
159163
if (updateJsonFile) {
160164
appCredential = JSON.parse(updateJsonFile.toString()).android;
@@ -182,9 +186,13 @@ export async function getApkInfo(fn: string) {
182186

183187
export async function getAppInfo(fn: string) {
184188
const appInfoParser = new AppInfoParser(fn);
185-
const bundleFile = await appInfoParser.parser.getEntryFromHarmonyApp(
186-
/rawfile\/bundle.harmony.js/,
187-
);
189+
// single scan (and single nested .hap extraction) for all three entries
190+
const [bundleFile, updateJsonFile, metaJsonFile] =
191+
await appInfoParser.parser.getEntriesFromHarmonyApp([
192+
/rawfile\/bundle.harmony.js/,
193+
/rawfile\/update.json/,
194+
/rawfile\/meta.json/,
195+
]);
188196
if (!bundleFile) {
189197
throw new Error(
190198
t('bundleNotFound', {
@@ -193,14 +201,10 @@ export async function getAppInfo(fn: string) {
193201
}),
194202
);
195203
}
196-
const updateJsonFile =
197-
await appInfoParser.parser.getEntryFromHarmonyApp(/rawfile\/update.json/);
198204
let appCredential = {};
199205
if (updateJsonFile) {
200206
appCredential = JSON.parse(updateJsonFile.toString()).harmony;
201207
}
202-
const metaJsonFile =
203-
await appInfoParser.parser.getEntryFromHarmonyApp(/rawfile\/meta.json/);
204208
let metaData: ParsedAppMetaInfo = {};
205209
if (metaJsonFile) {
206210
metaData = JSON.parse(metaJsonFile.toString()) as ParsedAppMetaInfo;
@@ -216,11 +220,23 @@ export async function getAppInfo(fn: string) {
216220
return { versionName, buildTime, ...appCredential };
217221
}
218222

223+
const IpaBundleFileName = /payload\/.+?\.app\/main.jsbundle/;
224+
const IpaUpdateJsonName = /payload\/.+?\.app\/assets\/update.json/;
225+
const IpaBuildTimeName = /payload\/.+?\.app\/pushy_build_time.txt/;
226+
// Not in root bundle when use `use_frameworks`
227+
const IpaBuildTimeFrameworkName =
228+
/payload\/.+?\.app\/frameworks\/react_native_update.framework\/pushy_build_time.txt/;
229+
219230
export async function getIpaInfo(fn: string) {
220231
const appInfoParser = new AppInfoParser(fn);
221-
const bundleFile = await appInfoParser.parser.getEntry(
222-
/payload\/.+?\.app\/main.jsbundle/,
223-
);
232+
// read all four entries in a single scan over the archive
233+
const entries = await appInfoParser.parser.getEntries([
234+
IpaBundleFileName,
235+
IpaUpdateJsonName,
236+
IpaBuildTimeName,
237+
IpaBuildTimeFrameworkName,
238+
]);
239+
const bundleFile = entries[String(IpaBundleFileName)];
224240
if (!bundleFile) {
225241
throw new Error(
226242
t('bundleNotFound', {
@@ -229,24 +245,16 @@ export async function getIpaInfo(fn: string) {
229245
}),
230246
);
231247
}
232-
const updateJsonFile = await appInfoParser.parser.getEntry(
233-
/payload\/.+?\.app\/assets\/update.json/,
234-
);
248+
const updateJsonFile = entries[String(IpaUpdateJsonName)];
235249
let appCredential = {};
236250
if (updateJsonFile) {
237251
appCredential = JSON.parse(updateJsonFile.toString()).ios;
238252
}
239253
const { CFBundleShortVersionString: versionName } =
240254
await appInfoParser.parse<ParsedIpaInfo>();
241-
let buildTimeTxtBuffer = await appInfoParser.parser.getEntry(
242-
/payload\/.+?\.app\/pushy_build_time.txt/,
243-
);
244-
if (!buildTimeTxtBuffer) {
245-
// Not in root bundle when use `use_frameworks`
246-
buildTimeTxtBuffer = await appInfoParser.parser.getEntry(
247-
/payload\/.+?\.app\/frameworks\/react_native_update.framework\/pushy_build_time.txt/,
248-
);
249-
}
255+
const buildTimeTxtBuffer =
256+
entries[String(IpaBuildTimeName)] ??
257+
entries[String(IpaBuildTimeFrameworkName)];
250258
if (!buildTimeTxtBuffer) {
251259
throw new Error(t('buildTimeNotFound'));
252260
}

‎tests/app-info-zip.test.ts‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,40 @@ suffix`,
145145
expect((buf as Buffer).toString()).toBe('{"harmony": true}');
146146
});
147147

148+
test('getEntriesFromHarmonyApp reads multiple entries in one pass', async () => {
149+
const zipPath = path.join(tempRoot, 'app.zip');
150+
await writeZip(zipPath, {
151+
'rawfile/bundle.harmony.js': 'bundle',
152+
'rawfile/update.json': '{"harmony":{}}',
153+
'other.txt': 'ignore',
154+
});
155+
156+
const zip = new Zip(zipPath);
157+
158+
const [bundle, updateJson, missing] = await zip.getEntriesFromHarmonyApp([
159+
/rawfile\/bundle.harmony.js/,
160+
/rawfile\/update.json/,
161+
/rawfile\/meta.json/,
162+
]);
163+
expect((bundle as Buffer).toString()).toBe('bundle');
164+
expect((updateJson as Buffer).toString()).toBe('{"harmony":{}}');
165+
expect(missing).toBeUndefined();
166+
});
167+
168+
test('getEntry matches case-sensitive regex against original entry name', async () => {
169+
const zipPath = path.join(tempRoot, 'app.zip');
170+
await writeZip(zipPath, {
171+
'base/manifest/AndroidManifest.xml': 'aab manifest',
172+
});
173+
174+
// regex with uppercase letters used to never match because only the
175+
// lowercased entry name was tested
176+
const buf = await new Zip(zipPath).getEntry(
177+
/^base\/manifest\/AndroidManifest\.xml$/,
178+
);
179+
expect((buf as Buffer).toString()).toBe('aab manifest');
180+
});
181+
148182
test('constructor resolves path for string input', () => {
149183
const relPath = 'some/relative/path.zip';
150184
const zip = new Zip(relPath);

0 commit comments

Comments
 (0)