Skip to content

Commit bb5b719

Browse files
committed
More thread safety precautions in IO handling for "last-file-trusted" importing
1 parent 547d00b commit bb5b719

File tree

3 files changed

+104
-11
lines changed

3 files changed

+104
-11
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package software.coley.recaf.info.properties.builtin;
2+
3+
import jakarta.annotation.Nonnull;
4+
import jakarta.annotation.Nullable;
5+
import software.coley.recaf.info.Info;
6+
import software.coley.recaf.info.properties.BasicProperty;
7+
import software.coley.recaf.info.properties.Property;
8+
9+
/**
10+
* Built in property to track the original index of the file as it appears in the zip structure.
11+
*
12+
* @author Matt Coley
13+
*/
14+
public class ZipEntryIndexProperty extends BasicProperty<Integer> {
15+
public static final String KEY = "zip-entry-index";
16+
17+
/**
18+
* @param value
19+
* The index of the local file in its containing zip archive.
20+
*/
21+
public ZipEntryIndexProperty(int value) {
22+
super(KEY, value);
23+
}
24+
25+
26+
@Override
27+
public boolean persistent() {
28+
return false;
29+
}
30+
31+
/**
32+
* @param info
33+
* Info instance.
34+
*
35+
* @return Zip entry index.
36+
* {@code null} when no property value is assigned.
37+
*/
38+
@Nullable
39+
public static Integer get(@Nonnull Info info) {
40+
Property<Integer> property = info.getProperty(KEY);
41+
if (property != null) {
42+
return property.value();
43+
}
44+
return null;
45+
}
46+
47+
/**
48+
* @param info
49+
* Info instance.
50+
* @param fallback
51+
* Fallback index if there is no recorded index for the info instance.
52+
*
53+
* @return Zip entry index.
54+
* {@code null} when no property value is assigned.
55+
*/
56+
public static int getOr(@Nonnull Info info, int fallback) {
57+
Integer value = get(info);
58+
if (value == null) return fallback;
59+
return value;
60+
}
61+
62+
/**
63+
* @param info
64+
* Info instance.
65+
* @param value
66+
* Zip entry index.
67+
*/
68+
public static void set(@Nonnull Info info, int value) {
69+
info.setProperty(new ZipEntryIndexProperty(value));
70+
}
71+
72+
/**
73+
* @param info
74+
* Info instance.
75+
*/
76+
public static void remove(@Nonnull Info info) {
77+
info.removeProperty(KEY);
78+
}
79+
}

recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import software.coley.recaf.info.properties.builtin.ZipCommentProperty;
3131
import software.coley.recaf.info.properties.builtin.ZipCompressionProperty;
3232
import software.coley.recaf.info.properties.builtin.ZipCreationTimeProperty;
33+
import software.coley.recaf.info.properties.builtin.ZipEntryIndexProperty;
3334
import software.coley.recaf.info.properties.builtin.ZipMarkerProperty;
3435
import software.coley.recaf.info.properties.builtin.ZipModificationTimeProperty;
3536
import software.coley.recaf.info.properties.builtin.ZipPrefixDataProperty;
@@ -214,7 +215,10 @@ private WorkspaceFileResource handleZip(@Nonnull WorkspaceFileResourceBuilder bu
214215
ThreadPoolFactory.newFixedThreadPool("zip-import") :
215216
ThreadPoolFactory.newSingleThreadExecutor("zip-import")) {
216217
List<Callable<Void>> tasks = new ArrayList<>();
217-
for (LocalFileHeader header : archive.getLocalFiles()) {
218+
List<LocalFileHeader> localFiles = archive.getLocalFiles();
219+
for (int i = 0; i < localFiles.size(); i++) {
220+
int entryIndex = i;
221+
LocalFileHeader header = localFiles.get(i);
218222
tasks.add(() -> {
219223
LocalFileHeaderSource headerSource = new LocalFileHeaderSource(header, isAndroid);
220224
String entryName = header.getFileNameAsString();
@@ -229,6 +233,7 @@ private WorkspaceFileResource handleZip(@Nonnull WorkspaceFileResourceBuilder bu
229233
Info info;
230234
try {
231235
info = infoImporter.readInfo(entryName, headerSource);
236+
ZipEntryIndexProperty.set(info, entryIndex);
232237
} catch (IOException ex) {
233238
logger.error("IO error reading ZIP entry '{}' - skipping", entryName, ex);
234239
return null;
@@ -339,7 +344,6 @@ public FileVisitResult visitFile(@Nonnull Path file, @Nonnull BasicFileAttribute
339344
.build();
340345
}
341346

342-
@SuppressWarnings("all") // Ignore synchronizing on parameter
343347
private void addInfo(@Nonnull BasicJvmClassBundle classes,
344348
@Nonnull BasicFileBundle files,
345349
@Nonnull Map<String, AndroidClassBundle> androidClassBundles,
@@ -348,8 +352,6 @@ private void addInfo(@Nonnull BasicJvmClassBundle classes,
348352
@Nonnull ByteSource infoSource,
349353
@Nonnull String pathName,
350354
@Nonnull Info info) {
351-
// TODO: The synchronization here should be more narrow to when each respective bundle/map is interacted with.
352-
// These are all concurrent maps (or sync
353355
if (info.isClass()) {
354356
addClassInfo(classes, files, versionedJvmClassBundles, pathName, info);
355357
} else if (info.isFile()) {
@@ -527,12 +529,19 @@ private void addFileInfo(@Nonnull BasicFileBundle files,
527529
// Warn if there are duplicate file entries.
528530
// Same cases for why this may occur are described above when handling classes.
529531
// The JVM will always use the last item for duplicate entries anyways.
530-
if (files.containsKey(pathName)) {
531-
logger.warn("Multiple duplicate entries for file '{}', dropping older entry", pathName);
532-
}
532+
synchronized (files) {
533+
FileInfo existingFile = files.get(pathName);
534+
if (existingFile != null) {
535+
int existingIndex = ZipEntryIndexProperty.getOr(existingFile, -1);
536+
int newIndex = ZipEntryIndexProperty.getOr(fileInfo, -1);
537+
if (newIndex < existingIndex)
538+
return;
539+
logger.warn("Multiple duplicate entries for file '{}', dropping older entry", pathName);
540+
}
533541

534-
// Store in bundle.
535-
files.initialPut(fileInfo);
542+
// Store in bundle.
543+
files.initialPut(fileInfo);
544+
}
536545
}
537546

538547
/**

recaf-core/src/test/java/software/coley/recaf/services/workspace/io/ResourceImporterTest.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package software.coley.recaf.services.workspace.io;
22

33
import org.junit.jupiter.api.BeforeAll;
4+
import org.junit.jupiter.api.RepeatedTest;
45
import org.junit.jupiter.api.Test;
56
import software.coley.recaf.info.BasicNativeLibraryFileInfo;
67
import software.coley.recaf.info.FileInfo;
@@ -230,7 +231,11 @@ void testAlwaysUseLastMultiReleaseClassEntry() throws IOException {
230231
assertArrayEquals(emptyBytes, resource.getFileBundle().iterator().next().getRawContent());
231232
}
232233

233-
@Test
234+
/**
235+
* This test is repeated to verify it wasn't successful on a race condition <i>(since our IO is multithreaded by default)</i>.
236+
* I know, this isn't an exhaustive/perfect test, but it is better than a single test run.
237+
*/
238+
@RepeatedTest(100)
234239
void testAlwaysUseLastFileEntry() throws IOException {
235240
String path = HelloWorld.class.getName().replace(".", "/");
236241
byte[] bytes = new byte[]{1, 2, 3};
@@ -255,7 +260,7 @@ void testAlwaysUseLastFileEntry() throws IOException {
255260
assertArrayEquals(bytes, resource.getFileBundle().iterator().next().getRawContent());
256261
}
257262

258-
@Test
263+
@RepeatedTest(100)
259264
void testDeduplicateClasses() throws IOException {
260265
String helloWorldPath = HelloWorld.class.getName().replace(".", "/");
261266
byte[] helloWorldBytes = TestClassUtils.fromRuntimeClass(HelloWorld.class).getBytecode();

0 commit comments

Comments
 (0)