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:
parent
720cb2a13a
commit
78be1304ec
2 changed files with 33 additions and 8 deletions
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue