package net.minecraft.world.level.chunk.storage; import com.google.common.annotations.VisibleForTesting; import com.mojang.logging.LogUtils; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import net.minecraft.Util; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.profiling.jfr.JvmProfiler; import net.minecraft.world.level.ChunkPos; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; /** * This class handles a single region (or anvil) file and all files for single chunks at chunk positions for that one region file. */ public class RegionFile implements AutoCloseable { private static final Logger LOGGER = LogUtils.getLogger(); private static final int SECTOR_BYTES = 4096; @VisibleForTesting protected static final int SECTOR_INTS = 1024; private static final int CHUNK_HEADER_SIZE = 5; private static final int HEADER_OFFSET = 0; private static final ByteBuffer PADDING_BUFFER = ByteBuffer.allocateDirect(1); private static final String EXTERNAL_FILE_EXTENSION = ".mcc"; private static final int EXTERNAL_STREAM_FLAG = 128; private static final int EXTERNAL_CHUNK_THRESHOLD = 256; private static final int CHUNK_NOT_PRESENT = 0; final RegionStorageInfo info; private final Path path; private final FileChannel file; private final Path externalFileDir; final RegionFileVersion version; private final ByteBuffer header = ByteBuffer.allocateDirect(8192); private final IntBuffer offsets; private final IntBuffer timestamps; @VisibleForTesting protected final RegionBitmap usedSectors = new RegionBitmap(); public RegionFile(RegionStorageInfo info, Path path, Path externalFileDir, boolean sync) throws IOException { this(info, path, externalFileDir, RegionFileVersion.getSelected(), sync); } public RegionFile(RegionStorageInfo info, Path path, Path externalFileDir, RegionFileVersion version, boolean sync) throws IOException { this.info = info; this.path = path; this.version = version; if (!Files.isDirectory(externalFileDir, new LinkOption[0])) { throw new IllegalArgumentException("Expected directory, got " + externalFileDir.toAbsolutePath()); } else { this.externalFileDir = externalFileDir; this.offsets = this.header.asIntBuffer(); this.offsets.limit(1024); this.header.position(4096); this.timestamps = this.header.asIntBuffer(); if (sync) { this.file = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.DSYNC); } else { this.file = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE); } this.usedSectors.force(0, 2); this.header.position(0); int i = this.file.read(this.header, 0L); if (i != -1) { if (i != 8192) { LOGGER.warn("Region file {} has truncated header: {}", path, i); } long l = Files.size(path); for (int j = 0; j < 1024; j++) { int k = this.offsets.get(j); if (k != 0) { int m = getSectorNumber(k); int n = getNumSectors(k); if (m < 2) { LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", path, j, m); this.offsets.put(j, 0); } else if (n == 0) { LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", path, j); this.offsets.put(j, 0); } else if (m * 4096L > l) { LOGGER.warn("Region file {} has an invalid sector at index: {}; sector {} is out of bounds", path, j, m); this.offsets.put(j, 0); } else { this.usedSectors.force(m, n); } } } } } } public Path getPath() { return this.path; } /** * Gets the path to store a chunk that can not be stored within the region file because it's larger than 1 MiB. */ private Path getExternalChunkPath(ChunkPos chunkPos) { String string = "c." + chunkPos.x + "." + chunkPos.z + ".mcc"; return this.externalFileDir.resolve(string); } @Nullable public synchronized DataInputStream getChunkDataInputStream(ChunkPos chunkPos) throws IOException { int i = this.getOffset(chunkPos); if (i == 0) { return null; } else { int j = getSectorNumber(i); int k = getNumSectors(i); int l = k * 4096; ByteBuffer byteBuffer = ByteBuffer.allocate(l); this.file.read(byteBuffer, j * 4096); byteBuffer.flip(); if (byteBuffer.remaining() < 5) { LOGGER.error("Chunk {} header is truncated: expected {} but read {}", chunkPos, l, byteBuffer.remaining()); return null; } else { int m = byteBuffer.getInt(); byte b = byteBuffer.get(); if (m == 0) { LOGGER.warn("Chunk {} is allocated, but stream is missing", chunkPos); return null; } else { int n = m - 1; if (isExternalStreamChunk(b)) { if (n != 0) { LOGGER.warn("Chunk has both internal and external streams"); } return this.createExternalChunkInputStream(chunkPos, getExternalChunkVersion(b)); } else if (n > byteBuffer.remaining()) { LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", chunkPos, n, byteBuffer.remaining()); return null; } else if (n < 0) { LOGGER.error("Declared size {} of chunk {} is negative", m, chunkPos); return null; } else { JvmProfiler.INSTANCE.onRegionFileRead(this.info, chunkPos, this.version, n); return this.createChunkInputStream(chunkPos, b, createStream(byteBuffer, n)); } } } } } /** * Gets a timestamp for the current time to be written to a region file. */ private static int getTimestamp() { return (int)(Util.getEpochMillis() / 1000L); } private static boolean isExternalStreamChunk(byte versionByte) { return (versionByte & 128) != 0; } private static byte getExternalChunkVersion(byte versionByte) { return (byte)(versionByte & -129); } @Nullable private DataInputStream createChunkInputStream(ChunkPos chunkPos, byte versionByte, InputStream inputStream) throws IOException { RegionFileVersion regionFileVersion = RegionFileVersion.fromId(versionByte); if (regionFileVersion == RegionFileVersion.VERSION_CUSTOM) { String string = new DataInputStream(inputStream).readUTF(); ResourceLocation resourceLocation = ResourceLocation.tryParse(string); if (resourceLocation != null) { LOGGER.error("Unrecognized custom compression {}", resourceLocation); return null; } else { LOGGER.error("Invalid custom compression id {}", string); return null; } } else if (regionFileVersion == null) { LOGGER.error("Chunk {} has invalid chunk stream version {}", chunkPos, versionByte); return null; } else { return new DataInputStream(regionFileVersion.wrap(inputStream)); } } @Nullable private DataInputStream createExternalChunkInputStream(ChunkPos chunkPos, byte versionByte) throws IOException { Path path = this.getExternalChunkPath(chunkPos); if (!Files.isRegularFile(path, new LinkOption[0])) { LOGGER.error("External chunk path {} is not file", path); return null; } else { return this.createChunkInputStream(chunkPos, versionByte, Files.newInputStream(path)); } } private static ByteArrayInputStream createStream(ByteBuffer sourceBuffer, int length) { return new ByteArrayInputStream(sourceBuffer.array(), sourceBuffer.position(), length); } /** * Packs the offset in 4 KiB sectors from the region file start and the amount of 4 KiB sectors used to store a chunk into one {@code int}. */ private int packSectorOffset(int sectorOffset, int sectorCount) { return sectorOffset << 8 | sectorCount; } /** * Gets the amount of 4 KiB sectors used to store a chunk. */ private static int getNumSectors(int packedSectorOffset) { return packedSectorOffset & 0xFF; } /** * Gets the offset in 4 KiB sectors from the start of the region file, where the data for a chunk starts. */ private static int getSectorNumber(int packedSectorOffset) { return packedSectorOffset >> 8 & 16777215; } /** * Gets the amount of sectors required to store chunk data of a certain size in bytes. */ private static int sizeToSectors(int size) { return (size + 4096 - 1) / 4096; } public boolean doesChunkExist(ChunkPos chunkPos) { int i = this.getOffset(chunkPos); if (i == 0) { return false; } else { int j = getSectorNumber(i); int k = getNumSectors(i); ByteBuffer byteBuffer = ByteBuffer.allocate(5); try { this.file.read(byteBuffer, j * 4096); byteBuffer.flip(); if (byteBuffer.remaining() != 5) { return false; } else { int l = byteBuffer.getInt(); byte b = byteBuffer.get(); if (isExternalStreamChunk(b)) { if (!RegionFileVersion.isValidVersion(getExternalChunkVersion(b))) { return false; } if (!Files.isRegularFile(this.getExternalChunkPath(chunkPos), new LinkOption[0])) { return false; } } else { if (!RegionFileVersion.isValidVersion(b)) { return false; } if (l == 0) { return false; } int m = l - 1; if (m < 0 || m > 4096 * k) { return false; } } return true; } } catch (IOException var9) { return false; } } } /** * Creates a new {@link java.io.InputStream} for a chunk stored in a separate file. */ public DataOutputStream getChunkDataOutputStream(ChunkPos chunkPos) throws IOException { return new DataOutputStream(this.version.wrap(new RegionFile.ChunkBuffer(chunkPos))); } public void flush() throws IOException { this.file.force(true); } public void clear(ChunkPos chunkPos) throws IOException { int i = getOffsetIndex(chunkPos); int j = this.offsets.get(i); if (j != 0) { this.offsets.put(i, 0); this.timestamps.put(i, getTimestamp()); this.writeHeader(); Files.deleteIfExists(this.getExternalChunkPath(chunkPos)); this.usedSectors.free(getSectorNumber(j), getNumSectors(j)); } } protected synchronized void write(ChunkPos chunkPos, ByteBuffer chunkData) throws IOException { int i = getOffsetIndex(chunkPos); int j = this.offsets.get(i); int k = getSectorNumber(j); int l = getNumSectors(j); int m = chunkData.remaining(); int n = sizeToSectors(m); int o; RegionFile.CommitOp commitOp; if (n >= 256) { Path path = this.getExternalChunkPath(chunkPos); LOGGER.warn("Saving oversized chunk {} ({} bytes} to external file {}", chunkPos, m, path); n = 1; o = this.usedSectors.allocate(n); commitOp = this.writeToExternalFile(path, chunkData); ByteBuffer byteBuffer = this.createExternalStub(); this.file.write(byteBuffer, o * 4096); } else { o = this.usedSectors.allocate(n); commitOp = () -> Files.deleteIfExists(this.getExternalChunkPath(chunkPos)); this.file.write(chunkData, o * 4096); } this.offsets.put(i, this.packSectorOffset(o, n)); this.timestamps.put(i, getTimestamp()); this.writeHeader(); commitOp.run(); if (k != 0) { this.usedSectors.free(k, l); } } private ByteBuffer createExternalStub() { ByteBuffer byteBuffer = ByteBuffer.allocate(5); byteBuffer.putInt(1); byteBuffer.put((byte)(this.version.getId() | 128)); byteBuffer.flip(); return byteBuffer; } /** * Writes a chunk to a separate file with only that chunk. This is used for chunks larger than 1 MiB */ private RegionFile.CommitOp writeToExternalFile(Path externalChunkFile, ByteBuffer chunkData) throws IOException { Path path = Files.createTempFile(this.externalFileDir, "tmp", null); FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE); try { chunkData.position(5); fileChannel.write(chunkData); } catch (Throwable var8) { if (fileChannel != null) { try { fileChannel.close(); } catch (Throwable var7) { var8.addSuppressed(var7); } } throw var8; } if (fileChannel != null) { fileChannel.close(); } return () -> Files.move(path, externalChunkFile, StandardCopyOption.REPLACE_EXISTING); } private void writeHeader() throws IOException { this.header.position(0); this.file.write(this.header, 0L); } private int getOffset(ChunkPos chunkPos) { return this.offsets.get(getOffsetIndex(chunkPos)); } public boolean hasChunk(ChunkPos chunkPos) { return this.getOffset(chunkPos) != 0; } /** * Gets the offset within the region file where the chunk metadata for a chunk can be found. */ private static int getOffsetIndex(ChunkPos chunkPos) { return chunkPos.getRegionLocalX() + chunkPos.getRegionLocalZ() * 32; } public void close() throws IOException { try { this.padToFullSector(); } finally { try { this.file.force(true); } finally { this.file.close(); } } } private void padToFullSector() throws IOException { int i = (int)this.file.size(); int j = sizeToSectors(i) * 4096; if (i != j) { ByteBuffer byteBuffer = PADDING_BUFFER.duplicate(); byteBuffer.position(0); this.file.write(byteBuffer, j - 1); } } class ChunkBuffer extends ByteArrayOutputStream { private final ChunkPos pos; public ChunkBuffer(final ChunkPos pos) { super(8096); super.write(0); super.write(0); super.write(0); super.write(0); super.write(RegionFile.this.version.getId()); this.pos = pos; } public void close() throws IOException { ByteBuffer byteBuffer = ByteBuffer.wrap(this.buf, 0, this.count); int i = this.count - 5 + 1; JvmProfiler.INSTANCE.onRegionFileWrite(RegionFile.this.info, this.pos, RegionFile.this.version, i); byteBuffer.putInt(0, i); RegionFile.this.write(this.pos, byteBuffer); } } interface CommitOp { void run() throws IOException; } }