-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(res): improve resource names (PR #2316)
- Loading branch information
Showing
4 changed files
with
234 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
121 changes: 121 additions & 0 deletions
121
jadx-core/src/main/java/jadx/core/xmlgen/ResNameUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package jadx.core.xmlgen; | ||
|
||
import jadx.core.deobf.NameMapper; | ||
|
||
import static jadx.core.deobf.NameMapper.*; | ||
|
||
class ResNameUtils { | ||
|
||
private ResNameUtils() { | ||
} | ||
|
||
/** | ||
* Sanitizes the name so that it can be used as a resource name. | ||
* By resource name is meant that: | ||
* <ul> | ||
* <li>It can be used by aapt2 as a resource entry name. | ||
* <li>It can be converted to a valid R class field name. | ||
* </ul> | ||
* <p> | ||
* If the {@code name} is already a valid resource name, the method returns it unchanged. | ||
* If not, the method creates a valid resource name based on {@code name}, appends the | ||
* {@code postfix}, and returns the result. | ||
*/ | ||
static String sanitizeAsResourceName(String name, String postfix, boolean allowNonPrintable) { | ||
if (name.isEmpty()) { | ||
return postfix; | ||
} | ||
|
||
final StringBuilder sb = new StringBuilder(name.length() + 1); | ||
boolean nameChanged = false; | ||
|
||
int cp = name.codePointAt(0); | ||
if (isValidResourceNameStart(cp, allowNonPrintable)) { | ||
sb.appendCodePoint(cp); | ||
} else { | ||
sb.append('_'); | ||
nameChanged = true; | ||
|
||
if (isValidResourceNamePart(cp, allowNonPrintable)) { | ||
sb.appendCodePoint(cp); | ||
} | ||
} | ||
|
||
for (int i = Character.charCount(cp); i < name.length(); i += Character.charCount(cp)) { | ||
cp = name.codePointAt(i); | ||
if (isValidResourceNamePart(cp, allowNonPrintable)) { | ||
sb.appendCodePoint(cp); | ||
} else { | ||
sb.append('_'); | ||
nameChanged = true; | ||
} | ||
} | ||
|
||
final String sanitizedName = sb.toString(); | ||
if (NameMapper.isReserved(sanitizedName)) { | ||
nameChanged = true; | ||
} | ||
|
||
return nameChanged | ||
? sanitizedName + postfix | ||
: sanitizedName; | ||
} | ||
|
||
/** | ||
* Converts the resource name to a field name of the R class. | ||
*/ | ||
static String convertToRFieldName(String resourceName) { | ||
return resourceName.replace('.', '_'); | ||
} | ||
|
||
/** | ||
* Determines whether the code point may be part of a resource name as the first character (aapt2 + | ||
* R class gen). | ||
*/ | ||
private static boolean isValidResourceNameStart(int codePoint, boolean allowNonPrintable) { | ||
return (allowNonPrintable || isPrintableAsciiCodePoint(codePoint)) | ||
&& (isValidAapt2ResourceNameStart(codePoint) && isValidIdentifierStart(codePoint)); | ||
} | ||
|
||
/** | ||
* Determines whether the code point may be part of a resource name as other than the first | ||
* character | ||
* (aapt2 + R class gen). | ||
*/ | ||
private static boolean isValidResourceNamePart(int codePoint, boolean allowNonPrintable) { | ||
return (allowNonPrintable || isPrintableAsciiCodePoint(codePoint)) | ||
&& ((isValidAapt2ResourceNamePart(codePoint) && isValidIdentifierPart(codePoint)) || codePoint == '.'); | ||
} | ||
|
||
/** | ||
* Determines whether the code point may be part of a resource name as the first character (aapt2). | ||
* <p> | ||
* Source: <a href= | ||
* "https://cs.android.com/android/platform/superproject/+/android15-release:frameworks/base/tools/aapt2/text/Unicode.cpp;l=112">aapt2/text/Unicode.cpp#L112</a> | ||
*/ | ||
private static boolean isValidAapt2ResourceNameStart(int codePoint) { | ||
return isXidStart(codePoint) || codePoint == '_'; | ||
} | ||
|
||
/** | ||
* Determines whether the code point may be part of a resource name as other than the first | ||
* character (aapt2). | ||
* <p> | ||
* Source: <a href= | ||
* "https://cs.android.com/android/platform/superproject/+/android15-release:frameworks/base/tools/aapt2/text/Unicode.cpp;l=118">aapt2/text/Unicode.cpp#L118</a> | ||
*/ | ||
private static boolean isValidAapt2ResourceNamePart(int codePoint) { | ||
return isXidContinue(codePoint) || codePoint == '.' || codePoint == '-'; | ||
} | ||
|
||
private static boolean isXidStart(int codePoint) { | ||
// TODO: Need to implement a full check if the code point is XID_Start. | ||
return codePoint < 0x0370 && Character.isUnicodeIdentifierStart(codePoint); | ||
} | ||
|
||
private static boolean isXidContinue(int codePoint) { | ||
// TODO: Need to implement a full check if the code point is XID_Continue. | ||
return codePoint < 0x0370 | ||
&& (Character.isUnicodeIdentifierPart(codePoint) && !Character.isIdentifierIgnorable(codePoint)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 89 additions & 0 deletions
89
jadx-core/src/test/java/jadx/core/xmlgen/ResNameUtilsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package jadx.core.xmlgen; | ||
|
||
import java.util.stream.Stream; | ||
|
||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.Arguments; | ||
import org.junit.jupiter.params.provider.MethodSource; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
|
||
class ResNameUtilsTest { | ||
|
||
@DisplayName("Check sanitizeAsResourceName(name, postfix, allowNonPrintable)") | ||
@ParameterizedTest(name = "({0}, {1}, {2}) -> {3}") | ||
@MethodSource("provideArgsForSanitizeAsResourceNameTest") | ||
void testSanitizeAsResourceName(String name, String postfix, boolean allowNonPrintable, String expectedResult) { | ||
assertThat(ResNameUtils.sanitizeAsResourceName(name, postfix, allowNonPrintable)).isEqualTo(expectedResult); | ||
} | ||
|
||
@DisplayName("Check convertToRFieldName(resourceName)") | ||
@ParameterizedTest(name = "{0} -> {1}") | ||
@MethodSource("provideArgsForConvertToRFieldNameTest") | ||
void testConvertToRFieldName(String resourceName, String expectedResult) { | ||
assertThat(ResNameUtils.convertToRFieldName(resourceName)).isEqualTo(expectedResult); | ||
} | ||
|
||
private static Stream<Arguments> provideArgsForSanitizeAsResourceNameTest() { | ||
return Stream.of( | ||
Arguments.of("name", "_postfix", false, "name"), | ||
|
||
Arguments.of("/name", "_postfix", true, "_name_postfix"), | ||
Arguments.of("na/me", "_postfix", true, "na_me_postfix"), | ||
Arguments.of("name/", "_postfix", true, "name__postfix"), | ||
|
||
Arguments.of("$name", "_postfix", true, "_name_postfix"), | ||
Arguments.of("na$me", "_postfix", true, "na_me_postfix"), | ||
Arguments.of("name$", "_postfix", true, "name__postfix"), | ||
|
||
Arguments.of(".name", "_postfix", true, "_.name_postfix"), | ||
Arguments.of("na.me", "_postfix", true, "na.me"), | ||
Arguments.of("name.", "_postfix", true, "name."), | ||
|
||
Arguments.of("0name", "_postfix", true, "_0name_postfix"), | ||
Arguments.of("na0me", "_postfix", true, "na0me"), | ||
Arguments.of("name0", "_postfix", true, "name0"), | ||
|
||
Arguments.of("-name", "_postfix", true, "_name_postfix"), | ||
Arguments.of("na-me", "_postfix", true, "na_me_postfix"), | ||
Arguments.of("name-", "_postfix", true, "name__postfix"), | ||
|
||
Arguments.of("Ĉname", "_postfix", false, "_name_postfix"), | ||
Arguments.of("naĈme", "_postfix", false, "na_me_postfix"), | ||
Arguments.of("nameĈ", "_postfix", false, "name__postfix"), | ||
|
||
Arguments.of("Ĉname", "_postfix", true, "Ĉname"), | ||
Arguments.of("naĈme", "_postfix", true, "naĈme"), | ||
Arguments.of("nameĈ", "_postfix", true, "nameĈ"), | ||
|
||
// Uncomment this when XID_Start and XID_Continue characters are correctly determined. | ||
// Arguments.of("Жname", "_postfix", true, "Жname"), | ||
// Arguments.of("naЖme", "_postfix", true, "naЖme"), | ||
// Arguments.of("nameЖ", "_postfix", true, "nameЖ"), | ||
// | ||
// Arguments.of("€name", "_postfix", true, "_name_postfix"), | ||
// Arguments.of("na€me", "_postfix", true, "na_me_postfix"), | ||
// Arguments.of("name€", "_postfix", true, "name__postfix"), | ||
|
||
Arguments.of("", "_postfix", true, "_postfix"), | ||
|
||
Arguments.of("if", "_postfix", true, "if_postfix"), | ||
Arguments.of("default", "_postfix", true, "default_postfix"), | ||
Arguments.of("true", "_postfix", true, "true_postfix"), | ||
Arguments.of("_", "_postfix", true, "__postfix")); | ||
} | ||
|
||
private static Stream<Arguments> provideArgsForConvertToRFieldNameTest() { | ||
return Stream.of( | ||
Arguments.of("ThemeDesign", "ThemeDesign"), | ||
Arguments.of("Theme.Design", "Theme_Design"), | ||
|
||
Arguments.of("Ĉ_ThemeDesign_Ĉ", "Ĉ_ThemeDesign_Ĉ"), | ||
Arguments.of("Ĉ_Theme.Design_Ĉ", "Ĉ_Theme_Design_Ĉ"), | ||
|
||
// The function must return a plausible result even though the resource name is invalid. | ||
Arguments.of("/_ThemeDesign_/", "/_ThemeDesign_/"), | ||
Arguments.of("/_Theme.Design_/", "/_Theme_Design_/")); | ||
} | ||
} |