From bd0dd10942991930024502fc2cadb349ecb732b8 Mon Sep 17 00:00:00 2001 From: Jonas Herzig Date: Sun, 28 Nov 2021 15:38:51 +0100 Subject: [PATCH] Ensure we cannot accidentally open a replay file twice E.g. from the Replay Viewer while it is still being recovered. Because that would potentially corrupt it. --- .../core/files/DelegatingReplayFile.java | 202 ++++++++++++++++++ .../core/files/ManagedReplayFile.java | 23 ++ .../core/files/ReplayFilesService.java | 50 ++++- 3 files changed, 269 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/replaymod/core/files/DelegatingReplayFile.java create mode 100644 src/main/java/com/replaymod/core/files/ManagedReplayFile.java diff --git a/src/main/java/com/replaymod/core/files/DelegatingReplayFile.java b/src/main/java/com/replaymod/core/files/DelegatingReplayFile.java new file mode 100644 index 00000000..26950f93 --- /dev/null +++ b/src/main/java/com/replaymod/core/files/DelegatingReplayFile.java @@ -0,0 +1,202 @@ +package com.replaymod.core.files; + +import com.replaymod.replaystudio.data.Marker; +import com.replaymod.replaystudio.data.ModInfo; +import com.replaymod.replaystudio.data.ReplayAssetEntry; +import com.replaymod.replaystudio.io.ReplayInputStream; +import com.replaymod.replaystudio.io.ReplayOutputStream; +import com.replaymod.replaystudio.lib.guava.base.Optional; +import com.replaymod.replaystudio.pathing.PathingRegistry; +import com.replaymod.replaystudio.pathing.path.Timeline; +import com.replaymod.replaystudio.protocol.PacketTypeRegistry; +import com.replaymod.replaystudio.replay.ReplayFile; +import com.replaymod.replaystudio.replay.ReplayMetaData; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.regex.Pattern; + +public class DelegatingReplayFile implements ReplayFile { + private final ReplayFile delegate; + + public DelegatingReplayFile(ReplayFile delegate) { + this.delegate = delegate; + } + + @Override + public Optional get(String entry) throws IOException { + return this.delegate.get(entry); + } + + @Override + public Optional getCache(String entry) throws IOException { + return this.delegate.getCache(entry); + } + + @Override + public Map getAll(Pattern pattern) throws IOException { + return this.delegate.getAll(pattern); + } + + @Override + public OutputStream write(String entry) throws IOException { + return this.delegate.write(entry); + } + + @Override + public OutputStream writeCache(String entry) throws IOException { + return this.delegate.writeCache(entry); + } + + @Override + public void remove(String entry) throws IOException { + this.delegate.remove(entry); + } + + @Override + public void removeCache(String entry) throws IOException { + this.delegate.removeCache(entry); + } + + @Override + public void save() throws IOException { + this.delegate.save(); + } + + @Override + public void saveTo(File target) throws IOException { + this.delegate.saveTo(target); + } + + @Override + public ReplayMetaData getMetaData() throws IOException { + return this.delegate.getMetaData(); + } + + @Override + public void writeMetaData(PacketTypeRegistry registry, ReplayMetaData metaData) throws IOException { + this.delegate.writeMetaData(registry, metaData); + } + + @Override + public ReplayInputStream getPacketData(PacketTypeRegistry registry) throws IOException { + return this.delegate.getPacketData(registry); + } + + @Override + public ReplayOutputStream writePacketData() throws IOException { + return this.delegate.writePacketData(); + } + + @Override + public Map getResourcePackIndex() throws IOException { + return this.delegate.getResourcePackIndex(); + } + + @Override + public void writeResourcePackIndex(Map index) throws IOException { + this.delegate.writeResourcePackIndex(index); + } + + @Override + public Optional getResourcePack(String hash) throws IOException { + return this.delegate.getResourcePack(hash); + } + + @Override + public OutputStream writeResourcePack(String hash) throws IOException { + return this.delegate.writeResourcePack(hash); + } + + @Override + public Map getTimelines(PathingRegistry pathingRegistry) throws IOException { + return this.delegate.getTimelines(pathingRegistry); + } + + @Override + public void writeTimelines(PathingRegistry pathingRegistry, Map timelines) throws IOException { + this.delegate.writeTimelines(pathingRegistry, timelines); + } + + @Override + public Optional getThumb() throws IOException { + return this.delegate.getThumb(); + } + + @Override + public void writeThumb(BufferedImage image) throws IOException { + this.delegate.writeThumb(image); + } + + @Override + public Optional getThumbBytes() throws IOException { + return this.delegate.getThumbBytes(); + } + + @Override + public void writeThumbBytes(byte[] image) throws IOException { + this.delegate.writeThumbBytes(image); + } + + @Override + public Optional> getInvisiblePlayers() throws IOException { + return this.delegate.getInvisiblePlayers(); + } + + @Override + public void writeInvisiblePlayers(Set uuids) throws IOException { + this.delegate.writeInvisiblePlayers(uuids); + } + + @Override + public Optional> getMarkers() throws IOException { + return this.delegate.getMarkers(); + } + + @Override + public void writeMarkers(Set markers) throws IOException { + this.delegate.writeMarkers(markers); + } + + @Override + public Collection getAssets() throws IOException { + return this.delegate.getAssets(); + } + + @Override + public Optional getAsset(UUID uuid) throws IOException { + return this.delegate.getAsset(uuid); + } + + @Override + public OutputStream writeAsset(ReplayAssetEntry asset) throws IOException { + return this.delegate.writeAsset(asset); + } + + @Override + public void removeAsset(UUID uuid) throws IOException { + this.delegate.removeAsset(uuid); + } + + @Override + public Collection getModInfo() throws IOException { + return this.delegate.getModInfo(); + } + + @Override + public void writeModInfo(Collection modInfo) throws IOException { + this.delegate.writeModInfo(modInfo); + } + + @Override + public void close() throws IOException { + this.delegate.close(); + } +} diff --git a/src/main/java/com/replaymod/core/files/ManagedReplayFile.java b/src/main/java/com/replaymod/core/files/ManagedReplayFile.java new file mode 100644 index 00000000..e000e354 --- /dev/null +++ b/src/main/java/com/replaymod/core/files/ManagedReplayFile.java @@ -0,0 +1,23 @@ +package com.replaymod.core.files; + +import com.replaymod.replaystudio.replay.ReplayFile; + +import java.io.IOException; + +public class ManagedReplayFile extends DelegatingReplayFile { + private Runnable onClose; + + public ManagedReplayFile(ReplayFile delegate, Runnable onClose) { + super(delegate); + + this.onClose = onClose; + } + + @Override + public void close() throws IOException { + super.close(); + + onClose.run(); + onClose = () -> {}; + } +} diff --git a/src/main/java/com/replaymod/core/files/ReplayFilesService.java b/src/main/java/com/replaymod/core/files/ReplayFilesService.java index 6439b344..9090f4b5 100644 --- a/src/main/java/com/replaymod/core/files/ReplayFilesService.java +++ b/src/main/java/com/replaymod/core/files/ReplayFilesService.java @@ -17,9 +17,14 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; public class ReplayFilesService { private final ReplayFoldersService folders; + private final Set lockedPaths = Collections.newSetFromMap(new ConcurrentHashMap<>()); public ReplayFilesService(ReplayFoldersService folders) { this.folders = folders; @@ -30,12 +35,39 @@ public class ReplayFilesService { } public ReplayFile open(Path input, Path output) throws IOException { - return new ZipReplayFile( - new ReplayStudio(), - input != null ? input.toFile() : null, - output.toFile(), - folders.getCachePathForReplay(output).toFile() - ); + Path realInput = input != null ? input.toAbsolutePath().normalize() : null; + Path realOutput = output.toAbsolutePath().normalize(); + + if (realInput != null && !lockedPaths.add(realInput)) { + throw new FileLockedException(realInput); + } + if (!Objects.equals(realInput, realOutput) && !lockedPaths.add(realOutput)) { + if (realInput != null) { + lockedPaths.remove(realInput); + } + throw new FileLockedException(realOutput); + } + + Runnable onClose = () -> { + if (realInput != null) { + lockedPaths.remove(realInput); + } + lockedPaths.remove(realOutput); + }; + + ReplayFile replayFile; + try { + replayFile = new ZipReplayFile( + new ReplayStudio(), + realInput != null ? realInput.toFile() : null, + realOutput.toFile(), + folders.getCachePathForReplay(realOutput).toFile() + ); + } catch (IOException e) { + onClose.run(); + throw e; + } + return new ManagedReplayFile(replayFile, onClose); } public void initialScan(ReplayMod core) { @@ -151,4 +183,10 @@ public class ReplayFilesService { e.printStackTrace(); } } + + public static class FileLockedException extends IOException { + public FileLockedException(Path path) { + super(path.toString()); + } + } }