From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Spottedleaf Date: Sun, 2 Feb 2020 02:25:10 -0800 Subject: [PATCH] Attempt to recalculate regionfile header if it is corrupt Instead of trying to relocate the chunk, which is seems to never be the correct choice, so we end up duplicating or swapping chunks, we instead drop the current regionfile header and recalculate - hoping that at least then we don't swap chunks, and maybe recover them all. diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java index c81392f5b4a6dcef9c1864c1b2c268914b904561..ad4081efec9c7eaf315ddb660f813f6ef3cfbb5b 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkSerializer.java @@ -70,6 +70,13 @@ public class ChunkSerializer { private static final String SKYLIGHT_STATE_TAG = "starlight.skylight_state"; private static final String STARLIGHT_VERSION_TAG = "starlight.light_version"; // Paper end - replace light engine impl + // Paper start + // TODO: Check on update + public static long getLastWorldSaveTime(CompoundTag chunkData) { + CompoundTag levelData = chunkData.getCompound("Level"); + return levelData.getLong("LastUpdate"); + } + // Paper end private static final Logger LOGGER = LogManager.getLogger(); public static final String TAG_UPGRADE_DATA = "UpgradeData"; @@ -124,7 +131,7 @@ public class ChunkSerializer { } // Paper end BiomeSource worldchunkmanager = chunkgenerator.getBiomeSource(); - CompoundTag nbttagcompound1 = nbt.getCompound("Level"); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate + CompoundTag nbttagcompound1 = nbt.getCompound("Level"); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate // Paper - diff on change ChunkPos chunkcoordintpair1 = new ChunkPos(nbttagcompound1.getInt("xPos"), nbttagcompound1.getInt("zPos")); // Paper - diff on change, see ChunkSerializer#getChunkCoordinate if (!Objects.equals(pos, chunkcoordintpair1)) { @@ -495,7 +502,7 @@ public class ChunkSerializer { nbttagcompound.put("Level", nbttagcompound1); nbttagcompound1.putInt("xPos", chunkcoordintpair.x); nbttagcompound1.putInt("zPos", chunkcoordintpair.z); - nbttagcompound1.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading + nbttagcompound1.putLong("LastUpdate", asyncsavedata != null ? asyncsavedata.worldTime : world.getGameTime()); // Paper - async chunk unloading // Paper - diff on change nbttagcompound1.putLong("InhabitedTime", chunk.getInhabitedTime()); nbttagcompound1.putString("Status", chunk.getStatus().getName()); UpgradeData chunkconverter = chunk.getUpgradeData(); diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java index 037bbd562e2f35e17c324cd200c55c5e6cb5d768..b889dbad607b6508fb4987d21d3be691a5b37072 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/ChunkStorage.java @@ -34,7 +34,7 @@ public class ChunkStorage implements AutoCloseable { this.fixerUpper = dataFixer; // Paper start - async chunk io // remove IO worker - this.regionFileCache = new RegionFileStorage(directory, dsync); // Paper - nuke IOWorker + this.regionFileCache = new RegionFileStorage(directory, dsync, true); // Paper - nuke IOWorker // Paper // Paper end - async chunk io } diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java index c8298a597818227de33a4afce4698ec0666cf758..6baceb6ce9021c489be6e79d338a9704285afa26 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionBitmap.java @@ -9,6 +9,27 @@ import java.util.BitSet; public class RegionBitmap { private final BitSet used = new BitSet(); + // Paper start + public final void copyFrom(RegionBitmap other) { + BitSet thisBitset = this.used; + BitSet otherBitset = other.used; + + for (int i = 0; i < Math.max(thisBitset.size(), otherBitset.size()); ++i) { + thisBitset.set(i, otherBitset.get(i)); + } + } + + public final boolean tryAllocate(int from, int length) { + BitSet bitset = this.used; + int firstSet = bitset.nextSetBit(from); + if (firstSet > 0 && firstSet < (from + length)) { + return false; + } + bitset.set(from, from + length); + return true; + } + // Paper end + public void force(int start, int size) { this.used.set(start, start + size); } diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java index c22391a0d4b7db49bd3994b0887939a7d8019391..4881e6ef4393a3d4fc1bd88e2574dcb6d7028e40 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java @@ -55,6 +55,341 @@ public class RegionFile implements AutoCloseable { public final java.util.concurrent.locks.ReentrantLock fileLock = new java.util.concurrent.locks.ReentrantLock(true); // Paper public final File regionFile; // Paper + // Paper start - try to recover from RegionFile header corruption + private static long roundToSectors(long bytes) { + long sectors = bytes >>> 12; // 4096 = 2^12 + long remainingBytes = bytes & 4095; + long sign = -remainingBytes; // sign is 1 if nonzero + return sectors + (sign >>> 63); + } + + private static final CompoundTag OVERSIZED_COMPOUND = new CompoundTag(); + + private CompoundTag attemptRead(long sector, int chunkDataLength, long fileLength) throws IOException { + try { + if (chunkDataLength < 0) { + return null; + } + + long offset = sector * 4096L + 4L; // offset for chunk data + + if ((offset + chunkDataLength) > fileLength) { + return null; + } + + ByteBuffer chunkData = ByteBuffer.allocate(chunkDataLength); + if (chunkDataLength != this.file.read(chunkData, offset)) { + return null; + } + + ((java.nio.Buffer)chunkData).flip(); + + byte compressionType = chunkData.get(); + if (compressionType < 0) { // compressionType & 128 != 0 + // oversized chunk + return OVERSIZED_COMPOUND; + } + + RegionFileVersion compression = RegionFileVersion.fromId(compressionType); + if (compression == null) { + return null; + } + + InputStream input = compression.wrap(new ByteArrayInputStream(chunkData.array(), chunkData.position(), chunkDataLength - chunkData.position())); + + return NbtIo.read((java.io.DataInput)new DataInputStream(new BufferedInputStream(input))); + } catch (Exception ex) { + return null; + } + } + + private int getLength(long sector) throws IOException { + ByteBuffer length = ByteBuffer.allocate(4); + if (4 != this.file.read(length, sector * 4096L)) { + return -1; + } + + return length.getInt(0); + } + + private void backupRegionFile() { + File backup = new File(this.regionFile.getParent(), this.regionFile.getName() + "." + new java.util.Random().nextLong() + ".backup"); + this.backupRegionFile(backup); + } + + private void backupRegionFile(File to) { + try { + this.file.force(true); + LOGGER.warn("Backing up regionfile \"" + this.regionFile.getAbsolutePath() + "\" to " + to.getAbsolutePath()); + java.nio.file.Files.copy(this.regionFile.toPath(), to.toPath()); + LOGGER.warn("Backed up the regionfile to " + to.getAbsolutePath()); + } catch (IOException ex) { + LOGGER.error("Failed to backup to " + to.getAbsolutePath(), ex); + } + } + + // note: only call for CHUNK regionfiles + void recalculateHeader() throws IOException { + if (!this.canRecalcHeader) { + return; + } + synchronized (this) { + LOGGER.warn("Corrupt regionfile header detected! Attempting to re-calculate header offsets for regionfile " + this.regionFile.getAbsolutePath(), new Throwable()); + + // try to backup file so maybe it could be sent to us for further investigation + + this.backupRegionFile(); + CompoundTag[] compounds = new CompoundTag[32 * 32]; // only in the regionfile (i.e exclude mojang/aikar oversized data) + int[] rawLengths = new int[32 * 32]; // length of chunk data including 4 byte length field, bytes + int[] sectorOffsets = new int[32 * 32]; // in sectors + boolean[] hasAikarOversized = new boolean[32 * 32]; + + long fileLength = this.file.size(); + long totalSectors = roundToSectors(fileLength); + + // search the regionfile from start to finish for the most up-to-date chunk data + + for (long i = 2, maxSector = Math.min((long)(Integer.MAX_VALUE >>> 8), totalSectors); i < maxSector; ++i) { // first two sectors are header, skip + int chunkDataLength = this.getLength(i); + CompoundTag compound = this.attemptRead(i, chunkDataLength, fileLength); + if (compound == null || compound == OVERSIZED_COMPOUND) { + continue; + } + + ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(compound); + int location = (chunkPos.x & 31) | ((chunkPos.z & 31) << 5); + + CompoundTag otherCompound = compounds[location]; + + if (otherCompound != null && ChunkSerializer.getLastWorldSaveTime(otherCompound) > ChunkSerializer.getLastWorldSaveTime(compound)) { + continue; // don't overwrite newer data. + } + + // aikar oversized? + File aikarOversizedFile = this.getOversizedFile(chunkPos.x, chunkPos.z); + boolean isAikarOversized = false; + if (aikarOversizedFile.exists()) { + try { + CompoundTag aikarOversizedCompound = this.getOversizedData(chunkPos.x, chunkPos.z); + if (ChunkSerializer.getLastWorldSaveTime(compound) == ChunkSerializer.getLastWorldSaveTime(aikarOversizedCompound)) { + // best we got for an id. hope it's good enough + isAikarOversized = true; + } + } catch (Exception ex) { + LOGGER.error("Failed to read aikar oversized data for absolute chunk (" + chunkPos.x + "," + chunkPos.z + ") in regionfile " + this.regionFile.getAbsolutePath() + ", oversized data for this chunk will be lost", ex); + // fall through, if we can't read aikar oversized we can't risk corrupting chunk data + } + } + + hasAikarOversized[location] = isAikarOversized; + compounds[location] = compound; + rawLengths[location] = chunkDataLength + 4; + sectorOffsets[location] = (int)i; + + int chunkSectorLength = (int)roundToSectors(rawLengths[location]); + i += chunkSectorLength; + --i; // gets incremented next iteration + } + + // forge style oversized data is already handled by the local search, and aikar data we just hope + // we get it right as aikar data has no identifiers we could use to try and find its corresponding + // local data compound + + java.nio.file.Path containingFolder = this.externalFileDir; + File[] regionFiles = containingFolder.toFile().listFiles(); + boolean[] oversized = new boolean[32 * 32]; + RegionFileVersion[] oversizedCompressionTypes = new RegionFileVersion[32 * 32]; + + if (regionFiles != null) { + ChunkPos ourLowerLeftPosition = RegionFileStorage.getRegionFileCoordinates(this.regionFile); + + if (ourLowerLeftPosition == null) { + LOGGER.fatal("Unable to get chunk location of regionfile " + this.regionFile.getAbsolutePath() + ", cannot recover oversized chunks"); + } else { + int lowerXBound = ourLowerLeftPosition.x; // inclusive + int lowerZBound = ourLowerLeftPosition.z; // inclusive + int upperXBound = lowerXBound + 32 - 1; // inclusive + int upperZBound = lowerZBound + 32 - 1; // inclusive + + // read mojang oversized data + for (File regionFile : regionFiles) { + ChunkPos oversizedCoords = getOversizedChunkPair(regionFile); + if (oversizedCoords == null) { + continue; + } + + if ((oversizedCoords.x < lowerXBound || oversizedCoords.x > upperXBound) || (oversizedCoords.z < lowerZBound || oversizedCoords.z > upperZBound)) { + continue; // not in our regionfile + } + + // ensure oversized data is valid & is newer than data in the regionfile + + int location = (oversizedCoords.x & 31) | ((oversizedCoords.z & 31) << 5); + + byte[] chunkData; + try { + chunkData = Files.readAllBytes(regionFile.toPath()); + } catch (Exception ex) { + LOGGER.error("Failed to read oversized chunk data in file " + regionFile.getAbsolutePath() + ", data will be lost", ex); + continue; + } + + CompoundTag compound = null; + + // We do not know the compression type, as it's stored in the regionfile. So we need to try all of them + RegionFileVersion compression = null; + for (RegionFileVersion compressionType : RegionFileVersion.VERSIONS.values()) { + try { + DataInputStream in = new DataInputStream(new BufferedInputStream(compressionType.wrap(new ByteArrayInputStream(chunkData)))); // typical java + compound = NbtIo.read((java.io.DataInput)in); + compression = compressionType; + break; // reaches here iff readNBT does not throw + } catch (Exception ex) { + continue; + } + } + + if (compound == null) { + LOGGER.error("Failed to read oversized chunk data in file " + regionFile.getAbsolutePath() + ", it's corrupt. Its data will be lost"); + continue; + } + + if (compounds[location] == null || ChunkSerializer.getLastWorldSaveTime(compound) > ChunkSerializer.getLastWorldSaveTime(compounds[location])) { + oversized[location] = true; + oversizedCompressionTypes[location] = compression; + } + } + } + } + + // now we need to calculate a new offset header + + int[] calculatedOffsets = new int[32 * 32]; + RegionBitmap newSectorAllocations = new RegionBitmap(); + newSectorAllocations.force(0, 2); // make space for header + + // allocate sectors for normal chunks + + for (int chunkX = 0; chunkX < 32; ++chunkX) { + for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { + int location = chunkX | (chunkZ << 5); + + if (oversized[location]) { + continue; + } + + int rawLength = rawLengths[location]; // bytes + int sectorOffset = sectorOffsets[location]; // sectors + int sectorLength = (int)roundToSectors(rawLength); + + if (newSectorAllocations.tryAllocate(sectorOffset, sectorLength)) { + calculatedOffsets[location] = sectorOffset << 8 | (sectorLength > 255 ? 255 : sectorLength); // support forge style oversized + } else { + LOGGER.error("Failed to allocate space for local chunk (overlapping data??) at (" + chunkX + "," + chunkZ + ") in regionfile " + this.regionFile.getAbsolutePath() + ", chunk will be regenerated"); + } + } + } + + // allocate sectors for oversized chunks + + for (int chunkX = 0; chunkX < 32; ++chunkX) { + for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { + int location = chunkX | (chunkZ << 5); + + if (!oversized[location]) { + continue; + } + + int sectorOffset = newSectorAllocations.allocate(1); + int sectorLength = 1; + + try { + this.file.write(this.createExternalStub(oversizedCompressionTypes[location]), sectorOffset * 4096); + // only allocate in the new offsets if the write succeeds + calculatedOffsets[location] = sectorOffset << 8 | (sectorLength > 255 ? 255 : sectorLength); // support forge style oversized + } catch (IOException ex) { + newSectorAllocations.free(sectorOffset, sectorLength); + LOGGER.error("Failed to write new oversized chunk data holder, local chunk at (" + chunkX + "," + chunkZ + ") in regionfile " + this.regionFile.getAbsolutePath() + " will be regenerated"); + } + } + } + + // rewrite aikar oversized data + + this.oversizedCount = 0; + for (int chunkX = 0; chunkX < 32; ++chunkX) { + for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { + int location = chunkX | (chunkZ << 5); + int isAikarOversized = hasAikarOversized[location] ? 1 : 0; + + this.oversizedCount += isAikarOversized; + this.oversized[location] = (byte)isAikarOversized; + } + } + + if (this.oversizedCount > 0) { + try { + this.writeOversizedMeta(); + } catch (Exception ex) { + LOGGER.error("Failed to write aikar oversized chunk meta, all aikar style oversized chunk data will be lost for regionfile " + this.regionFile.getAbsolutePath(), ex); + this.getOversizedMetaFile().delete(); + } + } else { + this.getOversizedMetaFile().delete(); + } + + this.usedSectors.copyFrom(newSectorAllocations); + + // before we overwrite the old sectors, print a summary of the chunks that got changed. + + LOGGER.info("Starting summary of changes for regionfile " + this.regionFile.getAbsolutePath()); + + for (int chunkX = 0; chunkX < 32; ++chunkX) { + for (int chunkZ = 0; chunkZ < 32; ++chunkZ) { + int location = chunkX | (chunkZ << 5); + + int oldOffset = this.offsets.get(location); + int newOffset = calculatedOffsets[location]; + + if (oldOffset == newOffset) { + continue; + } + + this.offsets.put(location, newOffset); // overwrite incorrect offset + + if (oldOffset == 0) { + // found lost data + LOGGER.info("Found missing data for local chunk (" + chunkX + "," + chunkZ + ") in regionfile " + this.regionFile.getAbsolutePath()); + } else if (newOffset == 0) { + LOGGER.warn("Data for local chunk (" + chunkX + "," + chunkZ + ") could not be recovered in regionfile " + this.regionFile.getAbsolutePath() + ", it will be regenerated"); + } else { + LOGGER.info("Local chunk (" + chunkX + "," + chunkZ + ") changed to point to newer data or correct chunk in regionfile " + this.regionFile.getAbsolutePath()); + } + } + } + + LOGGER.info("End of change summary for regionfile " + this.regionFile.getAbsolutePath()); + + // simply destroy the timestamp header, it's not used + + for (int i = 0; i < 32 * 32; ++i) { + this.timestamps.put(i, calculatedOffsets[i] != 0 ? (int)System.currentTimeMillis() : 0); // write a valid timestamp for valid chunks, I do not want to find out whatever dumb program actually checks this + } + + // write new header + try { + this.flush(); + this.file.force(true); // try to ensure it goes through... + LOGGER.info("Successfully wrote new header to disk for regionfile " + this.regionFile.getAbsolutePath()); + } catch (IOException ex) { + LOGGER.fatal("Failed to write new header to disk for regionfile " + this.regionFile.getAbsolutePath(), ex); + } + } + } + + final boolean canRecalcHeader; // final forces compile fail on new constructor + // Paper end + // Paper start - Cache chunk status private final ChunkStatus[] statuses = new ChunkStatus[32 * 32]; @@ -82,8 +417,19 @@ public class RegionFile implements AutoCloseable { public RegionFile(File file, File directory, boolean dsync) throws IOException { this(file.toPath(), directory.toPath(), RegionFileVersion.VERSION_DEFLATE, dsync); } + // Paper start - add can recalc flag + public RegionFile(File file, File directory, boolean dsync, boolean canRecalcHeader) throws IOException { + this(file.toPath(), directory.toPath(), RegionFileVersion.VERSION_DEFLATE, dsync, canRecalcHeader); + } + // Paper end - add can recalc flag public RegionFile(Path file, Path directory, RegionFileVersion outputChunkStreamVersion, boolean dsync) throws IOException { + // Paper start - add can recalc flag + this(file, directory, outputChunkStreamVersion, dsync, false); + } + public RegionFile(Path file, Path directory, RegionFileVersion outputChunkStreamVersion, boolean dsync, boolean canRecalcHeader) throws IOException { + this.canRecalcHeader = canRecalcHeader; + // Paper end - add can recalc flag this.header = ByteBuffer.allocateDirect(8192); this.regionFile = file.toFile(); // Paper initOversizedState(); // Paper @@ -112,14 +458,16 @@ public class RegionFile implements AutoCloseable { RegionFile.LOGGER.warn("Region file {} has truncated header: {}", file, i); } - long j = Files.size(file); + final long j = Files.size(file); final long regionFileSize = j; // Paper - recalculate header on header corruption + boolean needsHeaderRecalc = false; // Paper - recalculate header on header corruption + boolean hasBackedUp = false; // Paper - recalculate header on header corruption for (int k = 0; k < 1024; ++k) { - int l = this.offsets.get(k); + final int l = this.offsets.get(k); final int headerLocation = l; // Paper - we expect this to be the header location if (l != 0) { - int i1 = RegionFile.getSectorNumber(l); - int j1 = RegionFile.getNumSectors(l); + final int i1 = RegionFile.getSectorNumber(l); final int offset = i1; // Paper - we expect this to be offset in file in sectors + int j1 = RegionFile.getNumSectors(l); final int sectorLength; // Paper - diff on change, we expect this to be sector length of region - watch out for reassignments // Spigot start if (j1 == 255) { // We're maxed out, so we need to read the proper length from the section @@ -128,32 +476,102 @@ public class RegionFile implements AutoCloseable { j1 = (realLen.getInt(0) + 4) / 4096 + 1; } // Spigot end + sectorLength = j1; // Paper - diff on change, we expect this to be sector length of region if (i1 < 2) { RegionFile.LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", file, k, i1); - this.offsets.put(k, 0); + //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change } else if (j1 == 0) { RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", file, k); - this.offsets.put(k, 0); + //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change } else if ((long) i1 * 4096L > j) { RegionFile.LOGGER.warn("Region file {} has an invalid sector at index: {}; sector {} is out of bounds", file, k, i1); - this.offsets.put(k, 0); + //this.offsets.put(k, 0); // Paper - we catch this, but need it in the header for the summary change } else { - this.usedSectors.force(i1, j1); + //this.usedSectors.force(i1, j1); // Paper - move this down so we can check if it fails to allocate + } + // Paper start - recalculate header on header corruption + if (offset < 2 || sectorLength <= 0 || ((long)offset * 4096L) > regionFileSize) { + if (canRecalcHeader) { + LOGGER.error("Detected invalid header for regionfile " + this.regionFile.getAbsolutePath() + "! Recalculating header..."); + needsHeaderRecalc = true; + break; + } else { + // location = chunkX | (chunkZ << 5); + LOGGER.fatal("Detected invalid header for regionfile " + this.regionFile.getAbsolutePath() + + "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header"); + if (!hasBackedUp) { + hasBackedUp = true; + this.backupRegionFile(); + } + this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too + this.offsets.put(headerLocation, 0); // delete the entry from header + continue; + } + } + boolean failedToAllocate = !this.usedSectors.tryAllocate(offset, sectorLength); + if (failedToAllocate) { + LOGGER.error("Overlapping allocation by local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") in regionfile " + this.regionFile.getAbsolutePath()); } + if (failedToAllocate & !canRecalcHeader) { + // location = chunkX | (chunkZ << 5); + LOGGER.fatal("Detected invalid header for regionfile " + this.regionFile.getAbsolutePath() + + "! Cannot recalculate, removing local chunk (" + (headerLocation & 31) + "," + (headerLocation >>> 5) + ") from header"); + if (!hasBackedUp) { + hasBackedUp = true; + this.backupRegionFile(); + } + this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too + this.offsets.put(headerLocation, 0); // delete the entry from header + continue; + } + needsHeaderRecalc |= failedToAllocate; + // Paper end - recalculate header on header corruption } } + // Paper start - recalculate header on header corruption + // we move the recalc here so comparison to old header is correct when logging to console + if (needsHeaderRecalc) { // true if header gave us overlapping allocations or had other issues + LOGGER.error("Recalculating regionfile " + this.regionFile.getAbsolutePath() + ", header gave erroneous offsets & locations"); + this.recalculateHeader(); + } + // Paper end } } } private Path getExternalChunkPath(ChunkPos chunkPos) { - String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; + String s = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; // Paper - diff on change return this.externalFileDir.resolve(s); } + // Paper start + private static ChunkPos getOversizedChunkPair(File file) { + String fileName = file.getName(); + + if (!fileName.startsWith("c.") || !fileName.endsWith(".mcc")) { + return null; + } + + String[] split = fileName.split("\\."); + + if (split.length != 4) { + return null; + } + + try { + int x = Integer.parseInt(split[1]); + int z = Integer.parseInt(split[2]); + + return new ChunkPos(x, z); + } catch (NumberFormatException ex) { + return null; + } + } + // Paper end + @Nullable public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException { int i = this.getOffset(pos); @@ -177,6 +595,12 @@ public class RegionFile implements AutoCloseable { ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error if (bytebuffer.remaining() < 5) { RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", pos, l, bytebuffer.remaining()); + // Paper start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + // Paper end - recalculate header on regionfile corruption return null; } else { int i1 = bytebuffer.getInt(); @@ -184,6 +608,12 @@ public class RegionFile implements AutoCloseable { if (i1 == 0) { RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", pos); + // Paper start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + // Paper end - recalculate header on regionfile corruption return null; } else { int j1 = i1 - 1; @@ -191,17 +621,49 @@ public class RegionFile implements AutoCloseable { if (RegionFile.isExternalStreamChunk(b0)) { if (j1 != 0) { RegionFile.LOGGER.warn("Chunk has both internal and external streams"); + // Paper start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + // Paper end - recalculate header on regionfile corruption } - return this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0)); + // Paper start - recalculate header on regionfile corruption + final DataInputStream ret = this.createExternalChunkInputStream(pos, RegionFile.getExternalChunkVersion(b0)); + if (ret == null && this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + return ret; + // Paper end - recalculate header on regionfile corruption } else if (j1 > bytebuffer.remaining()) { RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", pos, j1, bytebuffer.remaining()); + // Paper start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + // Paper end - recalculate header on regionfile corruption return null; } else if (j1 < 0) { RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", i1, pos); + // Paper start - recalculate header on regionfile corruption + if (this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + // Paper end - recalculate header on regionfile corruption return null; } else { - return this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1)); + // Paper start - recalculate header on regionfile corruption + final DataInputStream ret = this.createChunkInputStream(pos, b0, RegionFile.createStream(bytebuffer, j1)); + if (ret == null && this.canRecalcHeader) { + this.recalculateHeader(); + return this.getChunkDataInputStream(pos); + } + return ret; + // Paper end - recalculate header on regionfile corruption } } } @@ -376,10 +838,15 @@ public class RegionFile implements AutoCloseable { } private ByteBuffer createExternalStub() { + // Paper start - add version param + return this.createExternalStub(this.version); + } + private ByteBuffer createExternalStub(RegionFileVersion version) { + // Paper end - add version param ByteBuffer bytebuffer = ByteBuffer.allocate(5); bytebuffer.putInt(1); - bytebuffer.put((byte) (this.version.getId() | 128)); + bytebuffer.put((byte) (version.getId() | 128)); // Paper - replace with version param ((java.nio.Buffer) bytebuffer).flip(); // CraftBukkit - decompile error return bytebuffer; } diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java index 6496108953effae82391b5c1ea6fdec8482731cd..4e0006b73fbd2634aa42334ae9dde79b4eccaf38 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java @@ -25,7 +25,15 @@ public class RegionFileStorage implements AutoCloseable { private final File folder; private final boolean sync; + private final boolean isChunkData; // Paper + RegionFileStorage(File directory, boolean dsync) { + // Paper start - add isChunkData param + this(directory, dsync, false); + } + RegionFileStorage(File directory, boolean dsync, boolean isChunkData) { + this.isChunkData = isChunkData; + // Paper end - add isChunkData param this.folder = directory; this.sync = dsync; } @@ -90,9 +98,9 @@ public class RegionFileStorage implements AutoCloseable { File file = this.folder; int j = chunkcoordintpair.getRegionX(); - File file1 = new File(file, "r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); + File file1 = new File(file, "r." + j + "." + chunkcoordintpair.getRegionZ() + ".mca"); // Paper - diff on change if (existingOnly && !file1.exists()) return null; // CraftBukkit - RegionFile regionfile1 = new RegionFile(file1, this.folder, this.sync); + RegionFile regionfile1 = new RegionFile(file1, this.folder, this.sync, this.isChunkData); // Paper - allow for chunk regionfiles to regen header this.regionCache.putAndMoveToFirst(i, regionfile1); // Paper start @@ -180,6 +188,13 @@ public class RegionFileStorage implements AutoCloseable { if (regionfile == null) { return null; } + // Paper start - Add regionfile parameter + return this.read(pos, regionfile); + } + public CompoundTag read(ChunkPos pos, RegionFile regionfile) throws IOException { + // We add the regionfile parameter to avoid the potential deadlock (on fileLock) if we went back to obtain a regionfile + // if we decide to re-read + // Paper end // CraftBukkit end try { // Paper DataInputStream datainputstream = regionfile.getChunkDataInputStream(pos); @@ -196,6 +211,17 @@ public class RegionFileStorage implements AutoCloseable { try { if (datainputstream != null) { nbttagcompound = NbtIo.read((DataInput) datainputstream); + // Paper start - recover from corrupt regionfile header + if (this.isChunkData) { + ChunkPos chunkPos = ChunkSerializer.getChunkCoordinate(nbttagcompound); + if (!chunkPos.equals(pos)) { + MinecraftServer.LOGGER.error("Attempting to read chunk data at " + pos.toString() + " but got chunk data for " + chunkPos.toString() + " instead! Attempting regionfile recalculation for regionfile " + regionfile.regionFile.getAbsolutePath()); + regionfile.recalculateHeader(); + regionfile.fileLock.lock(); // otherwise we will unlock twice and only lock once. + return this.read(pos, regionfile); + } + } + // Paper end - recover from corrupt regionfile header break label43; } diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java index b7835b9b904e7d4bff64f7189049e334f5ab4d6f..492bba91e0e61c678e5067a6f855674d42d7f4ea 100644 --- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java +++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileVersion.java @@ -12,7 +12,7 @@ import java.util.zip.InflaterInputStream; import javax.annotation.Nullable; public class RegionFileVersion { - private static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); + public static final Int2ObjectMap VERSIONS = new Int2ObjectOpenHashMap<>(); // Paper - public public static final RegionFileVersion VERSION_GZIP = register(new RegionFileVersion(1, GZIPInputStream::new, GZIPOutputStream::new)); public static final RegionFileVersion VERSION_DEFLATE = register(new RegionFileVersion(2, InflaterInputStream::new, DeflaterOutputStream::new)); public static final RegionFileVersion VERSION_NONE = register(new RegionFileVersion(3, (inputStream) -> {