Files
jadx/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java
T
Yaroslav e19a456642 feat(gui): added support view and open more resource files (PR #2483)
* feat(gui): added support view and open more resource files

* fix(gui): remove svg extension from resource types

* fix(gui): use error dialog from common utils

* fix: reformat code
2025-05-15 18:50:19 +01:00

404 lines
10 KiB
Java

package jadx.gui.treemodel;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JPopupMenu;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.Nullable;
import jadx.api.ICodeInfo;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.api.ResourcesLoader;
import jadx.api.impl.SimpleCodeInfo;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.core.xmlgen.ResContainer;
import jadx.gui.jobs.SimpleTask;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.BinaryContentPanel;
import jadx.gui.ui.codearea.CodeContentPanel;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.FontPanel;
import jadx.gui.ui.panel.ImagePanel;
import jadx.gui.ui.popupmenu.JResourcePopupMenu;
import jadx.gui.ui.tab.TabbedPane;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.res.ResTableHelper;
public class JResource extends JLoadableNode {
private static final long serialVersionUID = -201018424302612434L;
private static final ImageIcon ROOT_ICON = UiUtils.openSvgIcon("nodes/resourcesRoot");
private static final ImageIcon ARSC_ICON = UiUtils.openSvgIcon("nodes/resourceBundle");
private static final ImageIcon XML_ICON = UiUtils.openSvgIcon("nodes/xml");
private static final ImageIcon IMAGE_ICON = UiUtils.openSvgIcon("nodes/ImagesFileType");
private static final ImageIcon SO_ICON = UiUtils.openSvgIcon("nodes/binaryFile");
private static final ImageIcon MANIFEST_ICON = UiUtils.openSvgIcon("nodes/manifest");
private static final ImageIcon JAVA_ICON = UiUtils.openSvgIcon("nodes/java");
private static final ImageIcon APK_ICON = UiUtils.openSvgIcon("nodes/archiveApk");
private static final ImageIcon AUDIO_ICON = UiUtils.openSvgIcon("nodes/audioFile");
private static final ImageIcon VIDEO_ICON = UiUtils.openSvgIcon("nodes/videoFile");
private static final ImageIcon FONT_ICON = UiUtils.openSvgIcon("nodes/fontFile");
private static final ImageIcon HTML_ICON = UiUtils.openSvgIcon("nodes/html");
private static final ImageIcon JSON_ICON = UiUtils.openSvgIcon("nodes/json");
private static final ImageIcon TEXT_ICON = UiUtils.openSvgIcon("nodes/text");
private static final ImageIcon ARCHIVE_ICON = UiUtils.openSvgIcon("nodes/archive");
private static final ImageIcon UNKNOWN_ICON = UiUtils.openSvgIcon("nodes/unknown");
public static final Comparator<JResource> RESOURCES_COMPARATOR =
Comparator.<JResource>comparingInt(r -> r.type.ordinal())
.thenComparing(JResource::getName, String.CASE_INSENSITIVE_ORDER);
public enum JResType {
ROOT,
DIR,
FILE
}
private final transient String name;
private final transient String shortName;
private final transient JResType type;
private final transient ResourceFile resFile;
private transient volatile boolean loaded;
private transient List<JResource> subNodes = Collections.emptyList();
private transient ICodeInfo content = ICodeInfo.EMPTY;
public JResource(ResourceFile resFile, String name, JResType type) {
this(resFile, name, name, type);
}
public JResource(ResourceFile resFile, String name, String shortName, JResType type) {
this.resFile = resFile;
this.name = name;
this.shortName = shortName;
this.type = type;
this.loaded = false;
}
public synchronized void update() {
removeAllChildren();
if (Utils.isEmpty(subNodes)) {
if (type == JResType.DIR || type == JResType.ROOT
|| resFile.getType() == ResourceType.ARSC) {
// fake leaf to force show expand button
// real sub nodes will load on expand in loadNode() method
add(new TextNode(NLS.str("tree.loading")));
}
} else {
for (JResource res : subNodes) {
res.update();
add(res);
}
if (type != JResType.FILE) {
// no content, nothing to load
loaded = true;
}
}
}
@Override
public synchronized void loadNode() {
getCodeInfo();
update();
}
@Override
public synchronized SimpleTask getLoadTask() {
if (loaded) {
return null;
}
return new SimpleTask(NLS.str("progress.load"), this::getCodeInfo, this::update);
}
@Override
public String getName() {
return name;
}
public JResType getType() {
return type;
}
public List<JResource> getSubNodes() {
return subNodes;
}
public void addSubNode(JResource node) {
subNodes = ListUtils.safeAdd(subNodes, node);
}
public void sortSubNodes() {
sortResNodes(subNodes);
}
private static void sortResNodes(List<JResource> nodes) {
if (Utils.notEmpty(nodes)) {
nodes.forEach(JResource::sortSubNodes);
nodes.sort(RESOURCES_COMPARATOR);
}
}
@Override
public @Nullable ContentPanel getContentPanel(TabbedPane tabbedPane) {
if (resFile == null) {
return null;
}
if (resFile.getType() == ResourceType.IMG) {
return new ImagePanel(tabbedPane, this);
}
if (resFile.getType() == ResourceType.LIB) {
return new BinaryContentPanel(tabbedPane, this, false);
}
if (resFile.getType() == ResourceType.FONT) {
return new FontPanel(tabbedPane, this);
}
if (getSyntaxByExtension(resFile.getDeobfName()) == null) {
return new BinaryContentPanel(tabbedPane, this);
}
return new CodeContentPanel(tabbedPane, this);
}
@Override
public JPopupMenu onTreePopupMenu(MainWindow mainWindow) {
return new JResourcePopupMenu(mainWindow, this);
}
@Override
public synchronized ICodeInfo getCodeInfo() {
if (loaded) {
return content;
}
ICodeInfo codeInfo = loadContent();
content = codeInfo;
loaded = true;
return codeInfo;
}
private ICodeInfo loadContent() {
if (resFile == null || type != JResType.FILE) {
return ICodeInfo.EMPTY;
}
if (!isSupportedForView(resFile.getType())) {
return ICodeInfo.EMPTY;
}
ResContainer rc = resFile.loadContent();
if (rc == null) {
return ICodeInfo.EMPTY;
}
if (rc.getDataType() == ResContainer.DataType.RES_TABLE) {
ICodeInfo codeInfo = loadCurrentSingleRes(rc);
List<JResource> nodes = ResTableHelper.buildTree(rc);
sortResNodes(nodes);
subNodes = nodes;
return codeInfo;
}
// single node
return loadCurrentSingleRes(rc);
}
private ICodeInfo loadCurrentSingleRes(ResContainer rc) {
switch (rc.getDataType()) {
case TEXT:
case RES_TABLE:
return rc.getText();
case RES_LINK:
try {
return ResourcesLoader.decodeStream(rc.getResLink(), (size, is) -> {
if (size > 10 * 1024 * 1024L) {
return new SimpleCodeInfo("File too large for view");
}
return ResourcesLoader.loadToCodeWriter(is);
});
} catch (Exception e) {
return new SimpleCodeInfo("Failed to load resource file:\n" + Utils.getStackTrace(e));
}
case DECODED_DATA:
default:
return new SimpleCodeInfo("Unexpected resource type: " + rc);
}
}
@Override
public String getSyntaxName() {
if (resFile == null) {
return null;
}
switch (resFile.getType()) {
case CODE:
return super.getSyntaxName();
case MANIFEST:
case XML:
case ARSC:
return SyntaxConstants.SYNTAX_STYLE_XML;
default:
String syntax = getSyntaxByExtension(resFile.getDeobfName());
if (syntax != null) {
return syntax;
}
return super.getSyntaxName();
}
}
private static final Map<String, String> EXTENSION_TO_FILE_SYNTAX = jadx.core.utils.Utils.newConstStringMap(
"java", SyntaxConstants.SYNTAX_STYLE_JAVA,
"js", SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT,
"ts", SyntaxConstants.SYNTAX_STYLE_TYPESCRIPT,
"json", SyntaxConstants.SYNTAX_STYLE_JSON,
"css", SyntaxConstants.SYNTAX_STYLE_CSS,
"less", SyntaxConstants.SYNTAX_STYLE_LESS,
"html", SyntaxConstants.SYNTAX_STYLE_HTML,
"xml", SyntaxConstants.SYNTAX_STYLE_XML,
"yaml", SyntaxConstants.SYNTAX_STYLE_YAML,
"properties", SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE,
"ini", SyntaxConstants.SYNTAX_STYLE_INI,
"sql", SyntaxConstants.SYNTAX_STYLE_SQL);
private String getSyntaxByExtension(String name) {
int dot = name.lastIndexOf('.');
if (dot == -1) {
return null;
}
String ext = name.substring(dot + 1);
return EXTENSION_TO_FILE_SYNTAX.get(ext);
}
@Override
public Icon getIcon() {
switch (type) {
case ROOT:
return ROOT_ICON;
case DIR:
return Icons.FOLDER;
case FILE:
ResourceType resType = resFile.getType();
switch (resType) {
case MANIFEST:
return MANIFEST_ICON;
case ARSC:
return ARSC_ICON;
case XML:
return XML_ICON;
case IMG:
return IMAGE_ICON;
case LIB:
return SO_ICON;
case CODE:
return JAVA_ICON;
case APK:
return APK_ICON;
case VIDEOS:
return VIDEO_ICON;
case SOUNDS:
return AUDIO_ICON;
case FONT:
return FONT_ICON;
case HTML:
return HTML_ICON;
case JSON:
return JSON_ICON;
case TEXT:
return TEXT_ICON;
case ARCHIVE:
return ARCHIVE_ICON;
case UNKNOWN:
return UNKNOWN_ICON;
}
return UNKNOWN_ICON;
}
return Icons.FILE;
}
public static boolean isSupportedForView(ResourceType type) {
switch (type) {
case CODE:
case SOUNDS:
case VIDEOS:
case ARCHIVE:
case APK:
return false;
case MANIFEST:
case XML:
case ARSC:
case IMG:
case LIB:
case FONT:
case TEXT:
case JSON:
case HTML:
case UNKNOWN:
return true;
}
return true;
}
public static boolean isOpenInExternalTool(ResourceType type) {
switch (type) {
case SOUNDS:
case VIDEOS:
return true;
default:
return false;
}
}
public ResourceFile getResFile() {
return resFile;
}
@Override
public JClass getJParent() {
return null;
}
@Override
public String getID() {
if (type == JResType.ROOT) {
return "JResources";
}
return makeString();
}
@Override
public String makeString() {
return shortName;
}
@Override
public String makeLongString() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
JResource other = (JResource) o;
return name.equals(other.name) && type.equals(other.type);
}
@Override
public int hashCode() {
return name.hashCode() + 31 * type.ordinal();
}
}