Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes duplicates entries in XML resources #995

Merged
merged 6 commits into from
Oct 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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