243 lines
7.1 KiB
TypeScript
243 lines
7.1 KiB
TypeScript
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 =
|
|
'<?xml version="1.0" encoding="utf-8" standalone="no"?><manifest xmlns:android="http://schemas.android.com/apk/res/android" android:compileSdkVersion="34" package="com.rea.apktool.probe" platformBuildVersionCode="34">';
|
|
expect(parseManifestPackage(`${manifest}</manifest>`)).toBe(
|
|
"com.rea.apktool.probe",
|
|
);
|
|
});
|
|
|
|
it("reads package with reordered or single-quoted XML attributes", () => {
|
|
expect(
|
|
parseManifestPackage("<manifest debug='false' package='org.rea.app' />"),
|
|
).toBe("org.rea.app");
|
|
});
|
|
|
|
it("reports null when no package attribute exists", () => {
|
|
expect(parseManifestPackage("<manifest>\n</manifest>\n")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("parseDecodedStrings", () => {
|
|
it("projects entries from a real decoded strings.xml", () => {
|
|
const stringsXml = [
|
|
'<?xml version="1.0" encoding="utf-8"?>',
|
|
"<resources>",
|
|
' <string name="api_endpoint">https://api.example.com/v1</string>',
|
|
' <string name="app_name">ApktoolProbe</string>',
|
|
' <string name="debug_flag">true</string>',
|
|
"</resources>",
|
|
"",
|
|
].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 =
|
|
'<resources><string name="styled">Hello <b>world</b></string><string name="plain">ok</string></resources>';
|
|
expect(parseDecodedStrings(stringsXml)).toEqual({
|
|
entries: [{ name: "plain", value: "ok" }],
|
|
unparsedCount: 1,
|
|
});
|
|
});
|
|
|
|
it("preserves escaped entities verbatim", () => {
|
|
const stringsXml =
|
|
'<resources><string name="escaped">a & b <c></string></resources>';
|
|
expect(parseDecodedStrings(stringsXml).entries[0]?.value).toBe(
|
|
"a & b <c>",
|
|
);
|
|
});
|
|
|
|
it("keeps empty, self-closing, and reordered string attributes", () => {
|
|
expect(
|
|
parseDecodedStrings(
|
|
'<resources><string translatable="false" name="empty"/><string name="paired"></string></resources>',
|
|
),
|
|
).toEqual({
|
|
entries: [
|
|
{ name: "empty", value: "" },
|
|
{ name: "paired", value: "" },
|
|
],
|
|
unparsedCount: 0,
|
|
});
|
|
});
|
|
|
|
it("ignores comments without mistaking them for nested markup", () => {
|
|
expect(
|
|
parseDecodedStrings(
|
|
'<resources><!-- <string name="fake">bad</string> --><string name="real">before<!-- note -->after</string></resources>',
|
|
),
|
|
).toEqual({
|
|
entries: [{ name: "real", value: "beforeafter" }],
|
|
unparsedCount: 0,
|
|
});
|
|
});
|
|
|
|
it("does not include a trailing document comment in the last string", () => {
|
|
expect(
|
|
parseDecodedStrings(
|
|
'<resources><string name="a">one</string></resources><!-- </string> -->',
|
|
),
|
|
).toEqual({
|
|
entries: [{ name: "a", value: "one" }],
|
|
unparsedCount: 0,
|
|
});
|
|
});
|
|
|
|
it("preserves source whitespace and entity spelling", () => {
|
|
expect(
|
|
parseDecodedStrings(
|
|
'<resources><string name="raw"> a & b </string></resources>',
|
|
).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(
|
|
`<resources>${newline} <string name="raw">x & y</string>${newline}</resources>`,
|
|
),
|
|
).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("<resources><string>value</string></resources>"),
|
|
).toEqual({ entries: [], unparsedCount: 1 });
|
|
});
|
|
|
|
it("reports malformed XML and unsupported string markup as partial", () => {
|
|
expect(
|
|
parseDecodedStrings("<resources><string name='broken'>no</resources>"),
|
|
).toEqual({ entries: [], unparsedCount: 1 });
|
|
expect(
|
|
parseDecodedStrings(
|
|
'<resources><string name="styled">Hello <b>world</b></string></resources>',
|
|
),
|
|
).toEqual({ entries: [], unparsedCount: 1 });
|
|
});
|
|
});
|