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
This commit is contained in:
Jordan 2024-07-28 11:16:25 +02:00 committed by GitHub
parent 6052fc3128
commit d1f9d3d6d5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 97 additions and 29 deletions

View File

@ -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;

View File

@ -64,26 +64,47 @@ public class RollbackDatabase extends AsyncNotifyQueue {
public Future<Boolean> 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<Integer> 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<RollbackOptimizedHistory> 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`<?")) {
try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM`" + this.prefix + "_edits` WHERE `time`<?")) {
stmt.setInt(1, then);
return stmt.executeUpdate();
}
@ -160,7 +181,7 @@ public class RollbackDatabase extends AsyncNotifyQueue {
try {
int count = 0;
String stmtStr = """
SELECT * FROM `%sedits`
SELECT * FROM `%s_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();
}

View File

@ -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) {

View File

@ -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();

View File

@ -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;

View File

@ -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;
}

View File

@ -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);

View File

@ -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",

View File

@ -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.
*/