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.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import javax.annotation.Nullable; import net.minecraft.Util; import net.minecraft.resources.ResourceLocation; import net.minecraft.util.profiling.jfr.JvmProfiler; import net.minecraft.world.level.ChunkPos; import org.slf4j.Logger; 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 p_333062_, Path p_63633_, Path p_63634_, boolean p_63636_) throws IOException { this(p_333062_, p_63633_, p_63634_, RegionFileVersion.getSelected(), p_63636_); } public RegionFile(RegionStorageInfo p_335896_, Path p_196950_, Path p_196951_, RegionFileVersion p_331589_, boolean p_196952_) throws IOException { this.info = p_335896_; this.path = p_196950_; this.version = p_331589_; if (!Files.isDirectory(p_196951_)) { throw new IllegalArgumentException("Expected directory, got " + p_196951_.toAbsolutePath()); } else { this.externalFileDir = p_196951_; this.offsets = this.header.asIntBuffer(); this.offsets.limit(1024); this.header.position(4096); this.timestamps = this.header.asIntBuffer(); if (p_196952_) { this.file = FileChannel.open( p_196950_, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.DSYNC ); } else { this.file = FileChannel.open(p_196950_, 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: {}", p_196950_, i); } long j = Files.size(p_196950_); for (int k = 0; k < 1024; k++) { int l = this.offsets.get(k); if (l != 0) { int i1 = getSectorNumber(l); int j1 = getNumSectors(l); if (i1 < 2) { LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", p_196950_, k, i1); this.offsets.put(k, 0); } else if (j1 == 0) { LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", p_196950_, k); this.offsets.put(k, 0); } else if (i1 * 4096L > j) { LOGGER.warn("Region file {} has an invalid sector at index: {}; sector {} is out of bounds", p_196950_, k, i1); this.offsets.put(k, 0); } else { this.usedSectors.force(i1, j1); } } } } } } public Path getPath() { return this.path; } private Path getExternalChunkPath(ChunkPos p_63685_) { String s = "c." + p_63685_.x + "." + p_63685_.z + ".mcc"; return this.externalFileDir.resolve(s); } @Nullable public synchronized DataInputStream getChunkDataInputStream(ChunkPos p_63646_) throws IOException { int i = this.getOffset(p_63646_); 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 {}", p_63646_, l, bytebuffer.remaining()); return null; } else { int i1 = bytebuffer.getInt(); byte b0 = bytebuffer.get(); if (i1 == 0) { LOGGER.warn("Chunk {} is allocated, but stream is missing", p_63646_); return null; } else { int j1 = i1 - 1; if (isExternalStreamChunk(b0)) { if (j1 != 0) { LOGGER.warn("Chunk has both internal and external streams"); } return this.createExternalChunkInputStream(p_63646_, getExternalChunkVersion(b0)); } else if (j1 > bytebuffer.remaining()) { LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", p_63646_, j1, bytebuffer.remaining()); return null; } else if (j1 < 0) { LOGGER.error("Declared size {} of chunk {} is negative", i1, p_63646_); return null; } else { JvmProfiler.INSTANCE.onRegionFileRead(this.info, p_63646_, this.version, j1); return this.createChunkInputStream(p_63646_, b0, createStream(bytebuffer, j1)); } } } } } private static int getTimestamp() { return (int)(Util.getEpochMillis() / 1000L); } private static boolean isExternalStreamChunk(byte p_63639_) { return (p_63639_ & 128) != 0; } private static byte getExternalChunkVersion(byte p_63670_) { return (byte)(p_63670_ & -129); } @Nullable private DataInputStream createChunkInputStream(ChunkPos p_63651_, byte p_63652_, InputStream p_63653_) throws IOException { RegionFileVersion regionfileversion = RegionFileVersion.fromId(p_63652_); if (regionfileversion == RegionFileVersion.VERSION_CUSTOM) { String s = new DataInputStream(p_63653_).readUTF(); ResourceLocation resourcelocation = ResourceLocation.tryParse(s); if (resourcelocation != null) { LOGGER.error("Unrecognized custom compression {}", resourcelocation); return null; } else { LOGGER.error("Invalid custom compression id {}", s); return null; } } else if (regionfileversion == null) { LOGGER.error("Chunk {} has invalid chunk stream version {}", p_63651_, p_63652_); return null; } else { return new DataInputStream(regionfileversion.wrap(p_63653_)); } } @Nullable private DataInputStream createExternalChunkInputStream(ChunkPos p_63648_, byte p_63649_) throws IOException { Path path = this.getExternalChunkPath(p_63648_); if (!Files.isRegularFile(path)) { LOGGER.error("External chunk path {} is not file", path); return null; } else { return this.createChunkInputStream(p_63648_, p_63649_, Files.newInputStream(path)); } } private static ByteArrayInputStream createStream(ByteBuffer p_63660_, int p_63661_) { return new ByteArrayInputStream(p_63660_.array(), p_63660_.position(), p_63661_); } private int packSectorOffset(int p_63643_, int p_63644_) { return p_63643_ << 8 | p_63644_; } private static int getNumSectors(int p_63641_) { return p_63641_ & 0xFF; } private static int getSectorNumber(int p_63672_) { return p_63672_ >> 8 & 16777215; } private static int sizeToSectors(int p_63677_) { return (p_63677_ + 4096 - 1) / 4096; } public boolean doesChunkExist(ChunkPos p_63674_) { int i = this.getOffset(p_63674_); 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 b0 = bytebuffer.get(); if (isExternalStreamChunk(b0)) { if (!RegionFileVersion.isValidVersion(getExternalChunkVersion(b0))) { return false; } if (!Files.isRegularFile(this.getExternalChunkPath(p_63674_))) { return false; } } else { if (!RegionFileVersion.isValidVersion(b0)) { return false; } if (l == 0) { return false; } int i1 = l - 1; if (i1 < 0 || i1 > 4096 * k) { return false; } } return true; } } catch (IOException ioexception) { return false; } } } public DataOutputStream getChunkDataOutputStream(ChunkPos p_63679_) throws IOException { return new DataOutputStream(this.version.wrap(new RegionFile.ChunkBuffer(p_63679_))); } public void flush() throws IOException { this.file.force(true); } public void clear(ChunkPos p_156614_) throws IOException { int i = getOffsetIndex(p_156614_); 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(p_156614_)); this.usedSectors.free(getSectorNumber(j), getNumSectors(j)); } } protected synchronized void write(ChunkPos p_63655_, ByteBuffer p_63656_) throws IOException { int i = getOffsetIndex(p_63655_); int j = this.offsets.get(i); int k = getSectorNumber(j); int l = getNumSectors(j); int i1 = p_63656_.remaining(); int j1 = sizeToSectors(i1); int k1; RegionFile.CommitOp regionfile$commitop; if (j1 >= 256) { Path path = this.getExternalChunkPath(p_63655_); LOGGER.warn("Saving oversized chunk {} ({} bytes} to external file {}", p_63655_, i1, path); j1 = 1; k1 = this.usedSectors.allocate(j1); regionfile$commitop = this.writeToExternalFile(path, p_63656_); ByteBuffer bytebuffer = this.createExternalStub(); this.file.write(bytebuffer, k1 * 4096); } else { k1 = this.usedSectors.allocate(j1); regionfile$commitop = () -> Files.deleteIfExists(this.getExternalChunkPath(p_63655_)); this.file.write(p_63656_, k1 * 4096); } this.offsets.put(i, this.packSectorOffset(k1, j1)); this.timestamps.put(i, getTimestamp()); this.writeHeader(); regionfile$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; } private RegionFile.CommitOp writeToExternalFile(Path p_63663_, ByteBuffer p_63664_) throws IOException { Path path = Files.createTempFile(this.externalFileDir, "tmp", null); try (FileChannel filechannel = FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { p_63664_.position(5); filechannel.write(p_63664_); } return () -> Files.move(path, p_63663_, StandardCopyOption.REPLACE_EXISTING); } private void writeHeader() throws IOException { this.header.position(0); this.file.write(this.header, 0L); } private int getOffset(ChunkPos p_63687_) { return this.offsets.get(getOffsetIndex(p_63687_)); } public boolean hasChunk(ChunkPos p_63683_) { return this.getOffset(p_63683_) != 0; } private static int getOffsetIndex(ChunkPos p_63689_) { return p_63689_.getRegionLocalX() + p_63689_.getRegionLocalZ() * 32; } @Override 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 p_63696_) { super(8096); super.write(0); super.write(0); super.write(0); super.write(0); super.write(RegionFile.this.version.getId()); this.pos = p_63696_; } @Override 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; } }