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 });
});
});