diff --git a/jadx-core/src/main/java/jadx/api/plugins/events/types/ReloadProject.java b/jadx-core/src/main/java/jadx/api/plugins/events/types/ReloadProject.java index 79c9e91e7..bcde16d09 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/events/types/ReloadProject.java +++ b/jadx-core/src/main/java/jadx/api/plugins/events/types/ReloadProject.java @@ -6,7 +6,7 @@ import jadx.api.plugins.events.JadxEvents; public class ReloadProject implements IJadxEvent { - public static final ReloadProject INSTANCE = new ReloadProject(); + public static final ReloadProject EVENT = new ReloadProject(); private ReloadProject() { // singleton diff --git a/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java b/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java index a00c4a146..b50b7774e 100644 --- a/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java +++ b/jadx-gui/src/main/java/jadx/gui/cache/code/disk/DiskCodeCache.java @@ -1,26 +1,21 @@ package jadx.gui.cache.code.disk; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.BitSet; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,44 +27,37 @@ import jadx.api.JadxDecompiler; import jadx.core.Jadx; import jadx.core.dex.nodes.ClassNode; import jadx.core.dex.nodes.RootNode; +import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; -import static java.nio.file.StandardOpenOption.CREATE; -import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; -import static java.nio.file.StandardOpenOption.WRITE; - public class DiskCodeCache implements ICodeCache { private static final Logger LOG = LoggerFactory.getLogger(DiskCodeCache.class); - private static final int DATA_FORMAT_VERSION = 13; - - private static final byte[] JADX_NAMES_MAP_HEADER = "jadxnm".getBytes(StandardCharsets.US_ASCII); + private static final int DATA_FORMAT_VERSION = 14; + private final Path baseDir; private final Path srcDir; private final Path metaDir; private final Path codeVersionFile; - private final Path namesMapFile; private final String codeVersion; private final CodeMetadataAdapter codeMetadataAdapter; private final ExecutorService writePool; - private final Map writeOps = new ConcurrentHashMap<>(); - private final Map namesMap = new ConcurrentHashMap<>(); - private final Map allClsIds; + private final Map clsDataMap; - public DiskCodeCache(RootNode root, Path baseDir) { + public DiskCodeCache(RootNode root, Path projectCacheDir) { + baseDir = projectCacheDir.resolve("code"); srcDir = baseDir.resolve("sources"); metaDir = baseDir.resolve("metadata"); codeVersionFile = baseDir.resolve("code-version"); - namesMapFile = baseDir.resolve("names-map"); JadxArgs args = root.getArgs(); codeVersion = buildCodeVersion(args, root.getDecompiler()); writePool = Executors.newFixedThreadPool(args.getThreadsCount()); codeMetadataAdapter = new CodeMetadataAdapter(root); - allClsIds = buildClassIdsMap(root.getClasses()); + clsDataMap = buildClassDataMap(root.getClasses()); if (checkCodeVersion()) { - loadNamesMap(); + loadCachedSet(); } else { reset(); } @@ -91,10 +79,12 @@ public class DiskCodeCache implements ICodeCache { private void reset() { try { long start = System.currentTimeMillis(); - LOG.info("Resetting disk code cache, base dir: {}", srcDir.getParent().toAbsolutePath()); - FileUtils.deleteDirIfExists(srcDir); - FileUtils.deleteDirIfExists(metaDir); - FileUtils.deleteFileIfExists(namesMapFile); + LOG.info("Resetting disk code cache, base dir: {}", baseDir.toAbsolutePath()); + FileUtils.deleteDirIfExists(baseDir); + if (Files.exists(baseDir.getParent().resolve(codeVersionFile.getFileName()))) { + // remove old version cache files + FileUtils.deleteDirIfExists(baseDir.getParent()); + } FileUtils.makeDirs(srcDir); FileUtils.makeDirs(metaDir); FileUtils.writeFile(codeVersionFile, codeVersion); @@ -104,7 +94,7 @@ public class DiskCodeCache implements ICodeCache { } catch (Exception e) { throw new JadxRuntimeException("Failed to reset code cache", e); } finally { - namesMap.clear(); + clsDataMap.values().forEach(d -> d.setCached(false)); } } @@ -113,18 +103,22 @@ public class DiskCodeCache implements ICodeCache { */ @Override public void add(String clsFullName, ICodeInfo codeInfo) { - writeOps.put(clsFullName, codeInfo); - int clsId = getClsId(clsFullName); - namesMap.put(clsFullName, clsId); + CacheData clsData = getClsData(clsFullName); + clsData.setTmpCodeInfo(codeInfo); + clsData.setCached(true); writePool.execute(() -> { try { - FileUtils.writeFile(getJavaFile(clsId), codeInfo.getCodeStr()); - codeMetadataAdapter.write(getMetadataFile(clsId), codeInfo.getCodeMetadata()); + int clsId = clsData.getClsId(); + ICodeInfo code = clsData.getTmpCodeInfo(); + if (code != null) { + FileUtils.writeFile(getJavaFile(clsId), code.getCodeStr()); + codeMetadataAdapter.write(getMetadataFile(clsId), code.getCodeMetadata()); + } } catch (Exception e) { LOG.error("Failed to write code cache for " + clsFullName, e); remove(clsFullName); } finally { - writeOps.remove(clsFullName); + clsData.setTmpCodeInfo(null); } }); } @@ -135,12 +129,12 @@ public class DiskCodeCache implements ICodeCache { if (!contains(clsFullName)) { return null; } - ICodeInfo wrtCodeInfo = writeOps.get(clsFullName); - if (wrtCodeInfo != null) { - return wrtCodeInfo.getCodeStr(); + CacheData clsData = getClsData(clsFullName); + ICodeInfo tmpCodeInfo = clsData.getTmpCodeInfo(); + if (tmpCodeInfo != null) { + return tmpCodeInfo.getCodeStr(); } - int clsId = getClsId(clsFullName); - Path javaFile = getJavaFile(clsId); + Path javaFile = getJavaFile(clsData.getClsId()); if (!Files.exists(javaFile)) { return null; } @@ -152,16 +146,17 @@ public class DiskCodeCache implements ICodeCache { } @Override - public ICodeInfo get(String clsFullName) { + public @NotNull ICodeInfo get(String clsFullName) { try { if (!contains(clsFullName)) { return ICodeInfo.EMPTY; } - ICodeInfo wrtCodeInfo = writeOps.get(clsFullName); - if (wrtCodeInfo != null) { - return wrtCodeInfo; + CacheData clsData = getClsData(clsFullName); + ICodeInfo tmpCodeInfo = clsData.getTmpCodeInfo(); + if (tmpCodeInfo != null) { + return tmpCodeInfo; } - int clsId = getClsId(clsFullName); + int clsId = clsData.getClsId(); Path javaFile = getJavaFile(clsId); if (!Files.exists(javaFile)) { return ICodeInfo.EMPTY; @@ -176,17 +171,24 @@ public class DiskCodeCache implements ICodeCache { @Override public boolean contains(String clsFullName) { - return namesMap.containsKey(clsFullName); + return getClsData(clsFullName).isCached(); } @Override public void remove(String clsFullName) { try { - Integer clsId = namesMap.remove(clsFullName); - if (clsId != null) { - LOG.debug("Removing class info from disk: {}", clsFullName); - Files.deleteIfExists(getJavaFile(clsId)); - Files.deleteIfExists(getMetadataFile(clsId)); + CacheData clsData = getClsData(clsFullName); + if (clsData.isCached()) { + clsData.setCached(false); + if (clsData.getTmpCodeInfo() == null) { + LOG.debug("Removing class info from disk: {}", clsFullName); + int clsId = clsData.getClsId(); + Files.deleteIfExists(getJavaFile(clsId)); + Files.deleteIfExists(getMetadataFile(clsId)); + } else { + // class info not yet written to disk + clsData.setTmpCodeInfo(null); + } } } catch (Exception e) { throw new JadxRuntimeException("Failed to remove code cache for " + clsFullName, e); @@ -206,55 +208,39 @@ public class DiskCodeCache implements ICodeCache { + ":" + FileUtils.buildInputsHash(Utils.collectionMap(inputFiles, File::toPath)); } - private int getClsId(String clsFullName) { - Integer id = allClsIds.get(clsFullName); - if (id == null) { + private CacheData getClsData(String clsFullName) { + CacheData clsData = clsDataMap.get(clsFullName); + if (clsData == null) { throw new JadxRuntimeException("Unknown class name: " + clsFullName); } - return id; + return clsData; } - private void saveNamesMap() { - LOG.debug("Saving names map for disk cache..."); - try (OutputStream fileOutput = Files.newOutputStream(namesMapFile, WRITE, CREATE, TRUNCATE_EXISTING); - DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fileOutput))) { - out.write(JADX_NAMES_MAP_HEADER); - out.writeInt(namesMap.size()); - for (Map.Entry entry : namesMap.entrySet()) { - out.writeUTF(entry.getKey()); - out.writeInt(entry.getValue()); - } - } catch (Exception e) { - throw new JadxRuntimeException("Failed to save names map file", e); - } - } - - private void loadNamesMap() { - if (!Files.exists(namesMapFile)) { - reset(); - return; - } - namesMap.clear(); - try (InputStream fileInput = Files.newInputStream(namesMapFile); - DataInputStream in = new DataInputStream(new BufferedInputStream(fileInput))) { - in.skipBytes(JADX_NAMES_MAP_HEADER.length); - int count = in.readInt(); - for (int i = 0; i < count; i++) { - String clsName = in.readUTF(); - int clsId = in.readInt(); - namesMap.put(clsName, clsId); - Integer prevId = allClsIds.get(clsName); - if (prevId == null || prevId != clsId) { - LOG.debug("Unexpected class id, got: {}, expect: {}", clsId, prevId); - LOG.warn("Inconsistent disk cache, resetting..."); - reset(); - return; + private void loadCachedSet() { + long start = System.currentTimeMillis(); + BitSet cachedSet = new BitSet(clsDataMap.size()); + try (Stream stream = Files.walk(metaDir)) { + stream.forEach(file -> { + String fileName = file.getFileName().toString(); + if (fileName.endsWith(".jadxmd")) { + String idStr = StringUtils.removeSuffix(fileName, ".jadxmd"); + int clsId = Integer.parseInt(idStr, 16); + cachedSet.set(clsId); } - } - LOG.info("Found {} classes in disk cache, dir: {}", count, metaDir.getParent()); + }); } catch (Exception e) { - throw new JadxRuntimeException("Failed to load names map file", e); + throw new JadxRuntimeException("Failed to enumerate cached classes", e); } + int count = 0; + for (CacheData data : clsDataMap.values()) { + int clsId = data.getClsId(); + if (cachedSet.get(clsId)) { + data.setCached(true); + count++; + } + } + LOG.info("Found {} classes in disk cache, time: {}ms, dir: {}", + count, System.currentTimeMillis() - start, metaDir.getParent()); } private Path getJavaFile(int clsId) { @@ -271,25 +257,58 @@ public class DiskCodeCache implements ICodeCache { return Paths.get(firstByte, FileUtils.intToHex(clsId) + ext); } - private Map buildClassIdsMap(List classes) { + private Map buildClassDataMap(List classes) { int clsCount = classes.size(); - Map map = new HashMap<>(clsCount); + Map map = new HashMap<>(clsCount); for (int i = 0; i < clsCount; i++) { ClassNode cls = classes.get(i); - map.put(cls.getRawName(), i); + map.put(cls.getRawName(), new CacheData(i)); } return map; } - @SuppressWarnings("ResultOfMethodCallIgnored") @Override public void close() throws IOException { - try { - saveNamesMap(); - writePool.shutdown(); - writePool.awaitTermination(2, TimeUnit.MINUTES); - } catch (InterruptedException e) { - LOG.error("Failed to finish file writes", e); + synchronized (this) { + try { + writePool.shutdown(); + boolean completed = writePool.awaitTermination(1, TimeUnit.MINUTES); + if (!completed) { + LOG.warn("Disk code cache closing terminated by timeout"); + } + } catch (InterruptedException e) { + LOG.error("Failed to close disk code cache", e); + } + } + } + + private static final class CacheData { + private final int clsId; + private boolean cached; + private @Nullable ICodeInfo tmpCodeInfo; + + public CacheData(int clsId) { + this.clsId = clsId; + } + + public int getClsId() { + return clsId; + } + + public boolean isCached() { + return cached; + } + + public void setCached(boolean cached) { + this.cached = cached; + } + + public @Nullable ICodeInfo getTmpCodeInfo() { + return tmpCodeInfo; + } + + public void setTmpCodeInfo(@Nullable ICodeInfo tmpCodeInfo) { + this.tmpCodeInfo = tmpCodeInfo; } } } diff --git a/jadx-gui/src/main/java/jadx/gui/cache/manager/CacheManager.java b/jadx-gui/src/main/java/jadx/gui/cache/manager/CacheManager.java index 8a06c2beb..533d3ab38 100644 --- a/jadx-gui/src/main/java/jadx/gui/cache/manager/CacheManager.java +++ b/jadx-gui/src/main/java/jadx/gui/cache/manager/CacheManager.java @@ -111,7 +111,7 @@ public class CacheManager { if (Objects.equals(cacheDirValue, ".")) { return buildLocalCacheDir(project); } - Path cacheBaseDir = cacheDirValue == null ? JadxFiles.CACHE_DIR : Paths.get(cacheDirValue); + Path cacheBaseDir = cacheDirValue == null ? JadxFiles.PROJECTS_CACHE_DIR : Paths.get(cacheDirValue); return cacheBaseDir.resolve(buildProjectUniqName(project)); } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/cache/CachesTable.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/cache/CachesTable.java index b79515b78..81e4a8380 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/cache/CachesTable.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/cache/CachesTable.java @@ -1,7 +1,10 @@ package jadx.gui.settings.ui.cache; import java.awt.Dimension; -import java.io.File; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -10,9 +13,11 @@ import javax.swing.JTable; import javax.swing.ListSelectionModel; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.file.PathUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import jadx.api.plugins.events.types.ReloadProject; import jadx.core.utils.ListUtils; import jadx.core.utils.Utils; import jadx.gui.cache.manager.CacheManager; @@ -95,9 +100,9 @@ public class CachesTable extends JTable { private void calcSize(TableRow row) { String cacheDir = row.getCacheEntry().getCache(); try { - File dir = new File(cacheDir); - if (dir.exists()) { - long size = FileUtils.sizeOfDirectory(dir); + Path dir = Paths.get(cacheDir); + if (Files.isDirectory(dir)) { + BigInteger size = PathUtils.sizeOfDirectoryAsBigInteger(dir); row.setUsage(FileUtils.byteCountToDisplaySize(size)); } else { row.setUsage("not found"); @@ -130,7 +135,7 @@ public class CachesTable extends JTable { status -> { reloadData(); if (reload) { - mainWindow.reopen(); + mainWindow.events().send(ReloadProject.EVENT); } }); } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettings.java index 6ee94abfa..91b0116ec 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettings.java @@ -69,7 +69,7 @@ public class PluginSettings { } private void requestReload() { - mainWindow.events().send(ReloadProject.INSTANCE); + mainWindow.events().send(ReloadProject.EVENT); } public void install(String locationId) { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 668960c40..1e084c031 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -476,7 +476,7 @@ public class MainWindow extends JFrame { } public void reopen() { - synchronized (ReloadProject.INSTANCE) { + synchronized (ReloadProject.EVENT) { saveAll(); closeAll(); loadFiles(EMPTY_RUNNABLE); @@ -688,12 +688,25 @@ public class MainWindow extends JFrame { } public void resetCodeCache() { - Path cacheDir = project.getCacheDir(); - project.resetCacheDir(); backgroundExecutor.execute( NLS.str("preferences.cache.task.delete"), - () -> FileUtils.deleteDirIfExists(cacheDir), - status -> reopen()); + () -> { + try { + getWrapper().getCurrentDecompiler().ifPresent(jadx -> { + try { + jadx.getArgs().getCodeCache().close(); + } catch (Exception e) { + LOG.error("Failed to close code cache", e); + } + }); + Path cacheDir = project.getCacheDir(); + project.resetCacheDir(); + FileUtils.deleteDirIfExists(cacheDir); + } catch (Exception e) { + LOG.error("Error during code cache reset", e); + } + }, + status -> events().send(ReloadProject.EVENT)); } public void cancelBackgroundJobs() { @@ -1155,6 +1168,7 @@ public class MainWindow extends JFrame { decompileAllAction.setEnabled(loaded); deobfAction.setEnabled(loaded); quarkAction.setEnabled(loaded); + resetCacheAction.setEnabled(loaded); return false; }); } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/files/JadxFiles.java b/jadx-gui/src/main/java/jadx/gui/utils/files/JadxFiles.java index dee3b0014..4f82508ce 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/files/JadxFiles.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/files/JadxFiles.java @@ -16,6 +16,7 @@ public class JadxFiles { public static final Path CACHES_LIST = Paths.get(CONFIG_DIR, "caches.json"); public static final Path CACHE_DIR = Paths.get(DIRS.cacheDir); + public static final Path PROJECTS_CACHE_DIR = CACHE_DIR.resolve("projects"); static { FileUtils.makeDirs(Paths.get(CONFIG_DIR));