fix(apktool): read Apktool 2.9+ metadata in its line-oriented dialect (#7747)

Apktool 2.9.0 replaced SnakeYAML with the YamlWriter that 3.x still uses:
apktool.yml starts with an untagged `version: 2.x.y` line and strings are
written unquoted with Java-style escapes. Only a `version: 3.x.y` header
selected the producer dialect, so 2.9.0-2.12.x output went through YAML
core parsing: a versionName of `1.0 #beta` became `1.0`, escaped quotes kept
their backslashes, an empty name became null, and `1.0: beta` made the whole
decode fail as invalid metadata.

Select the line-oriented reader for 2.9.0 and later as well. Older tagged
SnakeYAML documents keep YAML scalar semantics.
This commit is contained in:
Dragolone authored and GitHub committed 2026-10-28 19:46:11 +08:00
1 parent fea4f2e397
commit 33698e6b31
3 files changed
+65 -12

No files matched your search

+6 -5
View File
@@ -100,12 +100,13 @@ still suppresses all string projection, including in this mode.
- **Metadata comes from apktool.yml as apktool printed it.** Version codes
keep their own spelling (`'7'` versus `7`); `package_name` is read from
the decoded manifest, not inferred.
Apktool 3's emitted `version: 3.x.y` header selects its single-line string
dialect: literal hashes, single quotes, empty strings, and the text `null`
stay part of the value; producer backslashes and Unicode escapes decode
once. Older/tagged documents retain YAML scalar semantics; double-quoted
values also retain YAML string semantics.
The `version: 2.9.0`-or-later header that Apktool 2.9+ and 3 emit selects
their single-line string dialect: literal hashes, single quotes, empty
strings, and the text `null` stay part of the value; producer backslashes
and Unicode escapes decode once. Older/tagged documents retain YAML scalar
semantics; double-quoted values also retain YAML string semantics.
This distinction follows the released
[2.9.0 metadata writer](https://github.com/iBotPeaches/Apktool/blob/b64e257c9b689eb9630ad0241f2fe4f4a749955b/brut.apktool/apktool-lib/src/main/java/brut/androlib/apk/VersionInfo.java#L38-L42),
[3.0.3 metadata writer](https://github.com/iBotPeaches/Apktool/blob/18b5e99cb56ff9451e8aa55b065dcf5bbd616975/brut.apktool/apktool-lib/src/main/java/brut/androlib/meta/VersionInfo.java#L49-L55)
and [string escape owner](https://github.com/iBotPeaches/Apktool/blob/18b5e99cb56ff9451e8aa55b065dcf5bbd616975/brut.j.yaml/src/main/java/brut/yaml/YamlStringEscapeUtils.java#L55-L116).
- **The manifest is the decoded text**, bounded at 512 KiB and reported as
+46
View File
@@ -190,6 +190,52 @@ sdkInfo:
});
});
describe("parseApktoolYml Apktool 2.9+ metadata dialect", () => {
// Source-derived from ApkInfo.write, VersionInfo.write and YamlWriter at
// Apktool v2.9.0 and v2.12.1, whose string escaping matches 3.0.3.
const yml = (version: string, versionName: string) =>
[
`version: ${version}`,
"apkFileName: app-release.apk",
"isFrameworkApk: false",
"usesFramework:",
" ids:",
" - 1",
" tag: null",
"sdkInfo:",
" minSdkVersion: 24",
" targetSdkVersion: 34",
"packageInfo:",
" forcedPackageId: 127",
" renameManifestPackage: null",
"versionInfo:",
" versionCode: 7",
` versionName: ${versionName}`,
"sharedLibrary: false",
"sparseResources: false",
"doNotCompress:",
"- arsc",
"",
].join("\n");
it.each([
["2.9.0", "1.0 #beta", "1.0 #beta"],
["2.9.3", String.raw`release \"quoted\"`, 'release "quoted"'],
["2.12.1", "1.0: beta", "1.0: beta"],
["2.12.1", "", ""],
])(
"reads Apktool %s version string %j as its writer escaped it",
(version, source, expected) => {
expect(parseApktoolYml(yml(version, source))).toEqual({
versionName: expected,
versionCode: "7",
minSdkVersion: "24",
targetSdkVersion: "34",
});
},
);
});
describe("parseManifestPackage", () => {
it("reads the package attribute from a decoded manifest", () => {
const manifest =
+13 -7
View File
@@ -23,14 +23,19 @@ const metaInfoTag = {
};
/**
* Read the producer's metadata dialect once. Apktool 3 emits an untagged,
* line-oriented document with Java-escaped strings; older tagged documents
* use YAML core semantics. Numeric values retain their source spelling.
* Read the producer's metadata dialect once. Apktool 2.9.0 and later (including
* 3) emit an untagged, line-oriented document with Java-escaped strings; older
* tagged documents use YAML core semantics. Numeric values retain their source
* spelling.
*/
export const parseApktoolYml = (yml: string): ApktoolMetadata => {
const source = yml.replace(/^\uFEFF/u, "");
const header = source.split(/\r\n?|\n/u, 1)[0] ?? "";
if (/^version: 3\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]+)?$/u.test(header))
if (
/^version: (?:2\.(?:9|[1-9]\d)|3\.\d+)\.\d+(?:[-+][0-9A-Za-z.+-]+)?$/u.test(
header,
)
)
return parseApktool3Metadata(source);
const document = parseDocument(yml, {
schema: "core",
@@ -65,9 +70,10 @@ const apktool3Fields = new Map<string, keyof ApktoolMetadata>([
]);
/**
* Apktool 3.0.3 YamlWriter writes strings without YAML quoting or comment
* syntax. Read its direct mapping records before a YAML parser can discard
* hashes, consume quotes, or reinterpret an empty string or literal `null`.
* Apktool 3.0.3 YamlWriter, like the 2.9.0 writer it descends from, writes
* strings without YAML quoting or comment syntax. Read its direct mapping
* records before a YAML parser can discard hashes, consume quotes, or
* reinterpret an empty string or literal `null`.
*/
const parseApktool3Metadata = (source: string): ApktoolMetadata => {
const values = new Map<keyof ApktoolMetadata, string>();