feat(res): improve resource names (PR #2316)
This commit is contained in:
@@ -114,7 +114,7 @@ public class AndroidResourcesUtils {
|
||||
}
|
||||
for (ResourceEntry resource : resStorage.getResources()) {
|
||||
String resTypeName = resource.getTypeName();
|
||||
String resName = resTypeName.equals("style") ? resource.getKeyName().replace('.', '_') : resource.getKeyName();
|
||||
String resName = resource.getKeyName().replace('.', '_');
|
||||
|
||||
ResClsInfo typeClsInfo = innerClsMap.computeIfAbsent(
|
||||
resTypeName,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,8 @@ package jadx.core.xmlgen;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
@@ -17,7 +13,6 @@ import org.slf4j.LoggerFactory;
|
||||
import jadx.api.ICodeInfo;
|
||||
import jadx.api.args.ResourceNameSource;
|
||||
import jadx.api.plugins.utils.ZipSecurity;
|
||||
import jadx.core.deobf.NameMapper;
|
||||
import jadx.core.dex.attributes.AFlag;
|
||||
import jadx.core.dex.nodes.FieldNode;
|
||||
import jadx.core.dex.nodes.IFieldInfoRef;
|
||||
@@ -33,8 +28,6 @@ import jadx.core.xmlgen.entry.ValuesParser;
|
||||
public class ResTableBinaryParser extends CommonBinaryParser implements IResTableParser {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ResTableBinaryParser.class);
|
||||
|
||||
private static final Pattern VALID_RES_KEY_PATTERN = Pattern.compile("[\\w\\d_]+");
|
||||
|
||||
private static final class PackageChunk {
|
||||
private final int id;
|
||||
private final String name;
|
||||
@@ -154,7 +147,6 @@ public class ResTableBinaryParser extends CommonBinaryParser implements IResTabl
|
||||
if (keyStringsOffset != 0) {
|
||||
is.skipToPos(keyStringsOffset, "Expected keyStrings string pool");
|
||||
keyStrings = parseStringPool();
|
||||
deobfKeyStrings(keyStrings);
|
||||
}
|
||||
|
||||
PackageChunk pkg = new PackageChunk(id, name, typeStrings, keyStrings);
|
||||
@@ -193,32 +185,6 @@ public class ResTableBinaryParser extends CommonBinaryParser implements IResTabl
|
||||
return pkg;
|
||||
}
|
||||
|
||||
private void deobfKeyStrings(BinaryXMLStrings keyStrings) {
|
||||
int keysCount = keyStrings.size();
|
||||
if (root.getArgs().isRenamePrintable()) {
|
||||
for (int i = 0; i < keysCount; i++) {
|
||||
String keyString = keyStrings.get(i);
|
||||
if (!NameMapper.isAllCharsPrintable(keyString)) {
|
||||
keyStrings.put(i, makeNewKeyName(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (root.getArgs().isRenameValid()) {
|
||||
Set<String> keySet = new HashSet<>(keysCount);
|
||||
for (int i = 0; i < keysCount; i++) {
|
||||
String keyString = keyStrings.get(i);
|
||||
boolean isNew = keySet.add(keyString);
|
||||
if (!isNew) {
|
||||
keyStrings.put(i, makeNewKeyName(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String makeNewKeyName(int idx) {
|
||||
return String.format("jadx_deobf_0x%08x", idx);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private void parseTypeSpecChunk(long chunkStart) throws IOException {
|
||||
is.checkInt16(0x0010, "Unexpected type spec header size");
|
||||
@@ -429,7 +395,7 @@ public class ResTableBinaryParser extends CommonBinaryParser implements IResTabl
|
||||
if (useRawResName) {
|
||||
newResEntry = new ResourceEntry(resRef, pkg.getName(), typeName, origKeyName, config);
|
||||
} else {
|
||||
String resName = getResName(typeName, resRef, origKeyName);
|
||||
String resName = getResName(resRef, origKeyName);
|
||||
newResEntry = new ResourceEntry(resRef, pkg.getName(), typeName, resName, config);
|
||||
ResourceEntry prevResEntry = resStorage.searchEntryWithSameName(newResEntry);
|
||||
if (prevResEntry != null) {
|
||||
@@ -449,7 +415,7 @@ public class ResTableBinaryParser extends CommonBinaryParser implements IResTabl
|
||||
return newResEntry;
|
||||
}
|
||||
|
||||
private String getResName(String typeName, int resRef, String origKeyName) {
|
||||
private String getResName(int resRef, String origKeyName) {
|
||||
if (this.useRawResName) {
|
||||
return origKeyName;
|
||||
}
|
||||
@@ -457,40 +423,38 @@ public class ResTableBinaryParser extends CommonBinaryParser implements IResTabl
|
||||
if (renamedKey != null) {
|
||||
return renamedKey;
|
||||
}
|
||||
// styles might contain dots in name, search for alias only for resources names
|
||||
if (typeName.equals("style")) {
|
||||
return origKeyName;
|
||||
}
|
||||
|
||||
IFieldInfoRef fldRef = root.getConstValues().getGlobalConstFields().get(resRef);
|
||||
FieldNode constField = fldRef instanceof FieldNode ? (FieldNode) fldRef : null;
|
||||
String resAlias = getResAlias(resRef, origKeyName, constField);
|
||||
resStorage.addRename(resRef, resAlias);
|
||||
|
||||
String newResName = getNewResName(resRef, origKeyName, constField);
|
||||
if (!origKeyName.equals(newResName)) {
|
||||
resStorage.addRename(resRef, newResName);
|
||||
}
|
||||
|
||||
if (constField != null) {
|
||||
constField.rename(resAlias);
|
||||
final String newFieldName = ResNameUtils.convertToRFieldName(newResName);
|
||||
constField.rename(newFieldName);
|
||||
constField.add(AFlag.DONT_RENAME);
|
||||
}
|
||||
return resAlias;
|
||||
|
||||
return newResName;
|
||||
}
|
||||
|
||||
private String getResAlias(int resRef, String origKeyName, @Nullable FieldNode constField) {
|
||||
String name;
|
||||
private String getNewResName(int resRef, String origKeyName, @Nullable FieldNode constField) {
|
||||
String newResName;
|
||||
if (constField == null || constField.getTopParentClass().isSynthetic()) {
|
||||
name = origKeyName;
|
||||
newResName = origKeyName;
|
||||
} else {
|
||||
name = getBetterName(root.getArgs().getResourceNameSource(), origKeyName, constField.getName());
|
||||
newResName = getBetterName(root.getArgs().getResourceNameSource(), origKeyName, constField.getName());
|
||||
}
|
||||
Matcher matcher = VALID_RES_KEY_PATTERN.matcher(name);
|
||||
if (matcher.matches()) {
|
||||
return name;
|
||||
|
||||
if (root.getArgs().isRenameValid()) {
|
||||
final boolean allowNonPrintable = !root.getArgs().isRenamePrintable();
|
||||
newResName = ResNameUtils.sanitizeAsResourceName(newResName, String.format("_res_0x%08x", resRef), allowNonPrintable);
|
||||
}
|
||||
// Making sure origKeyName compliant with resource file name rules
|
||||
String cleanedResName = cleanName(matcher);
|
||||
String newResName = String.format("res_0x%08x", resRef);
|
||||
if (cleanedResName.isEmpty()) {
|
||||
return newResName;
|
||||
}
|
||||
// autogenerate key name, appended with cleaned origKeyName to be human-friendly
|
||||
return newResName + "_" + cleanedResName.toLowerCase();
|
||||
|
||||
return newResName;
|
||||
}
|
||||
|
||||
public static String getBetterName(ResourceNameSource nameSource, String resName, String codeName) {
|
||||
@@ -507,19 +471,6 @@ public class ResTableBinaryParser extends CommonBinaryParser implements IResTabl
|
||||
}
|
||||
}
|
||||
|
||||
private String cleanName(Matcher matcher) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
boolean first = true;
|
||||
while (matcher.find()) {
|
||||
if (!first) {
|
||||
sb.append("_");
|
||||
}
|
||||
sb.append(matcher.group());
|
||||
first = false;
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private RawNamedValue parseValueMap() throws IOException {
|
||||
int nameRef = is.readInt32();
|
||||
return new RawNamedValue(nameRef, parseValue());
|
||||
|
||||
Reference in New Issue
Block a user