From cdd5bf536d9f3c5ae545c63f80e6252d4140592c Mon Sep 17 00:00:00 2001 From: Skylot <118523+skylot@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:01:03 +0000 Subject: [PATCH] fix(xapk): support files in sub-dirs in xapk (#2834) --- .../main/java/jadx/gui/treemodel/JRoot.java | 12 ++---- .../plugins/input/xapk/XApkCustomInput.java | 15 +++++++- .../jadx/plugins/input/xapk/XApkLoader.java | 37 ++++++++++++------- 3 files changed, 40 insertions(+), 24 deletions(-) diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java index bec6656a9..cdf0e9cc7 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java @@ -1,6 +1,5 @@ package jadx.gui.treemodel; -import java.io.File; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; @@ -27,6 +26,7 @@ public class JRoot extends JNode { private static final long serialVersionUID = 8888495789773527342L; private static final ImageIcon ROOT_ICON = UiUtils.openSvgIcon("nodes/rootPackageFolder"); + private static final Pattern SPLIT_PATH_PATTERN = Pattern.compile("[/\\\\]+"); private final transient JadxWrapper wrapper; private final transient MainWindow mainWindow; @@ -56,15 +56,9 @@ public class JRoot extends JNode { private JResource getHierarchyResources(List resources) { JResource root = new JResource(null, NLS.str("tree.resources_title"), JResType.ROOT); - String splitPathStr = Pattern.quote(File.separator); for (ResourceFile rf : resources) { - String rfName; - if (rf.getZipEntry() != null) { - rfName = rf.getDeobfName(); - } else { - rfName = new File(rf.getDeobfName()).getName(); - } - String[] parts = new File(rfName).getPath().split(splitPathStr); + String rfName = rf.getDeobfName(); + String[] parts = SPLIT_PATH_PATTERN.split(rfName); JResource curRf = root; int count = parts.length; for (int i = 0; i < count - 1; i++) { diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkCustomInput.java b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkCustomInput.java index a5eb315fb..eb421f630 100644 --- a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkCustomInput.java +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkCustomInput.java @@ -7,12 +7,14 @@ import java.util.ArrayList; import java.util.List; import jadx.api.ResourceFile; +import jadx.api.ResourceType; import jadx.api.ResourcesLoader; import jadx.api.plugins.CustomResourcesLoader; import jadx.api.plugins.JadxPluginContext; import jadx.api.plugins.input.ICodeLoader; import jadx.api.plugins.input.JadxCodeInput; import jadx.api.plugins.input.data.impl.EmptyCodeLoader; +import jadx.core.utils.files.FileUtils; import jadx.plugins.input.dex.DexInputPlugin; import jadx.plugins.input.xapk.data.XApkData; @@ -51,7 +53,18 @@ public class XApkCustomInput implements JadxCodeInput, CustomResourcesLoader { resLoader.defaultLoadFile(list, apkPath.toFile(), apkPath.getFileName() + "/"); } for (Path filePath : xApkData.getFiles()) { - resLoader.defaultLoadFile(list, filePath.toFile(), ""); + File innerFile = filePath.toFile(); + String relativePath = xApkData.getTmpDir().relativize(filePath).toString(); + if (FileUtils.isZipFile(innerFile)) { + // zip will be unpacked by default loader + resLoader.defaultLoadFile(list, innerFile, relativePath + "/"); + } else { + // create resource to file in tmp folder, but with name relative to xapk root + ResourceType type = ResourceType.getFileType(relativePath); + ResourceFile resFile = ResourceFile.createResourceFile(context.getDecompiler(), innerFile, type); + resFile.setDeobfName(relativePath); + list.add(resFile); + } } return true; } diff --git a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkLoader.java b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkLoader.java index 965efd110..5b796cff4 100644 --- a/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkLoader.java +++ b/jadx-plugins/jadx-xapk-input/src/main/java/jadx/plugins/input/xapk/XApkLoader.java @@ -75,25 +75,34 @@ public class XApkLoader { } } - private XApkData unpackXApk(File xapkFile, XApkManifest xApkManifest, ZipContent content) throws IOException { - Set declaredApks = xApkManifest.getSplitApks().stream() - .map(SplitApk::getFile).collect(Collectors.toSet()); + private XApkData unpackXApk(File xapkFile, XApkManifest xApkManifest, ZipContent content) { + Set declaredApks = xApkManifest.getSplitApks() + .stream() + .map(SplitApk::getFile) + .collect(Collectors.toSet()); List apks = new ArrayList<>(declaredApks.size()); - List files = new ArrayList<>(); - - String dirName = xapkFile.getName() + '_' + System.currentTimeMillis(); + List files = new ArrayList<>(content.getEntries().size()); + String dirName = FileUtils.md5Sum(xapkFile.getAbsolutePath()); Path tmpDir = context.files().getPluginTempDir().resolve(dirName); FileUtils.makeDirs(tmpDir); for (IZipEntry entry : content.getEntries()) { - String fileName = entry.getName(); - Path file = tmpDir.resolve(fileName); - try (InputStream inputStream = entry.getInputStream()) { - Files.copy(inputStream, file, StandardCopyOption.REPLACE_EXISTING); + if (entry.isDirectory()) { + continue; } - if (declaredApks.contains(fileName)) { - apks.add(file); - } else { - files.add(file); + try { + String fileName = entry.getName(); + Path file = tmpDir.resolve(fileName); + FileUtils.makeDirsForFile(file); + try (InputStream inputStream = entry.getInputStream()) { + Files.copy(inputStream, file, StandardCopyOption.REPLACE_EXISTING); + } + if (declaredApks.contains(fileName)) { + apks.add(file); + } else { + files.add(file); + } + } catch (Exception e) { + LOG.error("Failed to unpack XApk entry: {}", entry.getName(), e); } } return new XApkData(xApkManifest, tmpDir, apks, files);