From 78be1304ecbe11b10791a97b2161aca8eb778127 Mon Sep 17 00:00:00 2001 From: CRT Date: Mon, 12 Oct 2026 08:40:27 +0800 Subject: [PATCH] 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' --- cli/lib/gradle.ts | 25 +++++++++++++++++-------- tests/build-sign.test.ts | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/cli/lib/gradle.ts b/cli/lib/gradle.ts index bfc42d9..04a85c0 100644 --- a/cli/lib/gradle.ts +++ b/cli/lib/gradle.ts @@ -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 }[] = [], depth = 0): { name: string; attrs: Record }[] { - // 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 = {}; - 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 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 }[]): 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)); } diff --git a/tests/build-sign.test.ts b/tests/build-sign.test.ts index 4557084..99d392c 100644 --- a/tests/build-sign.test.ts +++ b/tests/build-sign.test.ts @@ -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');