Skip to content

Commit

Permalink
feat(res): fix duplicate entries and deobfuscate file names in XML re…
Browse files Browse the repository at this point in the history
…sources (PR #995)

* Fixes dublicates entries in XML resources
* can't use binary search on this list
* add entry config to name comparator, preserve renames by id, improve performance
* Deobf resource files
* Add break
* Changes ResourceFile

Co-authored-by: sergey-wowwow <bugi@MacBook-Pro.local>
Co-authored-by: Skylot <skylot@gmail.com>
  • Loading branch information
3 people committed Oct 25, 2020
1 parent 9f68493 commit 71617a1
Show file tree
Hide file tree
Showing 13 changed files with 150 additions and 63 deletions.
17 changes: 16 additions & 1 deletion jadx-core/src/main/java/jadx/api/ResourceFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.entry.ResourceEntry;

public class ResourceFile {

Expand Down Expand Up @@ -34,6 +35,7 @@ public String toString() {
private final String name;
private final ResourceType type;
private ZipRef zipRef;
private String deobfName;

public static ResourceFile createResourceFile(JadxDecompiler decompiler, String name, ResourceType type) {
if (!ZipSecurity.isValidZipEntryName(name)) {
Expand All @@ -48,10 +50,14 @@ protected ResourceFile(JadxDecompiler decompiler, String name, ResourceType type
this.type = type;
}

public String getName() {
public String getOriginalName() {
return name;
}

public String getDeobfName() {
return deobfName != null ? deobfName : name;
}

public ResourceType getType() {
return type;
}
Expand All @@ -64,6 +70,15 @@ void setZipRef(ZipRef zipRef) {
this.zipRef = zipRef;
}

public void setAlias(ResourceEntry ri) {
int index = name.lastIndexOf('.');
deobfName = String.format("%s%s/%s%s",
ri.getTypeName(),
ri.getConfig(),
ri.getKeyName(),
index == -1 ? "" : name.substring(index));
}

public ZipRef getZipRef() {
return zipRef;
}
Expand Down
2 changes: 1 addition & 1 deletion jadx-core/src/main/java/jadx/api/ResourceFileContent.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@ public ResourceFileContent(String name, ResourceType type, ICodeInfo content) {

@Override
public ResContainer loadContent() {
return ResContainer.textResource(getName(), content);
return ResContainer.textResource(getDeobfName(), content);
}
}
12 changes: 6 additions & 6 deletions jadx-core/src/main/java/jadx/api/ResourcesLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static <T> T decodeStream(ResourceFile rf, ResourceDecoder<T> decoder) th
try {
ZipRef zipRef = rf.getZipRef();
if (zipRef == null) {
File file = new File(rf.getName());
File file = new File(rf.getOriginalName());
try (InputStream inputStream = new BufferedInputStream(new FileInputStream(file))) {
return decoder.decode(file.length(), inputStream);
}
Expand All @@ -74,7 +74,7 @@ public static <T> T decodeStream(ResourceFile rf, ResourceDecoder<T> decoder) th
}
}
} catch (Exception e) {
throw new JadxException("Error decode: " + rf.getName(), e);
throw new JadxException("Error decode: " + rf.getDeobfName(), e);
}
}

Expand All @@ -86,7 +86,7 @@ static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf) {
CodeWriter cw = new CodeWriter();
cw.add("Error decode ").add(rf.getType().toString().toLowerCase());
Utils.appendStackTrace(cw, e.getCause());
return ResContainer.textResource(rf.getName(), cw.finish());
return ResContainer.textResource(rf.getDeobfName(), cw.finish());
}
}

Expand All @@ -96,7 +96,7 @@ private static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf,
case MANIFEST:
case XML:
ICodeInfo content = jadxRef.getXmlParser().parse(inputStream);
return ResContainer.textResource(rf.getName(), content);
return ResContainer.textResource(rf.getOriginalName(), content);

case ARSC:
return new ResTableParser(jadxRef.getRoot()).decodeFiles(inputStream);
Expand All @@ -110,12 +110,12 @@ private static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf,
}

private static ResContainer decodeImage(ResourceFile rf, InputStream inputStream) {
String name = rf.getName();
String name = rf.getOriginalName();
if (name.endsWith(".9.png")) {
try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
Res9patchStreamDecoder decoder = new Res9patchStreamDecoder();
decoder.decode(inputStream, os);
return ResContainer.decodedData(rf.getName(), os.toByteArray());
return ResContainer.decodedData(rf.getDeobfName(), os.toByteArray());
} catch (Exception e) {
LOG.error("Failed to decode 9-patch png image, path: {}", name, e);
}
Expand Down
29 changes: 23 additions & 6 deletions jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.ResTableParser;
import jadx.core.xmlgen.ResourceStorage;
import jadx.core.xmlgen.entry.ResourceEntry;
import jadx.core.xmlgen.entry.ValuesParser;

public class RootNode {
private static final Logger LOG = LoggerFactory.getLogger(RootNode.class);
Expand Down Expand Up @@ -131,13 +133,14 @@ public void loadResources(List<ResourceFile> resources) {
return;
}
try {
ResourceStorage resStorage = ResourcesLoader.decodeStream(arsc, (size, is) -> {
ResTableParser parser = new ResTableParser(this);
parser.decode(is);
return parser.getResStorage();
ResTableParser parser = ResourcesLoader.decodeStream(arsc, (size, is) -> {
ResTableParser tableParser = new ResTableParser(this);
tableParser.decode(is);
return tableParser;
});
if (resStorage != null) {
processResources(resStorage);
if (parser != null) {
processResources(parser.getResStorage());
updateObfuscatedFiles(parser, resources);
}
} catch (Exception e) {
LOG.error("Failed to parse '.arsc' file", e);
Expand All @@ -163,6 +166,20 @@ public void initClassPath() {
}
}

private void updateObfuscatedFiles(ResTableParser parser, List<ResourceFile> resources) {
ResourceStorage resStorage = parser.getResStorage();
ValuesParser valuesParser = new ValuesParser(this, parser.getStrings(), resStorage.getResourcesNames());
for (int i = 0; i < resources.size(); i++) {
ResourceFile resource = resources.get(i);
for (ResourceEntry ri : parser.getResStorage().getResources()) {
if (resource.getOriginalName().equals(valuesParser.getValueString(ri))) {
resource.setAlias(ri);
break;
}
}
}
}

private void initInnerClasses() {
// move inner classes
List<ClassNode> inner = new ArrayList<>();
Expand Down
2 changes: 1 addition & 1 deletion jadx-core/src/main/java/jadx/core/xmlgen/ResContainer.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public static ResContainer decodedData(String name, byte[] data) {
}

public static ResContainer resourceFileLink(ResourceFile resFile) {
return new ResContainer(resFile.getName(), Collections.emptyList(), resFile, DataType.RES_LINK);
return new ResContainer(resFile.getDeobfName(), Collections.emptyList(), resFile, DataType.RES_LINK);
}

public static ResContainer resourceTable(String name, List<ResContainer> subFiles, ICodeInfo rootContent) {
Expand Down
53 changes: 36 additions & 17 deletions jadx-core/src/main/java/jadx/core/xmlgen/ResTableParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import org.slf4j.Logger;
Expand Down Expand Up @@ -241,12 +242,12 @@ private void parseTypeChunk(long start, PackageChunk pkg) throws IOException {
is.checkPos(entriesStart, "Expected entry start");
for (int i = 0; i < entryCount; i++) {
if (entryIndexes[i] != NO_ENTRY) {
parseEntry(pkg, id, i, config);
parseEntry(pkg, id, i, config.getQualifiers());
}
}
}

private void parseEntry(PackageChunk pkg, int typeId, int entryId, EntryConfig config) throws IOException {
private void parseEntry(PackageChunk pkg, int typeId, int entryId, String config) throws IOException {
int size = is.readInt16();
int flags = is.readInt16();
int key = is.readInt32();
Expand All @@ -256,32 +257,50 @@ private void parseEntry(PackageChunk pkg, int typeId, int entryId, EntryConfig c

int resRef = pkg.getId() << 24 | typeId << 16 | entryId;
String typeName = pkg.getTypeStrings()[typeId - 1];
String keyName = pkg.getKeyStrings()[key];
if (keyName.isEmpty()) {
FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef);
if (constField != null) {
keyName = constField.getName();
constField.add(AFlag.DONT_RENAME);
} else {
keyName = "RES_" + resRef; // autogenerate key name
}
String origKeyName = pkg.getKeyStrings()[key];
ResourceEntry newResEntry = new ResourceEntry(resRef, pkg.getName(), typeName, getResName(resRef, origKeyName), config);
ResourceEntry prevResEntry = resStorage.searchEntryWithSameName(newResEntry);
if (prevResEntry != null) {
newResEntry = newResEntry.copyWithId();

// rename also previous entry for consistency
ResourceEntry replaceForPrevEntry = prevResEntry.copyWithId();
resStorage.replace(prevResEntry, replaceForPrevEntry);
resStorage.addRename(replaceForPrevEntry);
}
if (!Objects.equals(origKeyName, newResEntry.getKeyName())) {
resStorage.addRename(newResEntry);
}
ResourceEntry ri = new ResourceEntry(resRef, pkg.getName(), typeName, keyName);
ri.setConfig(config);

if ((flags & FLAG_COMPLEX) != 0 || size == 16) {
int parentRef = is.readInt32();
int count = is.readInt32();
ri.setParentRef(parentRef);
newResEntry.setParentRef(parentRef);
List<RawNamedValue> values = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
values.add(parseValueMap());
}
ri.setNamedValues(values);
newResEntry.setNamedValues(values);
} else {
ri.setSimpleValue(parseValue());
newResEntry.setSimpleValue(parseValue());
}
resStorage.add(newResEntry);
}

private String getResName(int resRef, String origKeyName) {
String renamedKey = resStorage.getRename(resRef);
if (renamedKey != null) {
return renamedKey;
}
if (!origKeyName.isEmpty()) {
return origKeyName;
}
FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef);
if (constField != null) {
constField.add(AFlag.DONT_RENAME);
return constField.getName();
}
resStorage.add(ri);
return "RES_" + resRef; // autogenerate key name
}

private RawNamedValue parseValueMap() throws IOException {
Expand Down
2 changes: 1 addition & 1 deletion jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ private void addSimpleValue(CodeWriter cw, String typeName, String itemTag, Stri

private String getFileName(ResourceEntry ri) {
StringBuilder sb = new StringBuilder();
String qualifiers = ri.getConfig().getQualifiers();
String qualifiers = ri.getConfig();
sb.append("res/values");
if (!qualifiers.isEmpty()) {
sb.append(qualifiers);
Expand Down
57 changes: 44 additions & 13 deletions jadx-core/src/main/java/jadx/core/xmlgen/ResourceStorage.java
Original file line number Diff line number Diff line change
@@ -1,39 +1,70 @@
package jadx.core.xmlgen;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import jadx.core.xmlgen.entry.ResourceEntry;

public class ResourceStorage {
private static final Comparator<ResourceEntry> RES_ENTRY_NAME_COMPARATOR = Comparator
.comparing(ResourceEntry::getConfig)
.thenComparing(ResourceEntry::getTypeName)
.thenComparing(ResourceEntry::getKeyName);

private final List<ResourceEntry> list = new ArrayList<>();
private String appPackage;

public Collection<ResourceEntry> getResources() {
return list;
/**
* Names in one config and type must be unique
*/
private final Map<ResourceEntry, ResourceEntry> uniqNameEntries = new TreeMap<>(RES_ENTRY_NAME_COMPARATOR);

/**
* Preserve same name for same id across different configs
*/
private final Map<Integer, String> renames = new HashMap<>();

public void add(ResourceEntry resEntry) {
list.add(resEntry);
uniqNameEntries.put(resEntry, resEntry);
}

public void replace(ResourceEntry prevResEntry, ResourceEntry newResEntry) {
int idx = list.indexOf(prevResEntry);
if (idx != -1) {
list.set(idx, newResEntry);
}
// don't remove from unique names so old name stays occupied
}

public void addRename(ResourceEntry entry) {
addRename(entry.getId(), entry.getKeyName());
}

public void addRename(int id, String keyName) {
renames.put(id, keyName);
}

public void add(ResourceEntry ri) {
list.add(ri);
public String getRename(int id) {
return renames.get(id);
}

public ResourceEntry searchEntryWithSameName(ResourceEntry resourceEntry) {
return uniqNameEntries.get(resourceEntry);
}

public void finish() {
list.sort(Comparator.comparingInt(ResourceEntry::getId));
uniqNameEntries.clear();
renames.clear();
}

public ResourceEntry getByRef(int refId) {
ResourceEntry key = new ResourceEntry(refId);
int index = Collections.binarySearch(list, key, Comparator.comparingInt(ResourceEntry::getId));
if (index < 0) {
return null;
}
return list.get(index);
public Iterable<ResourceEntry> getResources() {
return list;
}

public String getAppPackage() {
Expand Down
Loading

0 comments on commit 71617a1

Please sign in to comment.