fix(admin-service): 改用 yauzl.fromBuffer 解析 APK,支持 V2/V3 签名格式

yauzl.open() 在 APK V2/V3 签名包上报 'end of central directory record
signature not found',因为签名块会挤移 EOCD 位置。改为直接调用
yauzl.fromBuffer() + adbkit-apkreader 内部 BinaryXmlParser/ManifestParser
直接从内存解析 AndroidManifest.xml,兼容所有签名格式,且无需临时文件。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-03-06 09:18:56 -08:00
parent e00cbb71c5
commit 276eda2a84
1 changed files with 40 additions and 12 deletions

View File

@ -1,6 +1,3 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { Injectable, Logger, BadRequestException } from '@nestjs/common'; import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { IPackageParser, ParsedPackageInfo } from '../../domain/ports/package-parser.interface'; import { IPackageParser, ParsedPackageInfo } from '../../domain/ports/package-parser.interface';
@ -16,14 +13,47 @@ export class PackageParserService implements IPackageParser {
} }
private async parseApk(buffer: Buffer): Promise<ParsedPackageInfo> { private async parseApk(buffer: Buffer): Promise<ParsedPackageInfo> {
// adbkit-apkreader v3 底层用 yauzl.open(),只接受文件路径而非 Buffer // yauzl.open() 不支持 APK V2/V3 签名格式EOCD 被签名块挤移),
// 先写入临时文件,解析后删除 // 改用 yauzl.fromBuffer() + adbkit-apkreader 内部 ManifestParser 直接解析
const tmpFile = path.join(os.tmpdir(), `apk-parse-${Date.now()}.apk`);
try { try {
fs.writeFileSync(tmpFile, buffer); const yauzl = await import('yauzl');
const ApkReader = await import('adbkit-apkreader').then(m => m.default || m); const { ManifestParser } = await import('adbkit-apkreader/lib/apkreader/parser/manifest' as any).then(
const reader = await ApkReader.open(tmpFile); (m: any) => ({ ManifestParser: m.default || m }),
const manifest = await reader.readManifest(); );
const { BinaryXmlParser } = await import('adbkit-apkreader/lib/apkreader/parser/binaryxml' as any).then(
(m: any) => ({ BinaryXmlParser: m.default || m }),
);
const zipfile = await new Promise<any>((resolve, reject) =>
yauzl.fromBuffer(buffer, { lazyEntries: true }, (err: any, zf: any) =>
err ? reject(err) : resolve(zf),
),
);
const manifestBuffer = await new Promise<Buffer>((resolve, reject) => {
zipfile.readEntry();
zipfile.on('entry', (entry: any) => {
if (entry.fileName === 'AndroidManifest.xml') {
zipfile.openReadStream(entry, (err: any, stream: any) => {
if (err) return reject(err);
const chunks: Buffer[] = [];
stream.on('data', (c: Buffer) => chunks.push(c));
stream.on('end', () => resolve(Buffer.concat(chunks)));
stream.on('error', reject);
});
} else {
zipfile.readEntry();
}
});
zipfile.on('end', () => reject(new Error('AndroidManifest.xml not found')));
zipfile.on('error', reject);
});
const binaryXml = new BinaryXmlParser(manifestBuffer);
const parsed = binaryXml.parse();
const manifestParser = new ManifestParser(parsed);
const manifest = manifestParser.parse();
return { return {
packageName: manifest.package || 'unknown', packageName: manifest.package || 'unknown',
versionCode: manifest.versionCode || 0, versionCode: manifest.versionCode || 0,
@ -39,8 +69,6 @@ export class PackageParserService implements IPackageParser {
versionName: '0.0.0', versionName: '0.0.0',
platform: 'ANDROID', platform: 'ANDROID',
}; };
} finally {
try { fs.unlinkSync(tmpFile); } catch { /* ignore */ }
} }
} }