From d1f9d3d6d50a61c5199b23797880df32bfa9888f Mon Sep 17 00:00:00 2001 From: Jordan Date: Sun, 28 Jul 2024 11:16:25 +0200 Subject: [PATCH] fix: improve FAWE stream history (#2844) - reset "origin"/relative X/Z when larger than short min/max value so we do not write incorrect positions - history size can be larger than int max value - fixes #2583 --- .../command/tool/brush/CopyPastaBrush.java | 2 +- .../core/database/RollbackDatabase.java | 48 ++++++++++++++----- .../history/changeset/AbstractChangeSet.java | 2 +- .../changeset/AbstractDelegateChangeSet.java | 5 ++ .../changeset/FaweStreamChangeSet.java | 48 +++++++++++++++---- .../core/util/MainUtil.java | 2 +- .../com/sk89q/worldedit/LocalSession.java | 2 +- .../worldedit/command/HistorySubCommands.java | 4 +- .../history/changeset/ChangeSet.java | 13 +++++ 9 files changed, 97 insertions(+), 29 deletions(-) diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/command/tool/brush/CopyPastaBrush.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/command/tool/brush/CopyPastaBrush.java index b635c1e7d..1b60124cb 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/command/tool/brush/CopyPastaBrush.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/command/tool/brush/CopyPastaBrush.java @@ -91,7 +91,7 @@ public class CopyPastaBrush implements Brush, ResettableTool { newClipboard.setOrigin(position); ClipboardHolder holder = new ClipboardHolder(newClipboard); session.setClipboard(holder); - int blocks = builder.size(); + long blocks = builder.longSize(); player.print(Caption.of("fawe.worldedit.copy.command.copy", blocks)); } else { AffineTransform transform = null; diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/database/RollbackDatabase.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/database/RollbackDatabase.java index f8207f2dc..e40683f2e 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/database/RollbackDatabase.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/database/RollbackDatabase.java @@ -64,26 +64,47 @@ public class RollbackDatabase extends AsyncNotifyQueue { public Future init() { return call(() -> { try (PreparedStatement stmt = connection.prepareStatement("CREATE TABLE IF NOT EXISTS`" + this.prefix + - "edits` (`player` BLOB(16) NOT NULL,`id` INT NOT NULL, `time` INT NOT NULL,`x1`" + - "INT NOT NULL,`x2` INT NOT NULL,`z1` INT NOT NULL,`z2` INT NOT NULL,`y1`" + - "INT NOT NULL, `y2` INT NOT NULL, `size` INT NOT NULL, `command` VARCHAR, PRIMARY KEY (player, id))")) { + "_edits` (`player` BLOB(16) NOT NULL,`id` INT NOT NULL, `time` INT NOT NULL,`x1` " + + "INT NOT NULL,`x2` INT NOT NULL,`z1` INT NOT NULL,`z2` INT NOT NULL,`y1` " + + "INT NOT NULL, `y2` INT NOT NULL, `size` BIGINT NOT NULL, `command` VARCHAR, PRIMARY KEY (player, id))")) { stmt.executeUpdate(); } - try (PreparedStatement stmt = connection.prepareStatement("ALTER TABLE`" + this.prefix + "edits` ADD COLUMN `command` VARCHAR")) { + String alterTablePrefix = "ALTER TABLE`" + this.prefix + "edits` "; + try (PreparedStatement stmt = + connection.prepareStatement(alterTablePrefix + "ADD COLUMN `command` VARCHAR")) { stmt.executeUpdate(); } catch (SQLException ignored) { } // Already updated - try (PreparedStatement stmt = connection.prepareStatement("ALTER TABLE`" + this.prefix + "edits` ADD SIZE INT DEFAULT 0 NOT NULL")) { + try (PreparedStatement stmt = + connection.prepareStatement(alterTablePrefix + "ADD COLUMN `size` BIGINT DEFAULT 0 NOT NULL")) { stmt.executeUpdate(); } catch (SQLException ignored) { } // Already updated + + boolean migrated = false; + try (PreparedStatement stmt = + connection.prepareStatement("INSERT INTO `" + this.prefix + "_edits` " + + "(player, id, time, x1, x2, z1, z2, y1, y2, size, command) " + + "SELECT player, id, time, x1, x2, z1, z2, y1, y2, size, command " + + "FROM `" + this.prefix + "edits`")) { + + stmt.executeUpdate(); + migrated = true; + } catch (SQLException ignored) { + } // Already updated + if (migrated) { + try (PreparedStatement stmt = connection.prepareStatement("DROP TABLE `" + this.prefix + "edits`")) { + stmt.executeUpdate(); + } + } return true; }); } public Future delete(UUID uuid, int id) { return call(() -> { - try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM`" + this.prefix + "edits` WHERE `player`=? AND `id`=?")) { + try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM`" + this.prefix + "_edits` WHERE `player`=? " + + "AND `id`=?")) { stmt.setBytes(1, toBytes(uuid)); stmt.setInt(2, id); return stmt.executeUpdate(); @@ -94,7 +115,7 @@ public class RollbackDatabase extends AsyncNotifyQueue { public Future getEdit(@Nonnull UUID uuid, int id) { return call(() -> { try (PreparedStatement stmt = connection.prepareStatement("SELECT * FROM`" + this.prefix + - "edits` WHERE `player`=? AND `id`=?")) { + "_edits` WHERE `player`=? AND `id`=?")) { stmt.setBytes(1, toBytes(uuid)); stmt.setInt(2, id); ResultSet result = stmt.executeQuery(); @@ -119,7 +140,7 @@ public class RollbackDatabase extends AsyncNotifyQueue { CuboidRegion region = new CuboidRegion(BlockVector3.at(x1, y1, z1), BlockVector3.at(x2, y2, z2)); long time = result.getInt("time") * 1000L; - long size = result.getInt("size"); + long size = result.getLong("size"); String command = result.getString("command"); @@ -135,7 +156,7 @@ public class RollbackDatabase extends AsyncNotifyQueue { long now = System.currentTimeMillis() / 1000; final int then = (int) (now - diff); return call(() -> { - try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM`" + this.prefix + "edits` WHERE `time` ? AND `x2` >= ? AND `x1` <= ? @@ -202,7 +223,8 @@ public class RollbackDatabase extends AsyncNotifyQueue { } if (delete && uuid != null) { try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM`" + this.prefix + - "edits` WHERE `player`=? AND `time`>? AND `x2`>=? AND `x1`<=? AND `y2`>=? AND `y1`<=? AND `z2`>=? AND `z1`<=?")) { + "_edits` WHERE `player`=? AND `time`>? AND `x2`>=? AND `x1`<=? AND `y2`>=? AND `y1`<=? AND `z2`>=? " + + "AND `z1`<=?")) { byte[] uuidBytes = ByteBuffer .allocate(16) .putLong(uuid.getMostSignificantBits()) @@ -249,7 +271,7 @@ public class RollbackDatabase extends AsyncNotifyQueue { RollbackOptimizedHistory[] copy = IntStream.range(0, size) .mapToObj(i -> historyChanges.poll()).toArray(RollbackOptimizedHistory[]::new); - try (PreparedStatement stmt = connection.prepareStatement("INSERT OR REPLACE INTO`" + this.prefix + "edits`" + + try (PreparedStatement stmt = connection.prepareStatement("INSERT OR REPLACE INTO`" + this.prefix + "_edits`" + " (`player`,`id`,`time`,`x1`,`x2`,`z1`,`z2`,`y1`,`y2`,`command`,`size`) VALUES(?,?,?,?,?,?,?,?,?,?,?)")) { // `player`,`id`,`time`,`x1`,`x2`,`z1`,`z2`,`y1`,`y2`,`command`,`size`) VALUES(?,?,?,?,?,?,?,?,?,?,?)" for (RollbackOptimizedHistory change : copy) { @@ -270,7 +292,7 @@ public class RollbackDatabase extends AsyncNotifyQueue { stmt.setInt(8, pos1.y() - 128); stmt.setInt(9, pos2.y() - 128); stmt.setString(10, change.getCommand()); - stmt.setInt(11, change.size()); + stmt.setLong(11, change.longSize()); stmt.executeUpdate(); stmt.clearParameters(); } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/AbstractChangeSet.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/AbstractChangeSet.java index 08165577f..346543a0b 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/AbstractChangeSet.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/AbstractChangeSet.java @@ -306,7 +306,7 @@ public abstract class AbstractChangeSet implements ChangeSet, IBatchProcessor { } public boolean isEmpty() { - return queue.isEmpty() && workerSemaphore.availablePermits() == 1 && size() == 0; + return queue.isEmpty() && workerSemaphore.availablePermits() == 1 && longSize() == 0; } public void add(BlockVector3 loc, BaseBlock from, BaseBlock to) { diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/AbstractDelegateChangeSet.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/AbstractDelegateChangeSet.java index 195a5fdbd..a44e73013 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/AbstractDelegateChangeSet.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/AbstractDelegateChangeSet.java @@ -176,6 +176,11 @@ public class AbstractDelegateChangeSet extends AbstractChangeSet { return parent.size(); } + @Override + public long longSize() { + return parent.longSize(); + } + @Override public void delete() { parent.delete(); diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/FaweStreamChangeSet.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/FaweStreamChangeSet.java index ad7c82c9f..e7132bf5a 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/FaweStreamChangeSet.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/history/changeset/FaweStreamChangeSet.java @@ -25,6 +25,7 @@ import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.NoSuchElementException; @@ -35,11 +36,18 @@ import java.util.NoSuchElementException; public abstract class FaweStreamChangeSet extends AbstractChangeSet { public static final int HEADER_SIZE = 9; - private static final int version = 1; + private static final int VERSION = 2; + // equivalent to Short#MIN_VALUE three times stored with [(x) & 0xff, ((rx) >> 8) & 0xff] + private static final byte[] MAGIC_NEW_RELATIVE = new byte[]{0, (byte) 128, 0, (byte) 128, 0, (byte) 128}; private int mode; private final int compression; private final int minY; + protected long blockSize; + private int originX; + private int originZ; + private int version; + protected FaweStreamIdDelegate idDel; protected FaweStreamPositionDelegate posDel; @@ -192,6 +200,20 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet { int rx = -lx + (lx = x); int ry = -ly + (ly = y); int rz = -lz + (lz = z); + // Use LE/GE to ensure we don't accidentally write MAGIC_NEW_RELATIVE + if (rx >= Short.MAX_VALUE || rz >= Short.MAX_VALUE || rx <= Short.MIN_VALUE || rz <= Short.MIN_VALUE) { + stream.write(MAGIC_NEW_RELATIVE); + stream.write((byte) (x >> 24)); + stream.write((byte) (x >> 16)); + stream.write((byte) (x >> 8)); + stream.write((byte) (x)); + stream.write((byte) (z >> 24)); + stream.write((byte) (z >> 16)); + stream.write((byte) (z >> 8)); + stream.write((byte) (z)); + rx = 0; + rz = 0; + } stream.write((rx) & 0xff); stream.write(((rx) >> 8) & 0xff); stream.write((rz) & 0xff); @@ -203,6 +225,12 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet { @Override public int readX(FaweInputStream is) throws IOException { is.readFully(buffer); + // Don't break reading version 1 history (just in case) + if (version == 2 && Arrays.equals(buffer, MAGIC_NEW_RELATIVE)) { + lx = ((is.read() << 24) + (is.read() << 16) + (is.read() << 8) + is.read()); + lz = ((is.read() << 24) + (is.read() << 16) + (is.read() << 8) + is.read()); + is.readFully(buffer); + } return lx = lx + ((buffer[0] & 0xFF) | (buffer[1] << 8)); } @@ -222,7 +250,7 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet { public void writeHeader(OutputStream os, int x, int y, int z) throws IOException { os.write(mode); // Allows for version detection of history in case of changes to format. - os.write(version); + os.write(VERSION); setOrigin(x, z); os.write((byte) (x >> 24)); os.write((byte) (x >> 16)); @@ -238,8 +266,8 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet { public void readHeader(InputStream is) throws IOException { // skip mode int mode = is.read(); - int version = is.read(); - if (version != FaweStreamChangeSet.version) { + version = is.read(); + if (version != 1 && version != VERSION) { // version 1 is fine throw new UnsupportedOperationException(String.format("Version %s history not supported!", version)); } // origin @@ -266,12 +294,17 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet { } @Override - public int size() { + public long longSize() { // Flush so we can accurately get the size flush(); return blockSize; } + @Override + public int size() { + return (int) longSize(); + } + public abstract int getCompressedSize(); public abstract long getSizeInMemory(); @@ -304,11 +337,6 @@ public abstract class FaweStreamChangeSet extends AbstractChangeSet { public abstract NBTInputStream getTileRemoveIS() throws IOException; - protected int blockSize; - - private int originX; - private int originZ; - public void setOrigin(int x, int z) { originX = x; originZ = z; diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/MainUtil.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/MainUtil.java index 6b693162e..83f951c13 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/MainUtil.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/util/MainUtil.java @@ -217,7 +217,7 @@ public class MainUtil { // } else if (changeSet instanceof CPUOptimizedChangeSet) { // return changeSet.size() + 32; } else if (changeSet != null) { - return changeSet.size() * 128L; + return changeSet.longSize() * 128; // Approx } else { return 0; } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java index aa5f51e57..3dd5f5638 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/LocalSession.java @@ -500,7 +500,7 @@ public class LocalSession implements TextureHolder { if (Settings.settings().HISTORY.USE_DISK) { LocalSession.MAX_HISTORY_SIZE = Integer.MAX_VALUE; } - if (changeSet.size() == 0) { + if (changeSet.longSize() == 0) { return; } loadSessionHistoryFromDisk(player.getUniqueId(), world); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/HistorySubCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/HistorySubCommands.java index 8d28ad1eb..38fc18415 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/HistorySubCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/HistorySubCommands.java @@ -251,7 +251,7 @@ public class HistorySubCommands { long seconds = (System.currentTimeMillis() - edit.getBDFile().lastModified()) / 1000; String timeStr = MainUtil.secToTime(seconds); - int size = edit.size(); + long size = edit.longSize(); boolean biomes = edit.getBioFile().exists(); boolean createdEnts = edit.getEnttFile().exists(); boolean removedEnts = edit.getEntfFile().exists(); @@ -335,7 +335,7 @@ public class HistorySubCommands { long seconds = (System.currentTimeMillis() - rollback.getBDFile().lastModified()) / 1000; String timeStr = MainUtil.secToTime(seconds); - int size = edit.size(); + long size = edit.longSize(); TranslatableComponent elem = Caption.of( "fawe.worldedit.history.find.element", diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/history/changeset/ChangeSet.java b/worldedit-core/src/main/java/com/sk89q/worldedit/history/changeset/ChangeSet.java index b4ab9adc1..890b247d6 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/history/changeset/ChangeSet.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/history/changeset/ChangeSet.java @@ -81,11 +81,24 @@ public interface ChangeSet extends Closeable { * Get the number of stored changes. * * @return the change count + * @deprecated History could be larger than int max value so FAWE prefers {@link ChangeSet#longSize()} */ + @Deprecated(since = "TODO") int size(); //FAWE start + /** + * Get the number of stored changes. + * History could be larger than int max value so FAWE prefers this method. + * + * @return the change count + * @since TODO + */ + default long longSize() { + return size(); + } + /** * Close the changeset. */