[GH-ISSUE #4916] [Bug] decode_android_resources selects values-<lang>-r<DDD> for a three-digit region, a table Apktool never writes #3051

Closed
opened 2026-10-31 00:35:30 +00:00 by bsakaguchi · 6 comments
Owner

Originally created by @mertkayacs on GitHub (Oct 17, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/4916

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Native analysis or decompilation

Observed behavior

decode_android_resources accepts a regional locale alias and normalizes it at the selection owner: pt-BR reads res/values-pt-rBR/strings.xml. The same rule applies to a three-digit region, so es-419 reads res/values-es-r419/strings.xml. Apktool does not write that directory. It writes Latin American Spanish as res/values-b+es+419/strings.xml, so the request reports the table missing with the limitation No strings.xml was found for locale es-419 while the table is present under its real name. The grammar also admits es-r419 directly, which selects the same nonexistent directory.

Reproduction

  1. Decode an APK whose resources include a b+es+419 string table (Apktool lays it out as res/values-b+es+419/strings.xml).
  2. rea decode-android-resources /absolute/path/app.apk --locale es-419 --json
  3. Read string_tables[0].status and limitations: missing, No strings.xml was found for locale es-419.
  4. rea decode-android-resources /absolute/path/app.apk --locale b+es+419 --json returns the same table as present.

Source path, executed through the unit boundary on main d0253c99d: androidLocaleDirectorySuffix("es-419") returns "es-r419" (src/domain/apktool/apktoolResourceAnalysis.ts), and projectApktoolStringTables with locale: "es-419" reads res/values-es-r419/strings.xml (src/apktool/ApktoolStringTables.ts).

Expected behavior

es-419 selects values-b+es+419, the only spelling Apktool writes for a three-digit region. Apktool's ResConfig.computeQualifiers (brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResConfig.java, main) uses the legacy -<lang>[-r<REGION>] form only when the script is empty, the variant is empty and the region is empty or two characters long; every other locale is written as -b+<lang>[+<script>][+<region>][+<variant>]. aapt2's directory parser agrees: LocaleValue::InitFromParts (frameworks/base/libs/androidfw/Locale.cpp) takes a legacy region part only when it is r plus two characters, so values-es-r419 is not a directory aapt2 accepts as input. es-r419 should therefore not be admitted by the grammar at all.

Impact and scope

Every decode_android_resources request that names a three-digit region in the <lang>-<DDD> form (es-419, en-001, ar-015, fil-419 and the like) misses its table and reports it missing. Deterministic. Frequency across real APKs: NOT_ENOUGH_INFO; not measured, though es-419 is the common Latin American Spanish qualifier.

REA version

6.5.0, source d0253c99d48485b6a7100300aa98401811c2deb9

Runtime and platform

Node 24.18.0, Linux x64

Target and artifact context

Any APK with a values-b+<lang>+<DDD> string table; Apktool is the only provider involved.

Interface

CLI

Earliest failure stage

Request parsing or validation

Relevant output or evidence

Executed: androidLocaleDirectorySuffix("es-419") === "es-r419" and the projection reads res/values-es-r419/strings.xml (regression tests in the linked PR fail on main: 5 failed / 35 passed across the two unit files, 1 failed / 4 passed in the CLI locale boundary test). Source-derived: Apktool ResConfig.computeQualifiers and androidfw LocaleValue::InitFromParts as cited above. The -r419 spelling has one source, ResTable_config::appendDirLocale in androidfw ResourceTypes.cpp, which prints a config without a script that way; it is a runtime printer, not the decoder that lays out the workspace REA reads.

Typed outcome or recovery expected

es-419 and b+es+419 as two aliases of one physical table, read once and both present; es-r419 refused by the locale grammar.

Analysis or suspected cause

Source-derived, see above. Fix with regression tests prepared; PR to follow.

Originally created by @mertkayacs on GitHub (Oct 17, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4916 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Native analysis or decompilation ### Observed behavior `decode_android_resources` accepts a regional locale alias and normalizes it at the selection owner: `pt-BR` reads `res/values-pt-rBR/strings.xml`. The same rule applies to a three-digit region, so `es-419` reads `res/values-es-r419/strings.xml`. Apktool does not write that directory. It writes Latin American Spanish as `res/values-b+es+419/strings.xml`, so the request reports the table `missing` with the limitation `No strings.xml was found for locale es-419` while the table is present under its real name. The grammar also admits `es-r419` directly, which selects the same nonexistent directory. ### Reproduction 1. Decode an APK whose resources include a `b+es+419` string table (Apktool lays it out as `res/values-b+es+419/strings.xml`). 2. `rea decode-android-resources /absolute/path/app.apk --locale es-419 --json` 3. Read `string_tables[0].status` and `limitations`: `missing`, `No strings.xml was found for locale es-419`. 4. `rea decode-android-resources /absolute/path/app.apk --locale b+es+419 --json` returns the same table as `present`. Source path, executed through the unit boundary on main `d0253c99d`: `androidLocaleDirectorySuffix("es-419")` returns `"es-r419"` (`src/domain/apktool/apktoolResourceAnalysis.ts`), and `projectApktoolStringTables` with `locale: "es-419"` reads `res/values-es-r419/strings.xml` (`src/apktool/ApktoolStringTables.ts`). ### Expected behavior `es-419` selects `values-b+es+419`, the only spelling Apktool writes for a three-digit region. Apktool's `ResConfig.computeQualifiers` (`brut.apktool/apktool-lib/src/main/java/brut/androlib/res/table/ResConfig.java`, `main`) uses the legacy `-<lang>[-r<REGION>]` form only when the script is empty, the variant is empty and the region is empty or two characters long; every other locale is written as `-b+<lang>[+<script>][+<region>][+<variant>]`. aapt2's directory parser agrees: `LocaleValue::InitFromParts` (`frameworks/base/libs/androidfw/Locale.cpp`) takes a legacy region part only when it is `r` plus two characters, so `values-es-r419` is not a directory aapt2 accepts as input. `es-r419` should therefore not be admitted by the grammar at all. ### Impact and scope Every `decode_android_resources` request that names a three-digit region in the `<lang>-<DDD>` form (`es-419`, `en-001`, `ar-015`, `fil-419` and the like) misses its table and reports it `missing`. Deterministic. Frequency across real APKs: NOT_ENOUGH_INFO; not measured, though `es-419` is the common Latin American Spanish qualifier. ### REA version 6.5.0, source d0253c99d48485b6a7100300aa98401811c2deb9 ### Runtime and platform Node 24.18.0, Linux x64 ### Target and artifact context Any APK with a `values-b+<lang>+<DDD>` string table; Apktool is the only provider involved. ### Interface CLI ### Earliest failure stage Request parsing or validation ### Relevant output or evidence Executed: `androidLocaleDirectorySuffix("es-419") === "es-r419"` and the projection reads `res/values-es-r419/strings.xml` (regression tests in the linked PR fail on main: 5 failed / 35 passed across the two unit files, 1 failed / 4 passed in the CLI locale boundary test). Source-derived: Apktool `ResConfig.computeQualifiers` and androidfw `LocaleValue::InitFromParts` as cited above. The `-r419` spelling has one source, `ResTable_config::appendDirLocale` in androidfw `ResourceTypes.cpp`, which prints a config without a script that way; it is a runtime printer, not the decoder that lays out the workspace REA reads. ### Typed outcome or recovery expected `es-419` and `b+es+419` as two aliases of one physical table, read once and both `present`; `es-r419` refused by the locale grammar. ### Analysis or suspected cause Source-derived, see above. Fix with regression tests prepared; PR to follow.
Author
Owner

@mertkayacs commented on GitHub (Oct 17, 2026):

Fix with regression tests is ready on my fork (branch fix/apktool-numeric-region-locale) and I will open the PR as soon as one of my three open PRs here is resolved, since outside contributors are limited to three at a time. The alias owner maps a three-digit region to Apktool's b+ form (es-419 reads values-b+es+419), two-letter regions keep the -r form, and the grammar no longer admits -r.

<!-- gh-comment-id:6243201643 --> @mertkayacs commented on GitHub (Oct 17, 2026): Fix with regression tests is ready on my fork (branch fix/apktool-numeric-region-locale) and I will open the PR as soon as one of my three open PRs here is resolved, since outside contributors are limited to three at a time. The alias owner maps a three-digit region to Apktool's b+ form (es-419 reads values-b+es+419), two-letter regions keep the -r form, and the grammar no longer admits <lang>-r<DDD>.
Author
Owner

@mertkayacs commented on GitHub (Oct 17, 2026):

Opened https://github.com/morluto/rea/pull/4918 with the fix and its regression tests.

<!-- gh-comment-id:6243283952 --> @mertkayacs commented on GitHub (Oct 17, 2026): Opened https://github.com/morluto/rea/pull/4918 with the fix and its regression tests.
Author
Owner

@XinwangTao commented on GitHub (Oct 18, 2026):

Confirmed — this matches the two-level mismatch: the legacy -<lang>[-r<REGION>] form is only valid for two-character regions (per ResConfig.computeQualifiers and androidfw's LocaleValue::InitFromParts), so three-digit regions like es-419, en-001, ar-015 are always written as b++ by Apktool. Routing es-419 to values-es-r419 is therefore guaranteed to miss the table, and since values-es-r419 isn't even a directory aapt2 accepts as input, the grammar arguably shouldn't admit es-r419 at all.

The #4918 direction (select three-digit regions through the b+ locale table) looks right. Suggestions for coverage:

  1. Add fixtures for the other common three-digit regions (en-001, ar-015, fil-419), not just es-419, since they share the same code path.
  2. Decide and test what happens when a user passes es-r419 directly — rejecting it with a clear error (aapt2 can't read that spelling) may be better than silently normalizing.
  3. Keep the two-digit region path (pt-BR → pt-rBR) intact and add a regression test that both spellings resolve to the same underlying table where Apktool would write them identically.
<!-- gh-comment-id:6244205340 --> @XinwangTao commented on GitHub (Oct 18, 2026): Confirmed — this matches the two-level mismatch: the legacy `-<lang>[-r<REGION>]` form is only valid for two-character regions (per ResConfig.computeQualifiers and androidfw's LocaleValue::InitFromParts), so three-digit regions like es-419, en-001, ar-015 are always written as b+<lang>+<DDD> by Apktool. Routing es-419 to values-es-r419 is therefore guaranteed to miss the table, and since values-es-r419 isn't even a directory aapt2 accepts as input, the grammar arguably shouldn't admit es-r419 at all. The #4918 direction (select three-digit regions through the b+ locale table) looks right. Suggestions for coverage: 1. Add fixtures for the other common three-digit regions (en-001, ar-015, fil-419), not just es-419, since they share the same code path. 2. Decide and test what happens when a user passes `es-r419` directly — rejecting it with a clear error (aapt2 can't read that spelling) may be better than silently normalizing. 3. Keep the two-digit region path (pt-BR → pt-rBR) intact and add a regression test that both spellings resolve to the same underlying table where Apktool would write them identically.
Author
Owner

@mertkayacs commented on GitHub (Oct 18, 2026):

Thanks, all three are in #4918 now.

  1. en-001 and ar-015 are covered next to es-419: the alias test maps each to its b+ form, the selectable-table test accepts b+en+001 and b+ar+015, and the string-table projection test reads values-b+<lang>+<DDD>/strings.xml once for each pair of aliases and reports both present.
  2. Passing es-r419 directly is rejected by the grammar, as you suggested, rather than normalized. The CLI exits 1 with code: VALIDATION_ERROR and a locale field error reading Locale must look like de, pt-BR, pt-rBR, es-419, or b+zh+Hans+CN, so the accepted spelling is in the message. The domain test pins that message for es-r419, en-r001 and ar-r015, and the CLI boundary test pins the exit code, the error code and the field error for en-r419 and es-r419.
  3. The two-letter path is unchanged and pinned: pt-BR normalizes to pt-rBR, and the existing projection test reads values-pt-rBR/strings.xml once for pt-BR and pt-rBR together.

On the Apktool side I also confirmed the rule holds back to the supported floor: v2.7.0 ResConfigFlags.getLocaleString already writes a three-character region only as b+, and 2.11, 2.12 and 3.x keep that, so there is no version on which values-es-r419 would have been the right directory.

<!-- gh-comment-id:6247186639 --> @mertkayacs commented on GitHub (Oct 18, 2026): Thanks, all three are in #4918 now. 1. `en-001` and `ar-015` are covered next to `es-419`: the alias test maps each to its `b+` form, the selectable-table test accepts `b+en+001` and `b+ar+015`, and the string-table projection test reads `values-b+<lang>+<DDD>/strings.xml` once for each pair of aliases and reports both `present`. 2. Passing `es-r419` directly is rejected by the grammar, as you suggested, rather than normalized. The CLI exits 1 with `code: VALIDATION_ERROR` and a `locale` field error reading `Locale must look like de, pt-BR, pt-rBR, es-419, or b+zh+Hans+CN`, so the accepted spelling is in the message. The domain test pins that message for `es-r419`, `en-r001` and `ar-r015`, and the CLI boundary test pins the exit code, the error code and the field error for `en-r419` and `es-r419`. 3. The two-letter path is unchanged and pinned: `pt-BR` normalizes to `pt-rBR`, and the existing projection test reads `values-pt-rBR/strings.xml` once for `pt-BR` and `pt-rBR` together. On the Apktool side I also confirmed the rule holds back to the supported floor: v2.7.0 `ResConfigFlags.getLocaleString` already writes a three-character region only as `b+`, and 2.11, 2.12 and 3.x keep that, so there is no version on which `values-es-r419` would have been the right directory.
Author
Owner

@XinwangTao commented on GitHub (Oct 18, 2026):

Thanks — all three suggestions landed exactly as I hoped: fixtures for en-001/ar-015 next to es-419, a hard VALIDATION_ERROR for the -r419 spelling instead of silently normalizing a directory aapt2 can't parse, and the pinned two-letter path (pt-BR → pt-rBR) unchanged. The version-floor check (b+ form since Apktool 2.7.0) is useful context to put in the changelog so nobody reintroduces the legacy spelling.

<!-- gh-comment-id:6247429064 --> @XinwangTao commented on GitHub (Oct 18, 2026): Thanks — all three suggestions landed exactly as I hoped: fixtures for en-001/ar-015 next to es-419, a hard VALIDATION_ERROR for the -r419 spelling instead of silently normalizing a directory aapt2 can't parse, and the pinned two-letter path (pt-BR → pt-rBR) unchanged. The version-floor check (b+ form since Apktool 2.7.0) is useful context to put in the changelog so nobody reintroduces the legacy spelling.
Author
Owner

@morluto commented on GitHub (Oct 18, 2026):

Fixed on main in 29910d16b9, adapting the owner correction and regression cases from #4918 with Mert Kaya credited as coauthor. This does not qualify or merge the entire PR head.

The shared locale owner had applied the legacy two-letter region rule to numeric regions and admitted nonexistent -rDDD selectors. Numeric aliases now select b+ tables, CLI/MCP reject malformed es-r419, and table discovery uses that same owner. Caller labels stay intact, and es-419 / b+es+419 share one physical table read. The existing test that pinned the wrong en-r419 path was corrected. The commit marks the rejected selector compatibility change explicitly.

Baseline owner/projection regressions were red (12 failures, 41 passes). Final validation: 161 source tests in 5 files; 41 CLI/MCP/schema checks in 6 files; final MCP rerun passes after replacing parallel request-field arrays with one typed request record. Full static/architecture, docs (5 checks), type/lint/pre-push and discovery (1590 files exactly once) pass. Independent source review found no remaining directly related cleanup. Android documentation, AAPT2 and Apktool 2.7/2.12/3.0 source agree on numeric b+ region spelling.

A production compiled CLI with a controlled launcher and real owned decoded workspace reproduced empty strings plus No strings.xml was found for locale es-419 before the fix, then returned greeting: Hola afterward. In 10 completed controlled tasks, the baseline known-workaround task needed an explicit b+es+419 retry (2 CLI calls); the fixed task completed directly (1 call), retaining matching target/provider/strings/metadata/manifest facts. Four warm repeats per variant: median full-response cl100k estimate 1179→583, elapsed 1.592→0.799s, CPU 2.181→1.056s, sampled process-family RSS 521.8→516.0MB. First runs were separate; no cold-cache reset or general speedup claim. Model context/tool selection, real decoder/large-APK costs and Windows/Linux remain unmeasured.

No real Apktool engine or real APK decoding was performed; the launcher controlled the produced tree. These checks establish REA selection/projection and public contract behavior, not engine qualification.

<!-- gh-comment-id:6254138805 --> @morluto commented on GitHub (Oct 18, 2026): Fixed on main in 29910d16b98fe4b607a7aa15928789a73614b0b9, adapting the owner correction and regression cases from #4918 with Mert Kaya credited as coauthor. This does not qualify or merge the entire PR head. The shared locale owner had applied the legacy two-letter region rule to numeric regions and admitted nonexistent `-rDDD` selectors. Numeric aliases now select `b+` tables, CLI/MCP reject malformed `es-r419`, and table discovery uses that same owner. Caller labels stay intact, and `es-419` / `b+es+419` share one physical table read. The existing test that pinned the wrong `en-r419` path was corrected. The commit marks the rejected selector compatibility change explicitly. Baseline owner/projection regressions were red (12 failures, 41 passes). Final validation: 161 source tests in 5 files; 41 CLI/MCP/schema checks in 6 files; final MCP rerun passes after replacing parallel request-field arrays with one typed request record. Full static/architecture, docs (5 checks), type/lint/pre-push and discovery (1590 files exactly once) pass. Independent source review found no remaining directly related cleanup. Android documentation, AAPT2 and Apktool 2.7/2.12/3.0 source agree on numeric `b+` region spelling. A production compiled CLI with a controlled launcher and real owned decoded workspace reproduced empty strings plus `No strings.xml was found for locale es-419` before the fix, then returned `greeting: Hola` afterward. In 10 completed controlled tasks, the baseline known-workaround task needed an explicit `b+es+419` retry (2 CLI calls); the fixed task completed directly (1 call), retaining matching target/provider/strings/metadata/manifest facts. Four warm repeats per variant: median full-response cl100k estimate 1179→583, elapsed 1.592→0.799s, CPU 2.181→1.056s, sampled process-family RSS 521.8→516.0MB. First runs were separate; no cold-cache reset or general speedup claim. Model context/tool selection, real decoder/large-APK costs and Windows/Linux remain unmeasured. No real Apktool engine or real APK decoding was performed; the launcher controlled the produced tree. These checks establish REA selection/projection and public contract behavior, not engine qualification.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3051
No description provided.