package net.minecraft.world.level.chunk.storage; import java.io.ByteArrayOutputStream; import org.apache.logging.log4j.LogManager; import java.nio.file.StandardCopyOption; import java.nio.file.CopyOption; import java.nio.file.attribute.FileAttribute; import net.minecraft.Util; import java.io.BufferedOutputStream; import java.io.OutputStream; import java.io.DataOutputStream; import java.io.ByteArrayInputStream; import java.io.BufferedInputStream; import javax.annotation.Nullable; import java.io.InputStream; import java.io.DataInputStream; import net.minecraft.world.level.ChunkPos; import java.nio.file.StandardOpenOption; import java.nio.file.OpenOption; import java.nio.file.Files; import java.nio.file.LinkOption; import java.io.IOException; import java.io.File; import java.nio.IntBuffer; import java.nio.file.Path; import java.nio.channels.FileChannel; import java.nio.ByteBuffer; import org.apache.logging.log4j.Logger; public class RegionFile implements AutoCloseable { private static final Logger LOGGER; private static final ByteBuffer PADDING_BUFFER; private final FileChannel file; private final Path externalFileDir; private final RegionFileVersion version; private final ByteBuffer header; private final IntBuffer offsets; private final IntBuffer timestamps; private final RegionBitmap usedSectors; public RegionFile(final File file1, final File file2) throws IOException { this(file1.toPath(), file2.toPath(), RegionFileVersion.VERSION_DEFLATE); } public RegionFile(final Path path1, final Path path2, final RegionFileVersion cbb) throws IOException { this.header = ByteBuffer.allocateDirect(8192); this.usedSectors = new RegionBitmap(); this.version = cbb; if (!Files.isDirectory(path2)) { throw new IllegalArgumentException("Expected directory, got " + path2.toAbsolutePath()); } this.externalFileDir = path2; (this.offsets = this.header.asIntBuffer()).limit(1024); this.header.position(4096); this.timestamps = this.header.asIntBuffer(); this.file = FileChannel.open(path1, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE); this.usedSectors.force(0, 2); this.header.position(0); final int integer5 = this.file.read(this.header, 0L); if (integer5 != -1) { if (integer5 != 8192) { RegionFile.LOGGER.warn("Region file {} has truncated header: {}", path1, integer5); } for (int integer6 = 0; integer6 < 1024; ++integer6) { final int integer7 = this.offsets.get(integer6); if (integer7 != 0) { final int integer8 = getSectorNumber(integer7); final int integer9 = getNumSectors(integer7); this.usedSectors.force(integer8, integer9); } } } } private Path getExternalChunkPath(final ChunkPos bje) { final String string3 = "c." + bje.x + "." + bje.z + ".mcc"; return this.externalFileDir.resolve(string3); } @Nullable public synchronized DataInputStream getChunkDataInputStream(final ChunkPos bje) throws IOException { final int integer3 = this.getOffset(bje); if (integer3 == 0) { return null; } final int integer4 = getSectorNumber(integer3); final int integer5 = getNumSectors(integer3); final int integer6 = integer5 * 4096; final ByteBuffer byteBuffer7 = ByteBuffer.allocate(integer6); this.file.read(byteBuffer7, integer4 * 4096); byteBuffer7.flip(); if (byteBuffer7.remaining() < 5) { RegionFile.LOGGER.error("Chunk {} header is truncated: expected {} but read {}", bje, integer6, byteBuffer7.remaining()); return null; } final int integer7 = byteBuffer7.getInt(); final byte byte9 = byteBuffer7.get(); if (integer7 == 0) { RegionFile.LOGGER.warn("Chunk {} is allocated, but stream is missing", bje); return null; } final int integer8 = integer7 - 1; if (isExternalStreamChunk(byte9)) { if (integer8 != 0) { RegionFile.LOGGER.warn("Chunk has both internal and external streams"); } return this.createExternalChunkInputStream(bje, getExternalChunkVersion(byte9)); } if (integer8 > byteBuffer7.remaining()) { RegionFile.LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", bje, integer8, byteBuffer7.remaining()); return null; } if (integer8 < 0) { RegionFile.LOGGER.error("Declared size {} of chunk {} is negative", integer7, bje); return null; } return this.createChunkInputStream(bje, byte9, createStream(byteBuffer7, integer8)); } private static boolean isExternalStreamChunk(final byte byte1) { return (byte1 & 0x80) != 0x0; } private static byte getExternalChunkVersion(final byte byte1) { return (byte)(byte1 & 0xFFFFFF7F); } @Nullable private DataInputStream createChunkInputStream(final ChunkPos bje, final byte byte2, final InputStream inputStream) throws IOException { final RegionFileVersion cbb5 = RegionFileVersion.fromId(byte2); if (cbb5 == null) { RegionFile.LOGGER.error("Chunk {} has invalid chunk stream version {}", bje, byte2); return null; } return new DataInputStream(new BufferedInputStream(cbb5.wrap(inputStream))); } @Nullable private DataInputStream createExternalChunkInputStream(final ChunkPos bje, final byte byte2) throws IOException { final Path path4 = this.getExternalChunkPath(bje); if (!Files.isRegularFile(path4)) { RegionFile.LOGGER.error("External chunk path {} is not file", path4); return null; } return this.createChunkInputStream(bje, byte2, Files.newInputStream(path4)); } private static ByteArrayInputStream createStream(final ByteBuffer byteBuffer, final int integer) { return new ByteArrayInputStream(byteBuffer.array(), byteBuffer.position(), integer); } private int packSectorOffset(final int integer1, final int integer2) { return integer1 << 8 | integer2; } private static int getNumSectors(final int integer) { return integer & 0xFF; } private static int getSectorNumber(final int integer) { return integer >> 8; } private static int sizeToSectors(final int integer) { return (integer + 4096 - 1) / 4096; } public boolean doesChunkExist(final ChunkPos bje) { final int integer3 = this.getOffset(bje); if (integer3 == 0) { return false; } final int integer4 = getSectorNumber(integer3); final int integer5 = getNumSectors(integer3); final ByteBuffer byteBuffer6 = ByteBuffer.allocate(5); try { this.file.read(byteBuffer6, integer4 * 4096); byteBuffer6.flip(); if (byteBuffer6.remaining() != 5) { return false; } final int integer6 = byteBuffer6.getInt(); final byte byte8 = byteBuffer6.get(); if (isExternalStreamChunk(byte8)) { if (!RegionFileVersion.isValidVersion(getExternalChunkVersion(byte8))) { return false; } if (!Files.isRegularFile(this.getExternalChunkPath(bje))) { return false; } } else { if (!RegionFileVersion.isValidVersion(byte8)) { return false; } if (integer6 == 0) { return false; } final int integer7 = integer6 - 1; if (integer7 < 0 || integer7 > 4096 * integer5) { return false; } } } catch (IOException iOException7) { return false; } return true; } public DataOutputStream getChunkDataOutputStream(final ChunkPos bje) throws IOException { return new DataOutputStream(new BufferedOutputStream(this.version.wrap(new ChunkBuffer(bje)))); } protected synchronized void write(final ChunkPos bje, final ByteBuffer byteBuffer) throws IOException { final int integer4 = getOffsetIndex(bje); final int integer5 = this.offsets.get(integer4); final int integer6 = getSectorNumber(integer5); final int integer7 = getNumSectors(integer5); final int integer8 = byteBuffer.remaining(); int integer9 = sizeToSectors(integer8); int integer10; CommitOp b11; if (integer9 >= 256) { final Path path12 = this.getExternalChunkPath(bje); RegionFile.LOGGER.warn("Saving oversized chunk {} ({} bytes} to external file {}", bje, integer8, path12); integer9 = 1; integer10 = this.usedSectors.allocate(integer9); b11 = this.writeToExternalFile(path12, byteBuffer); final ByteBuffer byteBuffer2 = this.createExternalStub(); this.file.write(byteBuffer2, integer10 * 4096); } else { integer10 = this.usedSectors.allocate(integer9); b11 = (() -> Files.deleteIfExists(this.getExternalChunkPath(bje))); this.file.write(byteBuffer, integer10 * 4096); } final int integer11 = (int)(Util.getEpochMillis() / 1000L); this.offsets.put(integer4, this.packSectorOffset(integer10, integer9)); this.timestamps.put(integer4, integer11); this.writeHeader(); b11.run(); if (integer6 != 0) { this.usedSectors.free(integer6, integer7); } } private ByteBuffer createExternalStub() { final ByteBuffer byteBuffer2 = ByteBuffer.allocate(5); byteBuffer2.putInt(1); byteBuffer2.put((byte)(this.version.getId() | 0x80)); byteBuffer2.flip(); return byteBuffer2; } private CommitOp writeToExternalFile(final Path path, final ByteBuffer byteBuffer) throws IOException { final Path path2 = Files.createTempFile(this.externalFileDir, "tmp", null, new FileAttribute[0]); try (final FileChannel fileChannel5 = FileChannel.open(path2, StandardOpenOption.CREATE, StandardOpenOption.WRITE)) { byteBuffer.position(5); fileChannel5.write(byteBuffer); } return () -> Files.move(path2, path, StandardCopyOption.REPLACE_EXISTING); } private void writeHeader() throws IOException { this.header.position(0); this.file.write(this.header, 0L); } private int getOffset(final ChunkPos bje) { return this.offsets.get(getOffsetIndex(bje)); } public boolean hasChunk(final ChunkPos bje) { return this.getOffset(bje) != 0; } private static int getOffsetIndex(final ChunkPos bje) { return bje.getRegionLocalX() + bje.getRegionLocalZ() * 32; } @Override public void close() throws IOException { try { this.padToFullSector(); } finally { try { this.writeHeader(); } finally { try { this.file.force(true); } finally { this.file.close(); } } } } private void padToFullSector() throws IOException { final int integer2 = (int)this.file.size(); final int integer3 = sizeToSectors(integer2) * 4096; if (integer2 != integer3) { final ByteBuffer byteBuffer4 = RegionFile.PADDING_BUFFER.duplicate(); byteBuffer4.position(0); this.file.write(byteBuffer4, integer3 - 1); } } static { LOGGER = LogManager.getLogger(); PADDING_BUFFER = ByteBuffer.allocateDirect(1); } class ChunkBuffer extends ByteArrayOutputStream { private final ChunkPos pos; public ChunkBuffer(final ChunkPos bje) { super(8096); super.write(0); super.write(0); super.write(0); super.write(0); super.write(RegionFile.this.version.getId()); this.pos = bje; } @Override public void close() throws IOException { final ByteBuffer byteBuffer2 = ByteBuffer.wrap(this.buf, 0, this.count); byteBuffer2.putInt(0, this.count - 5 + 1); RegionFile.this.write(this.pos, byteBuffer2); } } interface CommitOp { void run() throws IOException; } }