Skip to content

Commit 9d36c45

Browse files
committed
change: Harden config file IO
(cherry picked from commit 45437ad)
1 parent 16b317f commit 9d36c45

4 files changed

Lines changed: 149 additions & 28 deletions

File tree

common/src/main/java/me/flashyreese/mods/sodiumextra/client/SodiumExtraClientMod.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public static CaffeineConfig mixinConfig() {
8383
}
8484

8585
private static SodiumExtraGameOptions loadConfig() {
86-
return SodiumExtraGameOptions.load(PlatformRuntimeInformation.getInstance().getConfigDirectory().resolve(SodiumExtraConfigKeys.FILE_NAME).toFile());
86+
return SodiumExtraGameOptions.load(PlatformRuntimeInformation.getInstance().getConfigDirectory().resolve(SodiumExtraConfigKeys.FILE_NAME));
8787
}
8888

8989
public static void armWaylandFullscreenResolutionRecovery() {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package me.flashyreese.mods.sodiumextra.client.config;
2+
3+
import java.io.IOException;
4+
import java.nio.ByteBuffer;
5+
import java.nio.channels.FileChannel;
6+
import java.nio.charset.StandardCharsets;
7+
import java.nio.file.Files;
8+
import java.nio.file.Path;
9+
import java.nio.file.StandardCopyOption;
10+
import java.nio.file.StandardOpenOption;
11+
12+
public final class ConfigFileIO {
13+
private ConfigFileIO() {
14+
}
15+
16+
public static void writeStringAtomically(Path path, String contents) throws IOException {
17+
writeBytesAtomically(path, contents.getBytes(StandardCharsets.UTF_8));
18+
}
19+
20+
public static void writeLinesAtomically(Path path, Iterable<String> lines) throws IOException {
21+
StringBuilder builder = new StringBuilder();
22+
for (String line : lines) {
23+
builder.append(line).append(System.lineSeparator());
24+
}
25+
26+
writeStringAtomically(path, builder.toString());
27+
}
28+
29+
public static void writeBytesAtomically(Path path, byte[] bytes) throws IOException {
30+
Path targetPath = path.toAbsolutePath();
31+
Path directory = targetPath.getParent();
32+
Path fileName = targetPath.getFileName();
33+
34+
if (directory == null || fileName == null) {
35+
throw new IOException("Path has no parent directory: " + path);
36+
}
37+
38+
Path tempPath = null;
39+
IOException failure = null;
40+
41+
try {
42+
Files.createDirectories(directory);
43+
tempPath = Files.createTempFile(directory, fileName.toString(), ".tmp");
44+
45+
writeBytesToTempFile(tempPath, bytes);
46+
Files.move(tempPath, targetPath, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
47+
tempPath = null;
48+
49+
forceDirectory(directory);
50+
} catch (IOException e) {
51+
failure = e;
52+
throw e;
53+
} finally {
54+
if (tempPath != null) {
55+
try {
56+
Files.deleteIfExists(tempPath);
57+
} catch (IOException e) {
58+
if (failure != null) {
59+
failure.addSuppressed(e);
60+
} else {
61+
throw e;
62+
}
63+
}
64+
}
65+
}
66+
}
67+
68+
public static Path moveCorruptFile(Path path) throws IOException {
69+
Path corruptPath = nextCorruptFilePath(path);
70+
Files.move(path, corruptPath);
71+
return corruptPath;
72+
}
73+
74+
private static void writeBytesToTempFile(Path tempPath, byte[] bytes) throws IOException {
75+
try (FileChannel channel = FileChannel.open(tempPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
76+
ByteBuffer buffer = ByteBuffer.wrap(bytes);
77+
while (buffer.hasRemaining()) {
78+
channel.write(buffer);
79+
}
80+
81+
channel.force(true);
82+
}
83+
}
84+
85+
private static Path nextCorruptFilePath(Path path) {
86+
Path basePath = path.resolveSibling(path.getFileName() + ".corrupt");
87+
if (!Files.exists(basePath)) {
88+
return basePath;
89+
}
90+
91+
for (int i = 1; ; i++) {
92+
Path candidate = path.resolveSibling(path.getFileName() + ".corrupt." + i);
93+
if (!Files.exists(candidate)) {
94+
return candidate;
95+
}
96+
}
97+
}
98+
99+
private static void forceDirectory(Path directory) {
100+
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
101+
channel.force(true);
102+
} catch (IOException ignored) {
103+
}
104+
}
105+
}

common/src/main/java/me/flashyreese/mods/sodiumextra/client/config/SodiumExtraGameOptions.java

Lines changed: 40 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.google.gson.FieldNamingPolicy;
44
import com.google.gson.Gson;
55
import com.google.gson.GsonBuilder;
6+
import com.google.gson.JsonParseException;
67
import com.google.gson.annotations.SerializedName;
78
import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap;
89
import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap;
@@ -17,10 +18,11 @@
1718
import org.lwjgl.glfw.GLFW;
1819

1920
import java.io.File;
20-
import java.io.FileReader;
21-
import java.io.FileWriter;
2221
import java.io.IOException;
2322
import java.lang.reflect.Modifier;
23+
import java.nio.charset.StandardCharsets;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
2426
import java.util.Arrays;
2527
import java.util.EnumSet;
2628
import java.util.Map;
@@ -38,30 +40,37 @@ public class SodiumExtraGameOptions implements StorageEventHandler {
3840
public RenderSettings renderSettings = new RenderSettings();
3941
@SerializedName(SodiumExtraConfigKeys.EXTRA_SETTINGS)
4042
public ExtraSettings extraSettings = new ExtraSettings();
41-
private File file;
43+
private Path path;
4244

4345
public static SodiumExtraGameOptions load(File file) {
46+
return load(file.toPath());
47+
}
48+
49+
public static SodiumExtraGameOptions load(Path path) {
4450
SodiumExtraGameOptions config;
51+
boolean shouldWriteChanges = true;
4552

46-
if (file.exists()) {
47-
try (FileReader reader = new FileReader(file)) {
53+
if (Files.exists(path)) {
54+
try (var reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
4855
config = gson.fromJson(reader, SodiumExtraGameOptions.class);
49-
} catch (Exception e) {
50-
SodiumExtraClientMod.logger().error("Could not parse config, falling back to defaults!", e);
56+
if (config == null) {
57+
throw new JsonParseException("Root element must be a JSON object");
58+
}
59+
} catch (IOException | JsonParseException | IllegalStateException e) {
60+
SodiumExtraClientMod.logger().warn("Could not read config, falling back to defaults", e);
5161
config = new SodiumExtraGameOptions();
62+
shouldWriteChanges = moveCorruptConfig(path);
5263
}
5364
} else {
5465
config = new SodiumExtraGameOptions();
5566
}
5667

57-
if (config == null) {
58-
SodiumExtraClientMod.logger().error("Could not parse config, falling back to defaults!");
59-
config = new SodiumExtraGameOptions();
60-
}
61-
6268
config.sanitize();
63-
config.file = file;
64-
config.writeChanges();
69+
config.path = path;
70+
71+
if (shouldWriteChanges) {
72+
config.writeChanges();
73+
}
6574

6675
return config;
6776
}
@@ -92,20 +101,27 @@ private void sanitize() {
92101
}
93102

94103
public void writeChanges() {
95-
File dir = this.file.getParentFile();
104+
if (this.path == null) {
105+
SodiumExtraClientMod.logger().warn("Could not save configuration file because no path was set");
106+
return;
107+
}
96108

97-
if (!dir.exists()) {
98-
if (!dir.mkdirs()) {
99-
throw new RuntimeException("Could not create parent directories");
100-
}
101-
} else if (!dir.isDirectory()) {
102-
throw new RuntimeException("The parent file is not a directory");
109+
try {
110+
this.sanitize();
111+
ConfigFileIO.writeStringAtomically(this.path, gson.toJson(this) + System.lineSeparator());
112+
} catch (IOException e) {
113+
SodiumExtraClientMod.logger().warn("Could not save configuration file", e);
103114
}
115+
}
104116

105-
try (FileWriter writer = new FileWriter(this.file)) {
106-
gson.toJson(this, writer);
117+
private static boolean moveCorruptConfig(Path path) {
118+
try {
119+
Path corruptPath = ConfigFileIO.moveCorruptFile(path);
120+
SodiumExtraClientMod.logger().warn("Moved corrupt configuration file to {}", corruptPath);
121+
return true;
107122
} catch (IOException e) {
108-
throw new RuntimeException("Could not save configuration file", e);
123+
SodiumExtraClientMod.logger().warn("Could not move corrupt configuration file", e);
124+
return false;
109125
}
110126
}
111127

common/src/main/java/me/flashyreese/mods/sodiumextra/client/recovery/WaylandFullscreenResolutionRecovery.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.google.gson.JsonElement;
66
import com.google.gson.JsonObject;
77
import com.google.gson.JsonParser;
8+
import me.flashyreese.mods.sodiumextra.client.config.ConfigFileIO;
89
import me.flashyreese.mods.sodiumextra.client.config.SodiumExtraConfigKeys;
910
import org.slf4j.Logger;
1011
import org.slf4j.LoggerFactory;
@@ -83,7 +84,7 @@ private static boolean recoverMinecraftOptions(Path optionsFile) {
8384
}
8485

8586
if (changed) {
86-
Files.write(optionsFile, output, StandardCharsets.UTF_8);
87+
ConfigFileIO.writeLinesAtomically(optionsFile, output);
8788
}
8889

8990
return changed;
@@ -109,8 +110,7 @@ private static JsonObject readJsonObject(Path path) {
109110

110111
private static void writeJson(Path path, JsonObject object) {
111112
try {
112-
Files.createDirectories(path.getParent());
113-
Files.writeString(path, GSON.toJson(object) + System.lineSeparator(), StandardCharsets.UTF_8);
113+
ConfigFileIO.writeStringAtomically(path, GSON.toJson(object) + System.lineSeparator());
114114
} catch (IOException e) {
115115
LOGGER.error("Failed to write Sodium Extra fullscreen recovery state", e);
116116
}

0 commit comments

Comments
 (0)