import { describe, expect, it } from "vitest"; import { parseApktoolYml, parseDecodedStrings, parseManifestPackage, } from "./ApktoolDecodeOutput.js"; const REAL_YML = [ "!!brut.androlib.meta.MetaInfo", "apkFileName: probe.apk", "compressionType: false", "doNotCompress:", "- resources.arsc", "isFrameworkApk: false", "packageInfo:", " forcedPackageId: '127'", " renameManifestPackage: null", "sdkInfo:", " minSdkVersion: '24'", " targetSdkVersion: '34'", "sharedLibrary: false", "sparseResources: false", "unknownFiles: {}", "usesFramework:", " ids:", " - 1", " tag: null", "versionInfo:", " versionCode: '7'", " versionName: 1.2.3", "", ].join("\n"); describe("parseApktoolYml", () => { it("projects version and SDK facts from a real apktool.yml", () => { expect(parseApktoolYml(REAL_YML)).toEqual({ versionName: "1.2.3", versionCode: "7", minSdkVersion: "24", targetSdkVersion: "34", }); }); it("accepts quoted version values some builds emit", () => { expect( parseApktoolYml( '!!brut.androlib.meta.MetaInfo\nversionInfo:\n versionName: "2.0.0"\n versionCode: 45\n', ), ).toEqual({ versionName: "2.0.0", versionCode: "45", minSdkVersion: null, targetSdkVersion: null, }); }); it("reads only the producer's nested fields and preserves scalar spelling", () => { expect( parseApktoolYml( [ "!!brut.androlib.meta.MetaInfo", "versionName: decoy", "versionInfo:", " versionName: 01.2.3", " versionCode: 045", " custom:", " versionName: unrelated", "sdkInfo:", " minSdkVersion: 24", " targetSdkVersion: '35'", "", ].join("\n"), ), ).toEqual({ versionName: "01.2.3", versionCode: "045", minSdkVersion: "24", targetSdkVersion: "35", }); }); it("distinguishes YAML nulls from quoted null strings in flow mappings", () => { expect( parseApktoolYml( [ "!!brut.androlib.meta.MetaInfo", 'versionInfo: { versionName: "null", versionCode: 0045, nested: { versionName: decoy } }', "sdkInfo: { minSdkVersion: null, targetSdkVersion: ~ }", "", ].join("\n"), ), ).toEqual({ versionName: "null", versionCode: "0045", minSdkVersion: null, targetSdkVersion: null, }); }); it("reports null facts for unrelated YAML", () => { expect(parseApktoolYml("version: 2.7.0\n")).toEqual({ versionName: null, versionCode: null, minSdkVersion: null, targetSdkVersion: null, }); }); }); describe("parseManifestPackage", () => { it("reads the package attribute from a decoded manifest", () => { const manifest = ''; expect(parseManifestPackage(`${manifest}`)).toBe( "com.rea.apktool.probe", ); }); it("reads package with reordered or single-quoted XML attributes", () => { expect( parseManifestPackage(""), ).toBe("org.rea.app"); }); it("reports null when no package attribute exists", () => { expect(parseManifestPackage("\n\n")).toBeNull(); }); }); describe("parseDecodedStrings", () => { it("projects entries from a real decoded strings.xml", () => { const stringsXml = [ '', "", ' https://api.example.com/v1', ' ApktoolProbe', ' true', "", "", ].join("\n"); expect(parseDecodedStrings(stringsXml)).toEqual({ entries: [ { name: "api_endpoint", value: "https://api.example.com/v1" }, { name: "app_name", value: "ApktoolProbe" }, { name: "debug_flag", value: "true" }, ], unparsedCount: 0, }); }); it("counts entries with nested markup instead of flattening them", () => { const stringsXml = 'Hello worldok'; expect(parseDecodedStrings(stringsXml)).toEqual({ entries: [{ name: "plain", value: "ok" }], unparsedCount: 1, }); }); it("preserves escaped entities verbatim", () => { const stringsXml = 'a & b <c>'; expect(parseDecodedStrings(stringsXml).entries[0]?.value).toBe( "a & b <c>", ); }); it("keeps empty, self-closing, and reordered string attributes", () => { expect( parseDecodedStrings( '', ), ).toEqual({ entries: [ { name: "empty", value: "" }, { name: "paired", value: "" }, ], unparsedCount: 0, }); }); it("ignores comments without mistaking them for nested markup", () => { expect( parseDecodedStrings( 'beforeafter', ), ).toEqual({ entries: [{ name: "real", value: "beforeafter" }], unparsedCount: 0, }); }); it("does not include a trailing document comment in the last string", () => { expect( parseDecodedStrings( 'one', ), ).toEqual({ entries: [{ name: "a", value: "one" }], unparsedCount: 0, }); }); it("preserves source whitespace and entity spelling", () => { expect( parseDecodedStrings( ' a & b ', ).entries, ).toEqual([{ name: "raw", value: " a & b " }]); }); it("keeps source slices aligned across CRLF and CR line endings", () => { for (const newline of ["\r\n", "\r"]) { expect( parseDecodedStrings( `${newline} x & y${newline}`, ), ).toEqual({ entries: [{ name: "raw", value: "x & y" }], unparsedCount: 0, }); } }); it("counts a string with no name as unsupported instead of emitting an empty name", () => { expect( parseDecodedStrings("value"), ).toEqual({ entries: [], unparsedCount: 1 }); }); it("reports malformed XML and unsupported string markup as partial", () => { expect( parseDecodedStrings("no"), ).toEqual({ entries: [], unparsedCount: 1 }); expect( parseDecodedStrings( 'Hello world', ), ).toEqual({ entries: [], unparsedCount: 1 }); }); });