diff --git a/README.md b/README.md
index 1cf6658..e58d0da 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ This is a (WIP) simple extension to open .rpx and .rpl files with Ghidra.
Install the extension by using the `Install Extensions` option inside Ghidra or extracting the .zip manually into `[GHIDRA_ROOT]\Ghidra\Extensions`. Make sure to restart the program after installing.
-Once the extension is installed, you can open a .rpx/.rpl via `File->Open File System...`. After opening the file, you should a filetree with a `converted.elf`, right click on it and select `Import`, confirm by pressing `Single file`.
+Once the extension is installed, you import a .rpx/.rpl via `File->Import...`.
# Building
diff --git a/data/languages/RPX.opinion b/data/languages/RPX.opinion
new file mode 100644
index 0000000..e7bdf04
--- /dev/null
+++ b/data/languages/RPX.opinion
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/main/java/de/mas/ghidra/utils/Utils.java b/src/main/java/de/mas/ghidra/utils/Utils.java
new file mode 100644
index 0000000..e3d57d7
--- /dev/null
+++ b/src/main/java/de/mas/ghidra/utils/Utils.java
@@ -0,0 +1,27 @@
+package de.mas.ghidra.utils;
+
+import java.nio.ByteBuffer;
+
+public class Utils {
+ private Utils() {
+ }
+
+ /**
+ * Grows a ByteBuffer if needed.
+ *
+ * @param buffer the original buffer
+ * @param size the needed size.
+ * @return A byte buffer with the expected size. If the buffer was big enough,
+ * the original buffer will be returned, otherwise a new one will be
+ * created.
+ */
+ public static ByteBuffer checkAndGrowByteBuffer(ByteBuffer buffer, long size) {
+ // This probably the worst way to do this.
+ if (buffer.remaining() < size) {
+ ByteBuffer newBuffer = ByteBuffer.allocate((int) (buffer.capacity() + size - buffer.remaining()));
+ newBuffer.put(buffer.array());
+ return newBuffer;
+ }
+ return buffer;
+ }
+}
diff --git a/src/main/java/de/mas/ghidra/wiiu/RPXUtils.java b/src/main/java/de/mas/ghidra/wiiu/RPXUtils.java
new file mode 100644
index 0000000..eebc6d3
--- /dev/null
+++ b/src/main/java/de/mas/ghidra/wiiu/RPXUtils.java
@@ -0,0 +1,155 @@
+package de.mas.ghidra.wiiu;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.zip.DataFormatException;
+import java.util.zip.Inflater;
+
+import de.mas.ghidra.utils.Utils;
+import generic.continues.RethrowContinuesFactory;
+import ghidra.app.util.bin.ByteProvider;
+import ghidra.app.util.bin.format.elf.ElfConstants;
+import ghidra.app.util.bin.format.elf.ElfException;
+import ghidra.app.util.bin.format.elf.ElfHeader;
+import ghidra.app.util.bin.format.elf.ElfSectionHeader;
+import ghidra.app.util.bin.format.elf.ElfSectionHeaderConstants;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class RPXUtils {
+ private static byte[] RPX_MAGIC = new byte[] { 0x7F, 0x45, 0x4C, 0x46, 0x01, 0x02, 0x01, (byte) 0xCA, (byte) 0xFE };
+ public static final int SHF_RPL_ZLIB = 0x08000000;
+ public static final int SHT_NOBITS = 0x00000008;
+
+ public static final int SHT_RPL_EXPORTS = 0x80000001;
+ public static final int SHT_RPL_IMPORTS = 0x80000002;
+ public static final int SHT_RPL_CRCS = 0x80000003;
+ public static final int SHT_RPL_FILEINFO = 0x80000004;
+
+ public static byte[] convertRPX(ByteProvider bProvider, TaskMonitor monitor)
+ throws ElfException, IOException, CancelledException, DataFormatException {
+ ElfHeader elfFile = ElfHeader.createElfHeader(RethrowContinuesFactory.INSTANCE, bProvider);
+ elfFile.parse();
+
+ ByteBuffer buffer = ByteBuffer.allocate(0);
+
+ long shdr_elf_offset = elfFile.e_ehsize() & 0xFFFFFFFF;
+ long shdr_data_elf_offset = shdr_elf_offset + elfFile.e_shnum() * elfFile.e_shentsize();
+
+ for (ElfSectionHeader h : elfFile.getSections()) {
+ monitor.checkCanceled();
+ long curSize = h.getSize();
+ long flags = h.getFlags();
+ long offset = h.getOffset();
+
+ if (offset != 0) {
+ if ((flags & SHT_NOBITS) != SHT_NOBITS) {
+ byte[] data = h.getData();
+
+ if ((flags & SHF_RPL_ZLIB) == SHF_RPL_ZLIB) {
+ monitor.setMessage("Decompressing section " + h.getTypeAsString());
+ long section_size_inflated = ByteBuffer.wrap(Arrays.copyOf(data, 4)).getInt() & 0xFFFFFFFF;
+ Inflater inflater = new Inflater();
+ inflater.setInput(data, 4, (int) h.getSize() - 4); // the first byte is the size
+
+ byte[] decompressed = new byte[(int) section_size_inflated];
+
+ inflater.inflate(decompressed);
+
+ inflater.end();
+
+ // Is this alignment really necessary?
+ curSize = (section_size_inflated + 0x03) & ~0x3;
+ flags &= ~SHF_RPL_ZLIB;
+ data = decompressed;
+ }
+ long newEnd = shdr_data_elf_offset + curSize;
+
+ buffer = Utils.checkAndGrowByteBuffer(buffer, newEnd);
+ buffer.position((int) shdr_data_elf_offset);
+ // System.out.println("Write data " + String.format("%08X",
+ // shdr_data_elf_offset));
+ buffer.put(data);
+ offset = shdr_data_elf_offset;
+ shdr_data_elf_offset += curSize;
+ }
+ }
+
+ // Hacky way to fix import relocations
+ if (h.getType() == ElfSectionHeaderConstants.SHT_SYMTAB) {
+ monitor.setMessage("Fix import relocations " + h.getTypeAsString());
+ int symbolCount = (int) ((int) (curSize) / h.getEntrySize());
+ long entryPos = 0;
+ for (int i = 0; i < symbolCount; i++) {
+ monitor.checkCanceled();
+ long test_offset = (int) (offset + entryPos + 4);
+ buffer.position((int) test_offset);
+ int val = buffer.getInt();
+
+ if ((val & 0xF0000000L) == 0xC0000000L) {
+ long fixedAddress = val - 0xC0000000L + 0x01000000L;
+ buffer.position((int) test_offset);
+ buffer.putInt((int) fixedAddress);
+ }
+ entryPos += h.getEntrySize();
+ }
+ }
+
+ buffer = Utils.checkAndGrowByteBuffer(buffer, shdr_elf_offset + 0x28);
+
+ monitor.setMessage("Converting section " + h.getTypeAsString());
+
+ buffer.position((int) shdr_elf_offset);
+ System.out.println("Write header " + String.format("%08X", shdr_elf_offset));
+ buffer.putInt(h.getName());
+ if (h.getType() == SHT_RPL_CRCS || h.getType() == SHT_RPL_FILEINFO || h.getType() == SHT_RPL_EXPORTS
+ || h.getType() == SHT_RPL_IMPORTS) {
+ buffer.putInt(ElfSectionHeaderConstants.SHT_NULL);
+ } else {
+ buffer.putInt(h.getType());
+ }
+ buffer.putInt((int) flags);
+
+ // Hacky way to fix import relocations
+ if ((h.getAddress() & 0xF0000000L) == 0xC0000000L) {
+ long fixedAddress = h.getAddress() - 0xC0000000L + 0x01000000L;
+ buffer.putInt((int) fixedAddress);
+ } else {
+ buffer.putInt((int) h.getAddress());
+ }
+
+ buffer.putInt((int) offset);
+ buffer.putInt((int) curSize);
+ buffer.putInt(h.getLink());
+ buffer.putInt(h.getInfo());
+ buffer.putInt((int) h.getAddressAlignment());
+ buffer.putInt((int) h.getEntrySize());
+
+ shdr_elf_offset += 0x28;
+ }
+
+ monitor.setMessage("Create new ELF header");
+
+ buffer = Utils.checkAndGrowByteBuffer(buffer, 36);
+
+ buffer.position(0);
+ buffer.put(RPX_MAGIC);
+ buffer.position(0x10);
+ buffer.putShort(ElfConstants.ET_EXEC); // e.e_type());
+ buffer.putShort(elfFile.e_machine());
+ buffer.putInt(elfFile.e_version());
+ buffer.putInt((int) elfFile.e_entry());
+ buffer.putInt((int) elfFile.e_phoff());
+ buffer.putInt(elfFile.e_ehsize()); // e.e_shoff());
+ buffer.putInt(elfFile.e_flags());
+ buffer.putShort(elfFile.e_ehsize());
+ buffer.putShort(elfFile.e_phentsize());
+ buffer.putShort(elfFile.e_phnum());
+ buffer.putShort(elfFile.e_shentsize());
+ buffer.putShort(elfFile.e_shnum());
+ buffer.putShort(elfFile.e_shstrndx());
+
+ return buffer.array();
+ }
+}
diff --git a/src/main/java/ghidra/app/util/opinion/RPXLoader.java b/src/main/java/ghidra/app/util/opinion/RPXLoader.java
new file mode 100644
index 0000000..4828c6f
--- /dev/null
+++ b/src/main/java/ghidra/app/util/opinion/RPXLoader.java
@@ -0,0 +1,79 @@
+package ghidra.app.util.opinion;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.zip.DataFormatException;
+
+import de.mas.ghidra.wiiu.RPXUtils;
+import generic.continues.GenericFactory;
+import ghidra.app.util.Option;
+import ghidra.app.util.bin.ByteArrayProvider;
+import ghidra.app.util.bin.ByteProvider;
+import ghidra.app.util.bin.format.elf.ElfException;
+import ghidra.app.util.bin.format.elf.ElfHeader;
+import ghidra.app.util.importer.MemoryConflictHandler;
+import ghidra.app.util.importer.MessageLog;
+import ghidra.app.util.importer.MessageLogContinuesFactory;
+import ghidra.program.model.listing.Program;
+import ghidra.util.exception.CancelledException;
+import ghidra.util.task.TaskMonitor;
+
+public class RPXLoader extends ElfLoader {
+ @Override
+ public Collection findSupportedLoadSpecs(ByteProvider provider) throws IOException {
+ byte[] header = new byte[] { 0x7F, 0x45, 0x4C, 0x46, 0x01, 0x02, 0x01, (byte) 0xCA, (byte) 0xFE };
+ List loadSpecs = new ArrayList<>();
+
+ if (Arrays.equals(provider.readBytes(0, header.length), header)) {
+ List results = QueryOpinionService.query(getName(), "0", null);
+
+ for (QueryResult result : results) {
+ loadSpecs.add(new LoadSpec(this, 0, result));
+ }
+ if (loadSpecs.isEmpty()) {
+ loadSpecs.add(new LoadSpec(this, 0, true));
+ }
+ return loadSpecs;
+
+ }
+ return loadSpecs;
+ }
+
+ @Override
+ public String getName() {
+ return "Wii U Executable (RPX/RPL)";
+ }
+
+ @Override
+ public LoaderTier getTier() {
+ return LoaderTier.SPECIALIZED_TARGET_LOADER;
+ }
+
+ @Override
+ public int getTierPriority() {
+ return 0;
+ }
+
+ @Override
+ public void load(ByteProvider provider, LoadSpec loadSpec, List