AAB manifest: read aapt2 Resources.proto field numbers (element name 3, attribute 4, child 5); real AGP bundles failed with 'AAB manifest without package / versionCode'

This commit is contained in:
CRTkafa 2026-10-12 08:40:27 +08:00
parent 720cb2a13a
commit 78be1304ec
2 changed files with 33 additions and 8 deletions

View file

@ -123,16 +123,17 @@ function fields(b: Buffer): Field[] | null {
/** Attributes (name -> raw value) of every element in an aapt2 proto XML, element name first. */
function elements(b: Buffer, out: { name: string; attrs: Record<string, string> }[] = [], depth = 0): { name: string; attrs: Record<string, string> }[] {
// XmlNode { XmlElement element = 1; string text = 2; ... } XmlElement { ns 1; string name = 4; XmlAttribute attribute = 5; XmlNode child = 6 }
// aapt2 Resources.proto: XmlNode { XmlElement element = 1; string text = 2; SourcePosition source = 3 }
// XmlElement { XmlNamespace namespace_declaration = 1; string namespace_uri = 2; string name = 3; XmlAttribute attribute = 4; XmlNode child = 5 }
// XmlAttribute { string namespace_uri = 1; string name = 2; string value = 3; ...; Item compiled_item = 6 }
if (depth > 64) return out;
const node = fields(b);
const el = node?.find(f => f.no === 1 && f.wire === 2);
if (!el) return out;
const ef = fields(el.v as Buffer) ?? [];
const name = (ef.find(f => f.no === 4)?.v as Buffer | undefined)?.toString('utf8') ?? '';
const name = (ef.find(f => f.no === 3 && f.wire === 2)?.v as Buffer | undefined)?.toString('utf8') ?? '';
const attrs: Record<string, string> = {};
for (const a of ef.filter(f => f.no === 5 && f.wire === 2)) {
for (const a of ef.filter(f => f.no === 4 && f.wire === 2)) {
const af = fields(a.v as Buffer) ?? [];
const an = (af.find(f => f.no === 2)?.v as Buffer | undefined)?.toString('utf8');
let av = (af.find(f => f.no === 3)?.v as Buffer | undefined)?.toString('utf8') ?? '';
@ -147,7 +148,7 @@ function elements(b: Buffer, out: { name: string; attrs: Record<string, string>
if (an) attrs[an] = av;
}
out.push({ name, attrs });
for (const c of ef.filter(f => f.no === 6 && f.wire === 2)) elements(c.v as Buffer, out, depth + 1);
for (const c of ef.filter(f => f.no === 5 && f.wire === 2)) elements(c.v as Buffer, out, depth + 1);
return out;
}
@ -165,16 +166,24 @@ export function aabInfo(aab: string | Buffer): AabInfo {
minSdk: sdk?.minSdkVersion ? Number(sdk.minSdkVersion) : undefined, targetSdk: sdk?.targetSdkVersion ? Number(sdk.targetSdkVersion) : undefined };
}
/** Builds an aapt2-style proto manifest (tests and fixtures). */
/** Builds an aapt2 proto manifest (Resources.proto field numbers, as in a real AGP bundle: namespace declaration,
* then name, attributes, children; versionCode as a compiled int like aapt2 writes it). Tests and fixtures. */
const ANDROID_NS = 'http://schemas.android.com/apk/res/android';
export function encodeProtoManifest(els: { name: string; attrs: Record<string, string> }[]): Buffer {
const vint = (n: number): Buffer => { const o: number[] = []; do { let x = n % 128; n = Math.floor(n / 128); if (n) x |= 128; o.push(x); } while (n); return Buffer.from(o); };
const ld = (no: number, b: Buffer): Buffer => Buffer.concat([vint(no * 8 + 2), vint(b.length), b]);
const s = (no: number, t: string) => ld(no, Buffer.from(t, 'utf8'));
const element = (i: number): Buffer => {
const e = els[i]!;
const attrs = Object.entries(e.attrs).map(([k, v]) => ld(5, Buffer.concat([s(1, 'http://schemas.android.com/apk/res/android'), s(2, k), s(3, v)])));
const child = i + 1 < els.length ? [ld(6, ld(1, element(i + 1)))] : [];
return Buffer.concat([s(4, e.name), ...attrs, ...child]);
const ns = i === 0 ? [ld(1, Buffer.concat([s(1, 'android'), s(2, ANDROID_NS)]))] : [];
const attrs = Object.entries(e.attrs).map(([k, v]) => {
const plain = k === 'package' || k === 'versionName';
// compiled_item = 6 { Primitive prim = 7 { int_decimal_value = 6 } } for numbers, like aapt2
const item = /^\d+$/.test(v) && !plain ? [ld(6, ld(7, Buffer.concat([vint(6 * 8), vint(Number(v))])))] : [];
return ld(4, Buffer.concat([plain ? Buffer.alloc(0) : s(1, ANDROID_NS), s(2, k), s(3, v), ...item]));
});
const child = i + 1 < els.length ? [ld(5, ld(1, element(i + 1)))] : [];
return Buffer.concat([...ns, s(3, e.name), ...attrs, ...child]);
};
return ld(1, element(0));
}

View file

@ -71,6 +71,22 @@ describe('zip + IPA + AAB', () => {
expect(ipaInfo(f).build).toBe('42');
expect(() => ipaInfo(makeZip([{ name: 'x', data: 'y' }]))).toThrow('not an iOS app archive');
});
test('aabInfo reads the manifest as AGP writes it (Resources.proto: element name = 3, attribute = 4, child = 5)', () => {
// a real Capacitor 8 / AGP bundle failed with "AAB manifest without package / versionCode": the parser read the
// element fields one number too high. These bytes are written by tag, independent of encodeProtoManifest.
const len = (n: number) => Buffer.from(n < 128 ? [n] : [(n & 127) | 128, n >> 7]);
const ld = (tag: number, b: Buffer) => Buffer.concat([Buffer.from([tag]), len(b.length), b]);
const str = (tag: number, t: string) => ld(tag, Buffer.from(t));
const NS = 'http://schemas.android.com/apk/res/android';
const attr = (name: string, value: string, int?: number) => ld(0x22, Buffer.concat([name === 'package' ? Buffer.alloc(0) : str(0x0a, NS), str(0x12, name), str(0x1a, value),
...(int === undefined ? [] : [ld(0x32, ld(0x3a, Buffer.from([0x30, int])))])]));
const usesSdk = ld(0x0a, Buffer.concat([str(0x1a, 'uses-sdk'), attr('minSdkVersion', '24', 24)]));
const manifest = ld(0x0a, Buffer.concat([ld(0x0a, Buffer.concat([str(0x0a, 'android'), str(0x12, NS)])), str(0x1a, 'manifest'),
attr('versionCode', '7', 7), attr('versionName', '1.0.0'), attr('package', 'com.example.game'), ld(0x2a, usesSdk)]));
const aab = makeZip([{ name: 'base/manifest/AndroidManifest.xml', data: manifest }]);
expect(aabInfo(aab)).toEqual({ packageName: 'com.example.game', versionCode: 7, versionName: '1.0.0', minSdk: 24, targetSdk: undefined });
});
test('aabInfo reads the proto manifest', () => {
expect(aabInfo(makeAab('com.example.game', 1234, '2.0.1'))).toEqual({ packageName: 'com.example.game', versionCode: 1234, versionName: '2.0.1', minSdk: 24, targetSdk: 35 });
expect(() => aabInfo(makeZip([{ name: 'x', data: 'y' }]))).toThrow('not an Android App Bundle');