1 | 1 | | package jadx.gui.utils.codecache.disk; |
2 | 2 | | |
| 3 | + | import java.io.BufferedInputStream; |
| 4 | + | import java.io.BufferedOutputStream; |
3 | 5 | | import java.io.ByteArrayOutputStream; |
| 6 | + | import java.io.DataInputStream; |
4 | 7 | | import java.io.DataOutputStream; |
5 | 8 | | import java.io.File; |
6 | 9 | | import java.io.IOException; |
| 10 | + | import java.io.InputStream; |
| 11 | + | import java.io.OutputStream; |
7 | 12 | | import java.nio.charset.StandardCharsets; |
8 | | - | import java.nio.file.FileVisitResult; |
9 | 13 | | import java.nio.file.Files; |
10 | 14 | | import java.nio.file.Path; |
11 | | - | import java.nio.file.PathMatcher; |
12 | | - | import java.nio.file.SimpleFileVisitor; |
13 | | - | import java.nio.file.attribute.BasicFileAttributes; |
| 15 | + | import java.nio.file.Paths; |
14 | 16 | | import java.nio.file.attribute.FileTime; |
15 | 17 | | import java.util.Collections; |
16 | | - | import java.util.HashSet; |
17 | 18 | | import java.util.List; |
18 | 19 | | import java.util.Map; |
19 | | - | import java.util.Set; |
20 | 20 | | import java.util.concurrent.ConcurrentHashMap; |
21 | 21 | | import java.util.concurrent.ExecutorService; |
22 | 22 | | import java.util.concurrent.Executors; |
| skipped 11 lines |
34 | 34 | | import jadx.core.utils.exceptions.JadxRuntimeException; |
35 | 35 | | import jadx.core.utils.files.FileUtils; |
36 | 36 | | |
| 37 | + | import static java.nio.file.StandardOpenOption.CREATE; |
| 38 | + | import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; |
| 39 | + | import static java.nio.file.StandardOpenOption.WRITE; |
| 40 | + | |
37 | 41 | | public class DiskCodeCache implements ICodeCache { |
38 | 42 | | private static final Logger LOG = LoggerFactory.getLogger(DiskCodeCache.class); |
39 | 43 | | |
40 | | - | private static final int DATA_FORMAT_VERSION = 10; |
| 44 | + | private static final int DATA_FORMAT_VERSION = 11; |
| 45 | + | |
| 46 | + | private static final byte[] JADX_NAMES_MAP_HEADER = "jadxnm".getBytes(StandardCharsets.US_ASCII); |
41 | 47 | | |
42 | 48 | | private final Path srcDir; |
43 | 49 | | private final Path metaDir; |
44 | 50 | | private final Path codeVersionFile; |
| 51 | + | private final Path namesMapFile; |
45 | 52 | | private final String codeVersion; |
46 | 53 | | private final CodeMetadataAdapter codeMetadataAdapter; |
47 | 54 | | private final ExecutorService writePool; |
48 | 55 | | private final Map<String, ICodeInfo> writeOps = new ConcurrentHashMap<>(); |
49 | | - | private final Set<String> cachedKeys = Collections.synchronizedSet(new HashSet<>()); |
| 56 | + | private final Map<String, Integer> namesMap = new ConcurrentHashMap<>(); |
50 | 57 | | |
51 | 58 | | public DiskCodeCache(RootNode root, Path baseDir) { |
52 | 59 | | srcDir = baseDir.resolve("sources"); |
53 | 60 | | metaDir = baseDir.resolve("metadata"); |
54 | 61 | | codeVersionFile = baseDir.resolve("code-version"); |
| 62 | + | namesMapFile = baseDir.resolve("names-map"); |
55 | 63 | | JadxArgs args = root.getArgs(); |
56 | 64 | | codeVersion = buildCodeVersion(args); |
57 | 65 | | writePool = Executors.newFixedThreadPool(args.getThreadsCount()); |
58 | 66 | | codeMetadataAdapter = new CodeMetadataAdapter(root); |
59 | 67 | | if (checkCodeVersion()) { |
60 | | - | collectCachedItems(); |
| 68 | + | loadNamesMap(); |
61 | 69 | | } else { |
62 | 70 | | reset(); |
63 | 71 | | } |
| skipped 4 lines |
68 | 76 | | if (!Files.exists(codeVersionFile)) { |
69 | 77 | | return false; |
70 | 78 | | } |
71 | | - | String currentCodeVer = readFileToString(codeVersionFile); |
| 79 | + | String currentCodeVer = FileUtils.readFile(codeVersionFile); |
72 | 80 | | return currentCodeVer.equals(codeVersion); |
73 | 81 | | } catch (Exception e) { |
74 | 82 | | LOG.warn("Failed to load code version file", e); |
| skipped 7 lines |
82 | 90 | | LOG.info("Resetting disk code cache, base dir: {}", srcDir.getParent().toAbsolutePath()); |
83 | 91 | | FileUtils.deleteDirIfExists(srcDir); |
84 | 92 | | FileUtils.deleteDirIfExists(metaDir); |
| 93 | + | FileUtils.deleteFileIfExists(namesMapFile); |
85 | 94 | | FileUtils.makeDirs(srcDir); |
86 | 95 | | FileUtils.makeDirs(metaDir); |
87 | | - | writeFile(codeVersionFile, codeVersion); |
88 | | - | cachedKeys.clear(); |
| 96 | + | FileUtils.writeFile(codeVersionFile, codeVersion); |
89 | 97 | | if (LOG.isDebugEnabled()) { |
90 | 98 | | LOG.info("Reset done in: {}ms", System.currentTimeMillis() - start); |
91 | 99 | | } |
92 | 100 | | } catch (Exception e) { |
93 | 101 | | throw new JadxRuntimeException("Failed to reset code cache", e); |
94 | | - | } |
95 | | - | } |
96 | | - | |
97 | | - | private void collectCachedItems() { |
98 | | - | cachedKeys.clear(); |
99 | | - | try { |
100 | | - | long start = System.currentTimeMillis(); |
101 | | - | PathMatcher matcher = metaDir.getFileSystem().getPathMatcher("glob:**.jadxmd"); |
102 | | - | Files.walkFileTree(metaDir, new SimpleFileVisitor<Path>() { |
103 | | - | @Override |
104 | | - | public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { |
105 | | - | if (matcher.matches(file)) { |
106 | | - | Path relPath = metaDir.relativize(file); |
107 | | - | String filePath = relPath.toString(); |
108 | | - | String clsName = filePath.substring(0, filePath.length() - 7).replace(File.separatorChar, '.'); |
109 | | - | cachedKeys.add(clsName); |
110 | | - | } |
111 | | - | return FileVisitResult.CONTINUE; |
112 | | - | } |
113 | | - | }); |
114 | | - | LOG.info("Found {} classes metadata in disk cache in {} ms, dir: {}", cachedKeys.size(), |
115 | | - | System.currentTimeMillis() - start, metaDir); |
116 | | - | } catch (Exception e) { |
117 | | - | LOG.error("Failed to collect cached items", e); |
| 102 | + | } finally { |
| 103 | + | namesMap.clear(); |
118 | 104 | | } |
119 | 105 | | } |
120 | 106 | | |
| skipped 3 lines |
124 | 110 | | @Override |
125 | 111 | | public void add(String clsFullName, ICodeInfo codeInfo) { |
126 | 112 | | writeOps.put(clsFullName, codeInfo); |
127 | | - | cachedKeys.add(clsFullName); |
| 113 | + | int clsId = getClsId(clsFullName); |
128 | 114 | | writePool.execute(() -> { |
129 | 115 | | try { |
130 | | - | writeFile(getJavaFile(clsFullName), codeInfo.getCodeStr()); |
131 | | - | codeMetadataAdapter.write(getMetadataFile(clsFullName), codeInfo.getCodeMetadata()); |
| 116 | + | FileUtils.writeFile(getJavaFile(clsId), codeInfo.getCodeStr()); |
| 117 | + | codeMetadataAdapter.write(getMetadataFile(clsId), codeInfo.getCodeMetadata()); |
132 | 118 | | } catch (Exception e) { |
133 | 119 | | LOG.error("Failed to write code cache for " + clsFullName, e); |
134 | 120 | | remove(clsFullName); |
| skipped 13 lines |
148 | 134 | | if (wrtCodeInfo != null) { |
149 | 135 | | return wrtCodeInfo.getCodeStr(); |
150 | 136 | | } |
151 | | - | Path javaFile = getJavaFile(clsFullName); |
| 137 | + | int clsId = getClsId(clsFullName); |
| 138 | + | Path javaFile = getJavaFile(clsId); |
152 | 139 | | if (!Files.exists(javaFile)) { |
153 | 140 | | return null; |
154 | 141 | | } |
155 | | - | return readFileToString(javaFile); |
| 142 | + | return FileUtils.readFile(javaFile); |
156 | 143 | | } catch (Exception e) { |
157 | 144 | | LOG.error("Failed to read class code for {}", clsFullName, e); |
158 | 145 | | return null; |
| skipped 10 lines |
169 | 156 | | if (wrtCodeInfo != null) { |
170 | 157 | | return wrtCodeInfo; |
171 | 158 | | } |
172 | | - | Path javaFile = getJavaFile(clsFullName); |
| 159 | + | int clsId = getClsId(clsFullName); |
| 160 | + | Path javaFile = getJavaFile(clsId); |
173 | 161 | | if (!Files.exists(javaFile)) { |
174 | 162 | | return ICodeInfo.EMPTY; |
175 | 163 | | } |
176 | | - | String code = readFileToString(javaFile); |
177 | | - | return codeMetadataAdapter.readAndBuild(getMetadataFile(clsFullName), code); |
| 164 | + | String code = FileUtils.readFile(javaFile); |
| 165 | + | return codeMetadataAdapter.readAndBuild(getMetadataFile(clsId), code); |
178 | 166 | | } catch (Exception e) { |
179 | 167 | | LOG.error("Failed to read code cache for {}", clsFullName, e); |
180 | 168 | | return ICodeInfo.EMPTY; |
| skipped 2 lines |
183 | 171 | | |
184 | 172 | | @Override |
185 | 173 | | public boolean contains(String clsFullName) { |
186 | | - | return cachedKeys.contains(clsFullName); |
| 174 | + | return namesMap.containsKey(clsFullName); |
187 | 175 | | } |
188 | 176 | | |
189 | 177 | | @Override |
190 | 178 | | public void remove(String clsFullName) { |
191 | 179 | | try { |
192 | 180 | | LOG.debug("Removing class info from disk: {}", clsFullName); |
193 | | - | cachedKeys.remove(clsFullName); |
194 | | - | Files.deleteIfExists(getJavaFile(clsFullName)); |
195 | | - | Files.deleteIfExists(getMetadataFile(clsFullName)); |
| 181 | + | Integer clsId = namesMap.remove(clsFullName); |
| 182 | + | if (clsId != null) { |
| 183 | + | Files.deleteIfExists(getJavaFile(clsId)); |
| 184 | + | Files.deleteIfExists(getMetadataFile(clsId)); |
| 185 | + | } |
196 | 186 | | } catch (Exception e) { |
197 | 187 | | throw new JadxRuntimeException("Failed to remove code cache for " + clsFullName, e); |
198 | 188 | | } |
199 | 189 | | } |
200 | 190 | | |
201 | | - | private static String readFileToString(Path textFile) throws IOException { |
202 | | - | return new String(Files.readAllBytes(textFile), StandardCharsets.UTF_8); |
203 | | - | } |
204 | | - | |
205 | | - | private void writeFile(Path file, String data) { |
206 | | - | try { |
207 | | - | FileUtils.makeDirsForFile(file); |
208 | | - | Files.write(file, data.getBytes(StandardCharsets.UTF_8)); |
209 | | - | } catch (Exception e) { |
210 | | - | LOG.error("Failed to write file: {}", file.toAbsolutePath(), e); |
211 | | - | } |
212 | | - | } |
213 | | - | |
214 | 191 | | private String buildCodeVersion(JadxArgs args) { |
215 | 192 | | return DATA_FORMAT_VERSION |
216 | 193 | | + ":" + args.makeCodeArgsHash() |
| skipped 21 lines |
238 | 215 | | } |
239 | 216 | | } |
240 | 217 | | |
241 | | - | private Path getJavaFile(String clsFullName) { |
242 | | - | return srcDir.resolve(clsFullName.replace('.', File.separatorChar) + ".java"); |
| 218 | + | private int getClsId(String clsFullName) { |
| 219 | + | return namesMap.computeIfAbsent(clsFullName, n -> namesMap.size()); |
| 220 | + | } |
| 221 | + | |
| 222 | + | private void saveNamesMap() { |
| 223 | + | LOG.debug("Saving names map for disk cache..."); |
| 224 | + | try (OutputStream fileOutput = Files.newOutputStream(namesMapFile, WRITE, CREATE, TRUNCATE_EXISTING); |
| 225 | + | DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fileOutput))) { |
| 226 | + | out.write(JADX_NAMES_MAP_HEADER); |
| 227 | + | out.writeInt(namesMap.size()); |
| 228 | + | for (Map.Entry<String, Integer> entry : namesMap.entrySet()) { |
| 229 | + | out.writeUTF(entry.getKey()); |
| 230 | + | out.writeInt(entry.getValue()); |
| 231 | + | } |
| 232 | + | } catch (Exception e) { |
| 233 | + | throw new JadxRuntimeException("Failed to save names map file", e); |
| 234 | + | } |
243 | 235 | | } |
244 | 236 | | |
245 | | - | private Path getMetadataFile(String clsFullName) { |
246 | | - | return metaDir.resolve(clsFullName.replace('.', File.separatorChar) + ".jadxmd"); |
| 237 | + | private void loadNamesMap() { |
| 238 | + | if (!Files.exists(namesMapFile)) { |
| 239 | + | reset(); |
| 240 | + | return; |
| 241 | + | } |
| 242 | + | namesMap.clear(); |
| 243 | + | try (InputStream fileInput = Files.newInputStream(namesMapFile); |
| 244 | + | DataInputStream in = new DataInputStream(new BufferedInputStream(fileInput))) { |
| 245 | + | in.skipBytes(JADX_NAMES_MAP_HEADER.length); |
| 246 | + | int count = in.readInt(); |
| 247 | + | for (int i = 0; i < count; i++) { |
| 248 | + | String clsName = in.readUTF(); |
| 249 | + | int clsId = in.readInt(); |
| 250 | + | namesMap.put(clsName, clsId); |
| 251 | + | } |
| 252 | + | LOG.info("Found {} classes in disk cache, dir: {}", count, metaDir.getParent()); |
| 253 | + | } catch (Exception e) { |
| 254 | + | throw new JadxRuntimeException("Failed to load names map file", e); |
| 255 | + | } |
| 256 | + | } |
| 257 | + | |
| 258 | + | private Path getJavaFile(int clsId) { |
| 259 | + | return srcDir.resolve(getPathForClsId(clsId, ".java")); |
| 260 | + | } |
| 261 | + | |
| 262 | + | private Path getMetadataFile(int clsId) { |
| 263 | + | return metaDir.resolve(getPathForClsId(clsId, ".jadxmd")); |
| 264 | + | } |
| 265 | + | |
| 266 | + | private Path getPathForClsId(int clsId, String ext) { |
| 267 | + | // all classes divided between 256 top level folders |
| 268 | + | String firstByte = FileUtils.byteToHex(clsId); |
| 269 | + | return Paths.get(firstByte, FileUtils.intToHex(clsId) + ext); |
247 | 270 | | } |
248 | 271 | | |
249 | 272 | | @SuppressWarnings("ResultOfMethodCallIgnored") |
250 | 273 | | @Override |
251 | 274 | | public void close() throws IOException { |
252 | 275 | | try { |
| 276 | + | saveNamesMap(); |
253 | 277 | | writePool.shutdown(); |
254 | 278 | | writePool.awaitTermination(2, TimeUnit.MINUTES); |
255 | 279 | | } catch (InterruptedException e) { |
| skipped 5 lines |