feat(gui): hex-viewer for binary asset files (#198)(PR #1969)

* implemented hex-viewer feature

* added support for opening lib (.so) files

* removed unused class

* fix formatting

* fixed error when opening an empty file

* fixed a slight inaccuracy in synchronizing highlights

* defaulted little endian to false

* synchronize hex editor with theme and use smali font

* fixed wrong displayed values

* applied code formatting

* fixed selection color in preview panel

* Changed Smali Editor font entry in settings less confusing
This commit is contained in:
Mino
2023-07-31 00:02:39 +01:00
committed by GitHub
parent fbb6aa580e
commit 0f5d07c6b1
13 changed files with 553 additions and 8 deletions
@@ -21,6 +21,7 @@ import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.core.xmlgen.ResContainer;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.codearea.BinaryContentPanel;
import jadx.gui.ui.codearea.CodeContentPanel;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.ImagePanel;
@@ -131,6 +132,12 @@ public class JResource extends JLoadableNode {
if (resFile.getType() == ResourceType.IMG) {
return new ImagePanel(tabbedPane, this);
}
if (resFile.getType() == ResourceType.LIB) {
return new BinaryContentPanel(tabbedPane, this, false);
}
if (getSyntaxByExtension(resFile.getDeobfName()) == null) {
return new BinaryContentPanel(tabbedPane, this);
}
return new CodeContentPanel(tabbedPane, this);
}
@@ -272,7 +279,6 @@ public class JResource extends JLoadableNode {
switch (type) {
case CODE:
case FONT:
case LIB:
case MEDIA:
return false;
@@ -280,6 +286,7 @@ public class JResource extends JLoadableNode {
case XML:
case ARSC:
case IMG:
case LIB:
case UNKNOWN:
return true;
}
@@ -0,0 +1,99 @@
package jadx.gui.ui.codearea;
import java.awt.BorderLayout;
import java.awt.Component;
import javax.swing.JSplitPane;
import javax.swing.JTabbedPane;
import javax.swing.border.EmptyBorder;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.LineNumbersMode;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.TabbedPane;
public class BinaryContentPanel extends AbstractCodeContentPanel {
private final transient CodePanel textCodePanel;
private final transient CodePanel hexCodePanel;
private final transient HexConfigurationPanel hexConfigurationPanel;
private final transient JTabbedPane areaTabbedPane;
public BinaryContentPanel(TabbedPane panel, JNode jnode) {
this(panel, jnode, true);
}
public BinaryContentPanel(TabbedPane panel, JNode jnode, boolean supportsText) {
super(panel, jnode);
setLayout(new BorderLayout());
setBorder(new EmptyBorder(0, 0, 0, 0));
if (supportsText) {
textCodePanel = new CodePanel(new CodeArea(this, jnode));
} else {
textCodePanel = null;
}
HexArea hexArea = new HexArea(this, jnode);
hexConfigurationPanel = new HexConfigurationPanel(hexArea.getConfiguration());
hexArea.setConfigurationPanel(hexConfigurationPanel);
hexCodePanel = new CodePanel(hexArea);
areaTabbedPane = buildTabbedPane();
add(areaTabbedPane);
getSelectedPanel().load();
}
private JTabbedPane buildTabbedPane() {
JSplitPane hexSplitPanel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, hexCodePanel, hexConfigurationPanel);
hexSplitPanel.setResizeWeight(0.8);
JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.BOTTOM);
tabbedPane.setBorder(new EmptyBorder(0, 0, 0, 0));
tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
if (textCodePanel != null) {
tabbedPane.add(textCodePanel, "Text");
}
tabbedPane.add(hexSplitPanel, "Hex");
tabbedPane.addChangeListener(e -> {
getSelectedPanel().load();
});
return tabbedPane;
}
@Override
public AbstractCodeArea getCodeArea() {
if (textCodePanel != null) {
return textCodePanel.getCodeArea();
} else {
return hexCodePanel.getCodeArea();
}
}
@Override
public void loadSettings() {
if (textCodePanel != null) {
textCodePanel.loadSettings();
}
hexCodePanel.loadSettings();
updateUI();
}
@Override
public JadxSettings getSettings() {
JadxSettings settings = super.getSettings();
settings.setLineNumbersMode(LineNumbersMode.NORMAL);
return settings;
}
private CodePanel getSelectedPanel() {
Component selectedComponent = areaTabbedPane.getSelectedComponent();
CodePanel selectedPanel;
if (selectedComponent instanceof CodePanel) {
selectedPanel = (CodePanel) selectedComponent;
} else if (selectedComponent instanceof JSplitPane) {
selectedPanel = (CodePanel) ((JSplitPane) selectedComponent).getLeftComponent();
} else {
throw new RuntimeException("tabbedPane.getSelectedComponent returned a Component "
+ "of unexpected type " + selectedComponent);
}
return selectedPanel;
}
}
@@ -0,0 +1,199 @@
package jadx.gui.ui.codearea;
import java.awt.BorderLayout;
import java.awt.Font;
import java.nio.charset.StandardCharsets;
import javax.swing.border.EmptyBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import org.apache.commons.lang3.StringUtils;
import org.fife.ui.rsyntaxtextarea.Theme;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.UiUtils;
public class HexArea extends AbstractCodeArea {
private static final Logger LOG = LoggerFactory.getLogger(HexArea.class);
private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
private final HexAreaConfiguration config;
private final JNode binaryNode;
private final HexPreviewPanel hexPreviewPanel;
private HexConfigurationPanel hexConfigurationPanel;
private byte[] bytes;
public HexArea(ContentPanel contentPanel, JNode node) {
super(contentPanel, node);
binaryNode = node;
config = new HexAreaConfiguration();
hexPreviewPanel = new HexPreviewPanel(config);
initView();
applyTheme();
}
@Override
public @NotNull ICodeInfo getCodeInfo() {
return ICodeInfo.EMPTY;
}
@Override
public void load() {
byte[] bytes = binaryNode.getCodeInfo().getCodeStr().getBytes(StandardCharsets.UTF_8);
setBytes(bytes);
if (getBytes().length > 0) {
// We set the caret after the first byte to prevent it from being highlighted
setCaretPosition(2);
} else {
setCaretPosition(0);
}
setLoaded();
}
@Override
public void refresh() {
}
@Override
public void loadSettings() {
super.loadSettings();
applyTheme();
}
private void applyTheme() {
Font font = getContentPanel().getTabbedPane().getMainWindow().getSettings().getSmaliFont();
setFont(font);
Theme theme = contentPanel.getTabbedPane().getMainWindow().getEditorTheme();
if (hexPreviewPanel != null) {
hexPreviewPanel.applyTheme(theme, font);
}
}
private void initView() {
addCaretListener(new HexCaretListener());
setLayout(new BorderLayout());
setBorder(new EmptyBorder(0, 0, 0, 0));
hexPreviewPanel.setFont(getFont());
add(hexPreviewPanel, BorderLayout.EAST);
}
private void setBytes(byte[] bytes) {
this.bytes = bytes;
String text;
if (bytes.length > 0) {
byte[] hexChars = new byte[bytes.length * 4 - 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 4] = HEX_ARRAY[v >>> 4];
hexChars[j * 4 + 1] = HEX_ARRAY[v & 0x0F];
if (j != bytes.length - 1) {
hexChars[j * 4 + 2] = ' ';
hexChars[j * 4 + 3] = (byte) ((j % config.bytesPerLine == config.bytesPerLine - 1) ? '\n' : ' ');
}
}
text = new String(hexChars, StandardCharsets.UTF_8);
} else {
text = "";
}
setText(text);
hexPreviewPanel.setBytes(bytes);
hexConfigurationPanel.setBytes(bytes);
}
public byte[] getBytes() {
return bytes;
}
public void setConfigurationPanel(HexConfigurationPanel hexConfigurationPanel) {
this.hexConfigurationPanel = hexConfigurationPanel;
}
@Override
public void copyAsStyledText() {
String text = getSelectedText();
if (text != null && !StringUtils.isEmpty(text)) {
text = text
.replace(" ", "")
.replace("\n", "");
UiUtils.copyToClipboard(text);
}
}
public HexAreaConfiguration getConfiguration() {
return config;
}
private class HexCaretListener implements CaretListener {
private boolean isListening = true;
private int previousCaretDot = -1;
@Override
public void caretUpdate(CaretEvent caretEvent) {
int dot = caretEvent.getDot();
int mark = caretEvent.getMark();
if (!isListening) {
return;
}
if (dot % 2 == 1) {
if (previousCaretDot > dot) {
if (mark == dot) {
mark--;
}
dot--;
} else {
if (mark == dot) {
mark++;
}
dot++;
}
isListening = false;
HexArea.this.setCaretPosition(mark);
HexArea.this.moveCaretPosition(dot);
isListening = true;
}
if (previousCaretDot != dot) {
onTextCursorMoved(dot, mark);
}
previousCaretDot = dot;
}
private void onTextCursorMoved(int dot, int mark) {
hexConfigurationPanel.setOffset(dot / 4);
int startIndex = Math.min(dot, mark);
int endIndex = Math.max(dot, mark);
int startOffset = startIndex / 4;
int endOffset = endIndex / 4;
if (startIndex % 4 == 2 && endIndex == startIndex + 2) {
// Highlighted an empty space
hexPreviewPanel.clearHighlights();
return;
}
if (startOffset < endOffset && startIndex % 4 == 2) {
startOffset++;
}
if (endOffset > startOffset && endIndex % 4 == 0) {
endOffset--;
}
hexPreviewPanel.highlightBytes(startOffset, endOffset);
}
}
}
@@ -0,0 +1,7 @@
package jadx.gui.ui.codearea;
public class HexAreaConfiguration {
public int bytesPerLine = 16;
public boolean littleEndian = false;
}
@@ -0,0 +1,139 @@
package jadx.gui.ui.codearea;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ItemEvent;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import org.apache.commons.lang3.ArrayUtils;
public class HexConfigurationPanel extends JPanel {
private final List<ValueFormatter> formatters = new ArrayList<>();
private final HexAreaConfiguration config;
private byte[] bytes = null;
private Integer offset = null;
private int row = 0;
public HexConfigurationPanel(HexAreaConfiguration configuration) {
this.config = configuration;
setLayout(new GridBagLayout());
addValueFormat("Signed 8 bit", 1, b -> Integer.toString(b.get()));
addValueFormat("Unsigned 8 bit", 1, b -> Integer.toString(b.get() & 0xFF));
addValueFormat("Signed 16 bit", 2, b -> Short.toString(b.getShort()));
addValueFormat("Unsigned 16 bit", 2, b -> Integer.toString(b.getShort() & 0xFFFF));
addValueFormat("Float 32 bit", 4, b -> Float.toString(b.getFloat()));
addValueFormat("Signed 32 bit", 4, b -> Integer.toString(b.getInt()));
addValueFormat("Unsigned 32 bit", 4, b -> Integer.toUnsignedString(b.getInt()));
addValueFormat("Signed 64 bit", 8, b -> Long.toString(b.getLong()));
addValueFormat("Float 64 bit", 8, b -> Double.toString(b.getDouble()));
addValueFormat("Unsigned 64 bit", 8, b -> Long.toUnsignedString(b.getLong()));
addValueFormat("Hexadecimal", 1, b -> Integer.toString(b.get(), 16));
addValueFormat("Octal", 1, b -> Integer.toString(b.get(), 8));
addValueFormat("Binary", 1, b -> Integer.toString(b.get(), 2));
GridBagConstraints constraints;
constraints = getConstraints();
constraints.gridwidth = 2;
JCheckBox littleEndianCheckBox = new JCheckBox("Little endian", false);
littleEndianCheckBox.addItemListener(ev -> {
config.littleEndian = ev.getStateChange() == ItemEvent.SELECTED;
reloadOffset();
});
add(littleEndianCheckBox, constraints);
// Workaround to force widgets to start from the top (otherwise centered)
constraints = getConstraints();
constraints.weighty = 1;
add(new JLabel(" "), constraints);
}
public void setOffset(int offset) {
this.offset = offset;
reloadOffset();
}
public void setBytes(byte[] bytes) {
this.bytes = bytes;
}
private void reloadOffset() {
if (bytes == null || offset == null) {
return;
}
for (int i = 0; i < formatters.size(); i++) {
ValueFormatter formatter = formatters.get(i);
if (canDisplay(offset, formatter.dataSize)) {
ByteBuffer buffer = decodeByteArray(offset, formatter.dataSize);
String value = formatter.function.apply(buffer);
((JTextField) getComponent(i * 2 + 1)).setText(value);
}
}
}
private GridBagConstraints getConstraints() {
GridBagConstraints constraints = new GridBagConstraints();
constraints.insets = new Insets(5, 5, 5, 5);
constraints.gridy = row;
row++;
return constraints;
}
private void addValueFormat(String name, int dataSize, Function<ByteBuffer, String> formatter) {
formatters.add(new ValueFormatter(dataSize, formatter));
GridBagConstraints constraints = getConstraints();
constraints.gridx = 0;
constraints.anchor = GridBagConstraints.WEST;
add(new JLabel(name), constraints);
constraints.fill = GridBagConstraints.HORIZONTAL;
constraints.gridx = 1;
JTextField textField = new JTextField();
textField.setEditable(false);
add(textField, constraints);
}
private boolean canDisplay(int offset, int size) {
return offset + size <= bytes.length;
}
private ByteBuffer decodeByteArray(int offset, int size) {
byte[] chunk = sliceBytes(offset, size);
if (config.littleEndian) {
ArrayUtils.reverse(chunk);
}
return ByteBuffer.wrap(chunk);
}
private byte[] sliceBytes(int offset, int size) {
byte[] slice = new byte[size];
System.arraycopy(bytes, offset, slice, 0, size);
return slice;
}
private static class ValueFormatter {
public final int dataSize;
public final Function<ByteBuffer, String> function;
public ValueFormatter(int dataSize, Function<ByteBuffer, String> function) {
this.dataSize = dataSize;
this.function = function;
}
}
}
@@ -0,0 +1,94 @@
package jadx.gui.ui.codearea;
import java.awt.Color;
import java.awt.Font;
import javax.swing.JTextArea;
import javax.swing.border.MatteBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.Highlighter;
import org.fife.ui.rsyntaxtextarea.SyntaxScheme;
import org.fife.ui.rsyntaxtextarea.Theme;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HexPreviewPanel extends JTextArea {
private static final Logger LOG = LoggerFactory.getLogger(HexPreviewPanel.class);
private final HexAreaConfiguration config;
private byte[] bytes = new byte[0];
private Color highlightColor = Color.YELLOW;
private boolean hasHighlight = false;
public HexPreviewPanel(HexAreaConfiguration configuration) {
super(0, configuration.bytesPerLine);
this.config = configuration;
initView();
}
public void setBytes(byte[] bytes) {
this.bytes = bytes;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
char c = (char) bytes[i];
if (c <= 0x1f || (c & (1 << 7)) != 0) {
sb.append('.');
} else {
sb.append(c);
}
if (i != bytes.length - 1 && i % config.bytesPerLine == config.bytesPerLine - 1) {
sb.append('\n');
}
}
setText(sb.toString());
}
public void clearHighlights() {
hasHighlight = false;
getHighlighter().removeAllHighlights();
}
public void highlightBytes(int startOffset, int endOffset) {
if (hasHighlight) {
getHighlighter().removeAllHighlights();
}
// Include line breaks in the index
startOffset += startOffset / config.bytesPerLine;
endOffset += endOffset / config.bytesPerLine;
Highlighter.HighlightPainter painter = new DefaultHighlighter.DefaultHighlightPainter(highlightColor);
try {
getHighlighter().addHighlight(startOffset, endOffset + 1, painter);
} catch (BadLocationException e) {
LOG.error("Unable to highlight bytes " + startOffset + ":" + endOffset, e);
}
hasHighlight = true;
}
public void setHighlightColor(Color highlightColor) {
this.highlightColor = highlightColor;
}
public void setBorderColor(Color borderColor) {
setBorder(new MatteBorder(0, 2, 0, 0, borderColor));
}
public void applyTheme(Theme theme, Font font) {
setBackground(theme.bgColor);
setHighlightColor(theme.selectionBG);
setBorderColor(theme.gutterBorderColor);
setDisabledTextColor(theme.scheme.getStyle(SyntaxScheme.IDENTIFIER).foreground);
setFont(font);
}
private void initView() {
setEnabled(false);
setEditable(false);
}
}
@@ -199,7 +199,7 @@ preferences.cfg=CFG-Grafiken für Methoden generieren (im 'dot'-Format)
preferences.raw_cfg=RAW CFG-Grafiken generieren
#preferences.integerFormat=Integer format
preferences.font=Schrift ändern
preferences.smali_font=Schrifteditor
#preferences.smali_font=
preferences.laf_theme=Thema
preferences.theme=Thema ändern
preferences.start_jobs=Autom. Hintergrunddekompilierung starten
@@ -199,7 +199,7 @@ preferences.cfg=Generate methods CFG graphs (in 'dot' format)
preferences.raw_cfg=Generate RAW CFG graphs
preferences.integerFormat=Integer format
preferences.font=Editor font
preferences.smali_font=Smali Editor font
preferences.smali_font=Monospaced font (Smali/Hex)
preferences.laf_theme=Theme
preferences.theme=Editor theme
preferences.start_jobs=Auto start background decompilation
@@ -199,7 +199,7 @@ preferences.cfg=메소드 CFG 그래프 생성 ('dot' 포맷)
preferences.raw_cfg=RAW CFG 그래프 생성
#preferences.integerFormat=Integer format
preferences.font=에디터 글씨체
preferences.smali_font=Smali 에디터 글씨체
#preferences.smali_font=
preferences.laf_theme=테마
preferences.theme=에디터 테마
preferences.start_jobs=백그라운드에서 디컴파일 자동 시작
@@ -199,7 +199,7 @@ preferences.cfg=Gera gráficos de métodos CFG no formato de pontos ('dot')
preferences.raw_cfg=Gera gráficos CFG no formato RAW
#preferences.integerFormat=Integer format
preferences.font=Fonte do editor
preferences.smali_font=Fonte do editor de smali
#preferences.smali_font=
preferences.laf_theme=Tema
preferences.theme=Tema do editor
preferences.start_jobs=Inicializar descompilação automaticamente em segundo-plano
@@ -199,7 +199,7 @@ preferences.cfg=Методы генерации графиков CFG (в "dot"
preferences.raw_cfg=Генерировать необработанные графики CFG
#preferences.integerFormat=Integer format
preferences.font=Шрифт редактора Java
preferences.smali_font=Шрифт редактора smali
#preferences.smali_font=
preferences.laf_theme=Тема приложения
preferences.theme=Тема редактора
preferences.start_jobs=Автоматическая декомпиляция
@@ -199,7 +199,7 @@ preferences.cfg=生成方法的 CFG 图('.dot'
preferences.raw_cfg=生成原始的 CFG 图
preferences.integerFormat=数值格式化
preferences.font=编辑器字体
preferences.smali_font=Smali编辑器字体
#preferences.smali_font=
preferences.laf_theme=主题
preferences.theme=编辑器主题
preferences.start_jobs=自动进行后台反编译
@@ -199,7 +199,7 @@ preferences.cfg=產生方法 CFG 圖表 ('dot' 格式)
preferences.raw_cfg=產生 RAW CFG 圖表
preferences.integerFormat=整數模式
preferences.font=編輯器字型
preferences.smali_font=Smali 編輯器字型
#preferences.smali_font=
preferences.laf_theme=主題
preferences.theme=編輯器主題
preferences.start_jobs=自動開始背景反編譯