diff --git a/CHANGELOG.md b/CHANGELOG.md index c0a378f..0a4e4e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ +# 1.0.6 +- Fix a crash on startup or during backup when backup data is corrupted. ([HeatherComputer/AdvancedBackups#110](https://github.com/HeatherComputer/AdvancedBackups/issues/110)) + +* * * + # 1.0.5 -- Fix a rare crash when entering a singleplayer world. ([HeatherComputer/AdvancedBackups#110](https://github.com/HeatherComputer/AdvancedBackups/issues/110)) +- Fix a rare crash when entering a singleplayer world. ([HeatherComputer/AdvancedBackups#87](https://github.com/HeatherComputer/AdvancedBackups/issues/87)) - Fix a potential crash during backup when a backup directory is missing. - Fix a potential crash when cleaning up old backup files. diff --git a/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/ABCoreMixin.java b/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/ABCoreMixin.java new file mode 100644 index 0000000..bde9f47 --- /dev/null +++ b/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/ABCoreMixin.java @@ -0,0 +1,42 @@ +package com.github.gtexpert.advancedbackupspatch.mixin; + +import computer.heather.advancedbackups.core.ABCore; +import computer.heather.advancedbackups.core.backups.gson.BackupManifest; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(value = ABCore.class, remap = false) +public abstract class ABCoreMixin { + + /** + * Prevent NPE when parsing a corrupted backup manifest in setActivity. + *

+ * The JAR v3.7.1 only catches {@link JsonParseException}, but {@code gson.fromJson()} + * can return null or a manifest with null fields for certain malformed JSON, + * causing a {@link NullPointerException} when accessing {@code manifest.general.activity}. + *

+ * This redirect converts the null case into a {@link JsonParseException}, + * which is already caught and handled by the existing recovery logic. + * + * @see Issue #110 + */ + @Redirect(method = "setActivity", + at = @At(value = "INVOKE", + target = "Lcom/google/gson/Gson;fromJson(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;")) + private static Object safeFromJson(Gson gson, String json, Class classOfT) { + Object result = gson.fromJson(json, classOfT); + if (result == null) { + throw new JsonParseException("Backup manifest parsed as null"); + } + BackupManifest manifest = (BackupManifest) result; + if (manifest.general == null) { + throw new JsonParseException("Backup manifest 'general' field is null"); + } + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/AdvancedBackupsMixin.java b/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/AdvancedBackupsMixin.java index 7e377c2..ecf17d3 100644 --- a/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/AdvancedBackupsMixin.java +++ b/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/AdvancedBackupsMixin.java @@ -32,6 +32,8 @@ public abstract class AdvancedBackupsMixin { /** * Disable EVENT_BUS.register(this) in the constructor. * The mod container is not yet established at construction time, so defer to preInit. + * + * @see Issue #87 */ @Redirect(method = "", at = @At(value = "INVOKE", @@ -43,6 +45,8 @@ private void redirectEventBusRegister(EventBus bus, Object target) { /** * Disable NetworkHandler.registerMessages() in the constructor. * Defer network channel registration until the mod container is established. + * + * @see Issue #87 */ @Redirect(method = "", at = @At(value = "INVOKE", @@ -54,6 +58,8 @@ private void redirectNetworkRegister() { /** * Register the event bus and network messages at the beginning of preInit, * where the mod container is properly established. + * + * @see Issue #87 */ @Inject(method = "preInit", at = @At("HEAD")) private void onPreInit(FMLPreInitializationEvent event, CallbackInfo ci) { @@ -65,6 +71,8 @@ private void onPreInit(FMLPreInitializationEvent event, CallbackInfo ci) { * Set ABCore loggers after preInit sets them on AdvancedBackups. * Without this, ABCore.infoLogger is null on the client side (only set in onServerStarting), * causing NPE in ClientConfigManager when receiving PacketToastTest. + * + * @see Issue #87 */ @Inject(method = "preInit", at = @At("TAIL")) private void onPreInitTail(FMLPreInitializationEvent event, CallbackInfo ci) { diff --git a/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/BackupWrapperMixin.java b/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/BackupWrapperMixin.java index 76e0534..6d29cfc 100644 --- a/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/BackupWrapperMixin.java +++ b/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/BackupWrapperMixin.java @@ -3,6 +3,10 @@ import java.io.File; import computer.heather.advancedbackups.core.backups.BackupWrapper; +import computer.heather.advancedbackups.core.backups.gson.BackupManifest; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -22,4 +26,32 @@ private static File[] safeListFiles(File directory) { File[] result = directory.listFiles(); return result != null ? result : new File[0]; } + + /** + * Prevent NPE when parsing a corrupted backup manifest in checkStartupBackups. + *

+ * The JAR v3.7.1 only catches {@link JsonParseException}, but {@code gson.fromJson()} + * can return null (or a manifest with null fields) for certain malformed JSON, + * causing a {@link NullPointerException} when accessing {@code manifest.general.activity}. + *

+ * This redirect converts the null case into a {@link JsonParseException}, + * which is already caught and handled by the existing recovery logic + * (creates default manifest and writes it back to disk). + * + * @see Issue #110 + */ + @Redirect(method = "checkStartupBackups", + at = @At(value = "INVOKE", + target = "Lcom/google/gson/Gson;fromJson(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;")) + private static Object safeFromJson(Gson gson, String json, Class classOfT) { + Object result = gson.fromJson(json, classOfT); + if (result == null) { + throw new JsonParseException("Backup manifest parsed as null"); + } + BackupManifest manifest = (BackupManifest) result; + if (manifest.general == null) { + throw new JsonParseException("Backup manifest 'general' field is null"); + } + return result; + } } diff --git a/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/ClientConfigManagerMixin.java b/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/ClientConfigManagerMixin.java index 3c095c7..37d45ac 100644 --- a/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/ClientConfigManagerMixin.java +++ b/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/ClientConfigManagerMixin.java @@ -25,6 +25,8 @@ public abstract class ClientConfigManagerMixin { * which may not see logger writes from the IntegratedServer thread * due to the Java Memory Model (non-volatile fields). * This provides a fallback to prevent NPE. + * + * @see Issue #87 */ @Inject(method = "loadOrCreateConfig", at = @At("HEAD")) private static void ensureLoggers(CallbackInfo ci) { diff --git a/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/CoreCommandSystemMixin.java b/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/CoreCommandSystemMixin.java new file mode 100644 index 0000000..7f4a4ea --- /dev/null +++ b/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/CoreCommandSystemMixin.java @@ -0,0 +1,42 @@ +package com.github.gtexpert.advancedbackupspatch.mixin; + +import computer.heather.advancedbackups.core.CoreCommandSystem; +import computer.heather.advancedbackups.core.backups.gson.BackupManifest; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +@Mixin(value = CoreCommandSystem.class, remap = false) +public abstract class CoreCommandSystemMixin { + + /** + * Prevent NPE when parsing a corrupted backup manifest in resetChainLength. + *

+ * The JAR v3.7.1 only catches {@link JsonParseException}, but {@code gson.fromJson()} + * can return null or a manifest with null fields for certain malformed JSON, + * causing a {@link NullPointerException} when accessing manifest fields. + *

+ * This redirect converts the null case into a {@link JsonParseException}, + * which is already caught and handled by the existing recovery logic. + * + * @see Issue #110 + */ + @Redirect(method = "resetChainLength", + at = @At(value = "INVOKE", + target = "Lcom/google/gson/Gson;fromJson(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;")) + private static Object safeFromJson(Gson gson, String json, Class classOfT) { + Object result = gson.fromJson(json, classOfT); + if (result == null) { + throw new JsonParseException("Backup manifest parsed as null"); + } + BackupManifest manifest = (BackupManifest) result; + if (manifest.incremental == null || manifest.differential == null) { + throw new JsonParseException("Backup manifest chain fields are null"); + } + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/ThreadedBackupMixin.java b/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/ThreadedBackupMixin.java index 6d12167..e09c9cc 100644 --- a/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/ThreadedBackupMixin.java +++ b/src/main/java/com/github/gtexpert/advancedbackupspatch/mixin/ThreadedBackupMixin.java @@ -3,6 +3,10 @@ import java.io.File; import computer.heather.advancedbackups.core.backups.ThreadedBackup; +import computer.heather.advancedbackups.core.backups.gson.BackupManifest; + +import com.google.gson.Gson; +import com.google.gson.JsonParseException; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -14,6 +18,8 @@ public abstract class ThreadedBackupMixin { /** * Prevent NPE when File.list() returns null in performRename. * This happens when the directory does not exist or an I/O error occurs. + * + * @see Issue #103 */ @Redirect(method = "performRename", at = @At(value = "INVOKE", @@ -26,6 +32,8 @@ private String[] safeListInRename(File file) { /** * Prevent NPE when File.list() returns null in performDelete. * This happens when the directory does not exist or an I/O error occurs. + * + * @see Issue #103 */ @Redirect(method = "performDelete", at = @At(value = "INVOKE", @@ -34,4 +42,28 @@ private String[] safeListInDelete(File file) { String[] result = file.list(); return result != null ? result : new String[0]; } + + /** + * Prevent NPE when parsing a corrupted backup manifest in makeDifferentialOrIncrementalBackup. + *

+ * Same issue as in {@code BackupWrapper.checkStartupBackups()} — the JAR v3.7.1 + * only catches {@link JsonParseException}, but {@code gson.fromJson()} can return null + * or a manifest with null fields, causing a {@link NullPointerException}. + * + * @see Issue #110 + */ + @Redirect(method = "makeDifferentialOrIncrementalBackup", + at = @At(value = "INVOKE", + target = "Lcom/google/gson/Gson;fromJson(Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;")) + private Object safeFromJson(Gson gson, String json, Class classOfT) { + Object result = gson.fromJson(json, classOfT); + if (result == null) { + throw new JsonParseException("Backup manifest parsed as null"); + } + BackupManifest manifest = (BackupManifest) result; + if (manifest.differential == null || manifest.incremental == null) { + throw new JsonParseException("Backup manifest fields are null"); + } + return result; + } } diff --git a/src/main/resources/mixins.advancedbackupspatch.json b/src/main/resources/mixins.advancedbackupspatch.json index 5ae7042..aaf96b7 100644 --- a/src/main/resources/mixins.advancedbackupspatch.json +++ b/src/main/resources/mixins.advancedbackupspatch.json @@ -3,9 +3,11 @@ "package": "com.github.gtexpert.advancedbackupspatch.mixin", "compatibilityLevel": "JAVA_8", "mixins": [ + "ABCoreMixin", "AdvancedBackupsMixin", "BackupWrapperMixin", "ClientConfigManagerMixin", + "CoreCommandSystemMixin", "ThreadedBackupMixin" ] }