Initial minecraft-builder-mcp prototype and Gothic hall build

This commit is contained in:
Emil
2026-09-12 23:22:20 +03:00
commit 6682668c16
103 changed files with 10064 additions and 0 deletions
@@ -0,0 +1,32 @@
package io.github.minecraftbuilder.paper;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class ComponentTokensTest {
private static final String ADMIN = "a".repeat(32), AGENT = "b".repeat(32), CAMERA = "c".repeat(32);
@Test void acceptsDistinctBoundedSafeBearerTokens() {
assertDoesNotThrow(() -> ComponentTokens.validate(ADMIN, AGENT, CAMERA));
assertDoesNotThrow(() -> ComponentTokens.validate("a".repeat(512), "b".repeat(512), "c".repeat(506) + ".-_~09"));
}
@Test void rejectsHeaderInjectionWhitespaceUnicodeLengthAndNullWithoutEchoingSecret() {
for (String invalid : new String[]{null, "", "short", "x".repeat(513), "dummy-private-token-that-must-not-leak\n",
"dummy-private-token-that-must-not-leak\r\nInjected: yes", "x".repeat(31) + " ", "x".repeat(31) + "ж"}) {
IllegalArgumentException error = assertThrows(IllegalArgumentException.class, () -> ComponentTokens.validate(ADMIN, AGENT, invalid));
assertEquals("Invalid component tokens: configure three distinct values of 32..512 safe ASCII characters", error.getMessage());
assertFalse(error.getMessage().contains("dummy-private"));
assertFalse(error.getMessage().contains(ADMIN));
assertFalse(error.getMessage().contains(AGENT));
}
assertThrows(IllegalArgumentException.class, () -> ComponentTokens.validate("x\n".repeat(32), AGENT, CAMERA));
assertThrows(IllegalArgumentException.class, () -> ComponentTokens.validate(ADMIN, "x\n".repeat(32), CAMERA));
}
@Test void rejectsSharedCapabilitiesForEveryPair() {
assertThrows(IllegalArgumentException.class, () -> ComponentTokens.validate(ADMIN, ADMIN, CAMERA));
assertThrows(IllegalArgumentException.class, () -> ComponentTokens.validate(ADMIN, AGENT, ADMIN));
assertThrows(IllegalArgumentException.class, () -> ComponentTokens.validate(ADMIN, AGENT, AGENT));
}
}
@@ -0,0 +1,26 @@
package io.github.minecraftbuilder.paper;
import org.junit.jupiter.api.Test;
import java.net.*;
import java.net.http.*;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class RpcServerTest {
private static final String ADMIN = "a".repeat(48), AGENT = "b".repeat(48);
@Test void capabilitiesAreSeparatedAndIngressIsBounded() throws Exception {
try (RpcServer server = new RpcServer(0,ADMIN,AGENT,(m,p,a)->Map.of("method",m))) {
HttpClient client=HttpClient.newHttpClient();
URI endpoint=URI.create("http://127.0.0.1:"+server.port()+"/v1/rpc");
assertEquals(403,send(client,endpoint,"bad","{\"method\":\"project_context\"}").statusCode());
assertEquals(403,send(client,endpoint,AGENT,"{\"method\":\"chat_poll\"}").statusCode());
assertEquals(200,send(client,endpoint,AGENT,"{\"method\":\"project_context\"}").statusCode());
assertEquals(200,send(client,endpoint,ADMIN,"{\"method\":\"chat_poll\"}").statusCode());
assertTrue(send(client,endpoint,ADMIN,"x".repeat(1_048_577)).body().contains("budget_exceeded"));
assertEquals(400,send(client,endpoint,ADMIN,"not-json").statusCode());
}
}
private HttpResponse<String> send(HttpClient client,URI uri,String token,String body) throws Exception {
return client.send(HttpRequest.newBuilder(uri).header("Authorization","Bearer "+token).POST(HttpRequest.BodyPublishers.ofString(body)).build(),HttpResponse.BodyHandlers.ofString());
}
}
@@ -0,0 +1,192 @@
package io.github.minecraftbuilder.paper;
import io.github.minecraftbuilder.core.BlockPos;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.zip.*;
import static org.junit.jupiter.api.Assertions.*;
class SchematicAssetsTest {
@TempDir Path root;
private static final BlockPos ZERO = new BlockPos(0, 0, 0);
private static final String STONE = "minecraft:stone";
private static final String STAIRS = "minecraft:oak_stairs[facing=north,half=bottom,shape=inner_left,waterlogged=false]";
@Test void roundTripKeepsEveryDenseCellAndOriginOffset() throws Exception {
SchematicAssets assets = new SchematicAssets(root);
Map<BlockPos, String> blocks = new LinkedHashMap<>();
blocks.put(new BlockPos(-2, 70, 4), STONE);
blocks.put(new BlockPos(-1, 70, 4), "minecraft:air");
blocks.put(new BlockPos(-2, 71, 4), "minecraft:oak_log[axis=x]");
blocks.put(new BlockPos(-1, 71, 4), STAIRS);
BlockPos anchor = new BlockPos(-1, 69, 3);
var asset = assets.exportSnapshot("Башня", blocks, anchor, 5000);
assertEquals(new BlockPos(-1, 1, 1), asset.offset());
assertEquals(4, asset.blockCount());
assertEquals(5000, asset.dataVersion());
assertEquals(64, asset.sha256().length());
assertEquals(blocks, assets.read(asset.assetId(), anchor, 0));
assertEquals(List.of(asset), assets.list());
try (DataInputStream nbt = new DataInputStream(new GZIPInputStream(Files.newInputStream(root.resolve(asset.assetId() + ".schem"))))) {
assertEquals(10, nbt.readUnsignedByte());
assertEquals("Schematic", nbt.readUTF());
assertEquals(3, nbt.readUnsignedByte()); assertEquals("Version", nbt.readUTF()); assertEquals(2, nbt.readInt());
}
}
@Test void rotationMovesOffsetAndTransformsStairsLogsSlabsWithoutMirroring() throws Exception {
SchematicAssets assets = new SchematicAssets(root);
Map<BlockPos, String> blocks = Map.of(new BlockPos(1, 0, 0), STAIRS, new BlockPos(2, 0, 0), "minecraft:oak_log[axis=x]",
new BlockPos(3, 0, 0), "minecraft:oak_slab[type=top,waterlogged=false]");
var asset = assets.exportSnapshot("Rotation", blocks, ZERO, 5000);
Map<BlockPos, String> rotated = assets.read(asset.assetId(), new BlockPos(10, 64, 20), 90);
assertEquals("minecraft:oak_stairs[facing=east,half=bottom,shape=inner_left,waterlogged=false]", rotated.get(new BlockPos(10, 64, 21)));
assertEquals("minecraft:oak_log[axis=z]", rotated.get(new BlockPos(10, 64, 22)));
assertEquals("minecraft:oak_slab[type=top,waterlogged=false]", rotated.get(new BlockPos(10, 64, 23)));
assertEquals("minecraft:oak_log[axis=x]", assets.read(asset.assetId(), ZERO, 180).get(new BlockPos(-2, 0, 0)));
assertEquals("minecraft:oak_stairs[facing=west,half=bottom,shape=inner_left,waterlogged=false]", assets.read(asset.assetId(), ZERO, 270).get(new BlockPos(0, 0, -1)));
}
@Test void independentlyWrittenFixtureUsesXThenZThenYVarintOrder() throws Exception {
SchematicAssets assets = new SchematicAssets(root);
Files.write(root.resolve("external.schem"), fixture(2, 1, 2, List.of(STONE, "minecraft:air"), new byte[]{0, 1, 1, 0}, out -> {}));
assertEquals(Map.of(ZERO, STONE, new BlockPos(1, 0, 0), "minecraft:air", new BlockPos(0, 0, 1), "minecraft:air", new BlockPos(1, 0, 1), STONE), assets.read("external", ZERO, 0));
}
@Test void paletteIndicesAbove127RoundTripAsMultibyteVarints() throws Exception {
SchematicAssets assets = new SchematicAssets(root);
List<String> palette = new ArrayList<>();
for (String block : List.of("oak_stairs", "spruce_stairs", "cobblestone_stairs", "stone_brick_stairs"))
for (String facing : List.of("north", "east", "south", "west"))
for (String half : List.of("bottom", "top"))
for (String shape : List.of("straight", "inner_left", "inner_right", "outer_left", "outer_right"))
palette.add("minecraft:" + block + "[facing=" + facing + ",half=" + half + ",shape=" + shape + ",waterlogged=false]");
ByteArrayOutputStream indices = new ByteArrayOutputStream();
Map<BlockPos, String> expected = new LinkedHashMap<>();
for (int i = 0; i < palette.size(); i++) {
if (i < 128) indices.write(i); else { indices.write((i & 127) | 128); indices.write(i >>> 7); }
expected.put(new BlockPos(i, 0, 0), palette.get(i));
}
Files.write(root.resolve("varints.schem"), fixture(palette.size(), 1, 1, palette, indices.toByteArray(), out -> {}));
assertEquals(expected, assets.read("varints", ZERO, 0));
var exported = assets.exportSnapshot("Large palette", expected, ZERO, 5000);
assertEquals(expected, assets.read(exported.assetId(), ZERO, 0));
}
@Test void rejectsSparseSnapshotUnsupportedBlocksAndUnrepresentableOrigin() throws Exception {
SchematicAssets assets = new SchematicAssets(root);
assertThrows(IOException.class, () -> assets.exportSnapshot("Gap", Map.of(ZERO, STONE, new BlockPos(2, 0, 0), STONE), ZERO, 5000));
assertThrows(IOException.class, () -> assets.exportSnapshot("Chest", Map.of(ZERO, "minecraft:chest"), ZERO, 5000));
assertThrows(IOException.class, () -> assets.exportSnapshot("Offset", Map.of(new BlockPos(Integer.MIN_VALUE, 0, 0), STONE), new BlockPos(Integer.MAX_VALUE, 0, 0), 5000));
assertEquals(0, assets.list().size());
}
@Test void rejectsEntitiesAndBlockEntitiesRatherThanDroppingThem() throws Exception {
for (String field : List.of("Entities", "BlockEntities")) {
Files.write(root.resolve("entity.schem"), fixture(1, 1, 1, List.of(STONE), new byte[]{0}, out -> {
tag(out, 9, field); out.writeByte(10); out.writeInt(1); tag(out, 8, "Id"); out.writeUTF("minecraft:pig"); out.writeByte(0);
}));
IOException error = assertThrows(IOException.class, () -> new SchematicAssets(root).read("entity", ZERO, 0));
assertTrue(error.getMessage().contains("unsupported"));
}
}
@Test void rejectsBiomeAndUnknownTopLevelData() throws Exception {
Files.write(root.resolve("biome.schem"), fixture(1, 1, 1, List.of(STONE), new byte[]{0}, out -> { tag(out, 7, "BiomeData"); out.writeInt(1); out.writeByte(0); }));
assertThrows(IOException.class, () -> new SchematicAssets(root).read("biome", ZERO, 0));
}
@Test void rejectsBadPaletteMissingExtraAndMalformedVarints() throws Exception {
SchematicAssets assets = new SchematicAssets(root);
for (byte[] data : List.of(new byte[]{2}, new byte[0], new byte[]{0, 0}, new byte[]{(byte) 128}, new byte[]{(byte) 128, 0}, new byte[]{(byte) 255, (byte) 255, (byte) 255, (byte) 255, 127})) {
Files.write(root.resolve("bad.schem"), fixture(1, 1, 1, List.of(STONE), data, out -> {}));
assertThrows(IOException.class, () -> assets.read("bad", ZERO, 0));
}
}
@Test void rejectsDuplicateTagsUnsupportedVersionAndHugeVolume() throws Exception {
SchematicAssets assets = new SchematicAssets(root);
Files.write(root.resolve("duplicate.schem"), fixture(1, 1, 1, List.of(STONE), new byte[]{0}, out -> { tag(out, 3, "Version"); out.writeInt(3); }));
assertThrows(IOException.class, () -> assets.read("duplicate", ZERO, 0));
Files.write(root.resolve("large.schem"), fixture(4097, 1, 1, List.of(STONE), new byte[]{0}, out -> {}));
assertThrows(IOException.class, () -> assets.read("large", ZERO, 0));
byte[] future = fixture(1, 1, 1, List.of(STONE), new byte[]{0}, out -> {});
byte[] raw = new GZIPInputStream(new ByteArrayInputStream(future)).readAllBytes();
// Locate integer following the independent fixture's Version field and replace 2 with 3.
try (ByteArrayInputStream bytes = new ByteArrayInputStream(raw); DataInputStream in = new DataInputStream(bytes)) {
in.readByte(); in.readUTF(); in.readByte(); in.readUTF(); int at = raw.length - bytes.available(); raw[at + 3] = 3;
}
Files.write(root.resolve("future.schem"), gzip(raw));
assertThrows(IOException.class, () -> assets.read("future", ZERO, 0));
}
@Test void rejectsGzipBombTruncationExcessiveDepthAndHostileArrayLength() throws Exception {
SchematicAssets assets = new SchematicAssets(root);
Files.write(root.resolve("bomb.schem"), gzip(new byte[4_194_305]));
assertThrows(IOException.class, () -> assets.read("bomb", ZERO, 0));
Files.write(root.resolve("short.schem"), new byte[]{31, (byte) 139});
assertThrows(IOException.class, () -> assets.read("short", ZERO, 0));
Files.write(root.resolve("array.schem"), fixture(1, 1, 1, List.of(STONE), new byte[]{0}, out -> { tag(out, 11, "Offset"); out.writeInt(Integer.MAX_VALUE); }));
assertThrows(IOException.class, () -> assets.read("array", ZERO, 0));
Files.write(root.resolve("deep.schem"), fixture(1, 1, 1, List.of(STONE), new byte[]{0}, out -> {
tag(out, 10, "Metadata"); for (int i = 0; i < 30; i++) tag(out, 10, "deep"); for (int i = 0; i < 31; i++) out.writeByte(0);
}));
assertThrows(IOException.class, () -> assets.read("deep", ZERO, 0));
}
@Test void rejectsPathsSymlinksAndCoordinateOverflow() throws Exception {
SchematicAssets assets = new SchematicAssets(root);
assertThrows(IOException.class, () -> assets.read("../outside", ZERO, 0));
Path external = Files.createTempFile("mcb-schematic-test-", ".schem");
try {
Files.write(external, fixture(1, 1, 1, List.of(STONE), new byte[]{0}, out -> {}));
Files.createSymbolicLink(root.resolve("linked.schem"), external);
assertThrows(IOException.class, () -> assets.read("linked", ZERO, 0));
Files.delete(root.resolve("linked.schem"));
} finally { Files.deleteIfExists(external); }
var asset = assets.exportSnapshot("Offset", Map.of(new BlockPos(1, 0, 0), STONE), ZERO, 5000);
assertThrows(IOException.class, () -> assets.read(asset.assetId(), new BlockPos(Integer.MAX_VALUE, 0, 0), 0));
assertThrows(IOException.class, () -> assets.read(asset.assetId(), ZERO, 45));
}
@Test void rejectsUnspecifiedRotationalPropertiesAndUnsupportedStates() throws Exception {
assertThrows(IOException.class, () -> SchematicAssets.rotateState("minecraft:oak_stairs", 90));
assertThrows(IOException.class, () -> SchematicAssets.rotateState("minecraft:oak_log", 90));
assertThrows(IOException.class, () -> SchematicAssets.rotateState("minecraft:oak_log[axis=x,axis=z]", 0));
assertThrows(IOException.class, () -> SchematicAssets.rotateState("minecraft:oak_stairs[facing=up]", 0));
assertThrows(IOException.class, () -> SchematicAssets.rotateState("minecraft:oak_slab[type=top,waterlogged=true]", 0));
assertThrows(IOException.class, () -> SchematicAssets.rotateState("minecraft:stone[rotation=4]", 90));
}
@Test void metadataAndPlacementRejectFutureDataVersionOnTheActualRead() throws Exception {
SchematicAssets assets = new SchematicAssets(root);
var asset = assets.exportSnapshot("Versioned", Map.of(ZERO, STONE), ZERO, 5000);
assertEquals(asset, assets.metadata(asset.assetId()));
assertThrows(IOException.class, () -> assets.read(asset.assetId(), ZERO, 0, 4999));
assertEquals(Map.of(ZERO, STONE), assets.read(asset.assetId(), ZERO, 0, 5000));
assertEquals(Map.of(ZERO, STONE), assets.read(asset.assetId(), ZERO, 0, 5001));
}
private interface Extra { void write(DataOutputStream out) throws IOException; }
/** Independent spec fixture writer, intentionally does not call codec encode. */
private static byte[] fixture(int width, int height, int length, List<String> palette, byte[] data, Extra extra) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(bytes)) {
tag(out, 10, "Schematic"); tag(out, 3, "Version"); out.writeInt(2); tag(out, 3, "DataVersion"); out.writeInt(5000);
tag(out, 2, "Width"); out.writeShort(width); tag(out, 2, "Height"); out.writeShort(height); tag(out, 2, "Length"); out.writeShort(length);
tag(out, 3, "PaletteMax"); out.writeInt(palette.size()); tag(out, 10, "Palette");
for (int i = 0; i < palette.size(); i++) { tag(out, 3, palette.get(i)); out.writeInt(i); }
out.writeByte(0); tag(out, 7, "BlockData"); out.writeInt(data.length); out.write(data); extra.write(out); out.writeByte(0);
}
return gzip(bytes.toByteArray());
}
private static void tag(DataOutputStream out, int type, String name) throws IOException { out.writeByte(type); out.writeUTF(name); }
private static byte[] gzip(byte[] bytes) throws IOException {
ByteArrayOutputStream compressed = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(compressed)) { gzip.write(bytes); }
return compressed.toByteArray();
}
}