From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Nassim Jahnke <nassim@njahnke.dev>
Date: Mon, 5 Feb 2024 11:54:04 +0100
Subject: [PATCH] Improve tag parser handling


diff --git a/src/main/java/com/mojang/brigadier/CommandDispatcher.java b/src/main/java/com/mojang/brigadier/CommandDispatcher.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/com/mojang/brigadier/CommandDispatcher.java
+++ b/src/main/java/com/mojang/brigadier/CommandDispatcher.java
@@ -0,0 +0,0 @@ public class CommandDispatcher<S> {
             }
             final CommandContextBuilder<S> context = contextSoFar.copy();
             final StringReader reader = new StringReader(originalReader);
+            boolean stop = false; // Paper - Handle non-recoverable exceptions
             try {
                 try {
                     child.parse(reader, context);
+                    // Paper start - Handle non-recoverable exceptions
+                } catch (final io.papermc.paper.brigadier.TagParseCommandSyntaxException e) {
+                    stop = true;
+                    throw e;
+                    // Paper end - Handle non-recoverable exceptions
                 } catch (final RuntimeException ex) {
                     throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException().createWithContext(reader, ex.getMessage());
                 }
@@ -0,0 +0,0 @@ public class CommandDispatcher<S> {
                 }
                 errors.put(child, ex);
                 reader.setCursor(cursor);
+                if (stop) return new ParseResults<>(contextSoFar, originalReader, errors); // Paper - Handle non-recoverable exceptions
                 continue;
             }
 
diff --git a/src/main/java/io/papermc/paper/brigadier/TagParseCommandSyntaxException.java b/src/main/java/io/papermc/paper/brigadier/TagParseCommandSyntaxException.java
new file mode 100644
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
--- /dev/null
+++ b/src/main/java/io/papermc/paper/brigadier/TagParseCommandSyntaxException.java
@@ -0,0 +0,0 @@
+package io.papermc.paper.brigadier;
+
+import com.mojang.brigadier.LiteralMessage;
+import com.mojang.brigadier.exceptions.CommandSyntaxException;
+import com.mojang.brigadier.exceptions.SimpleCommandExceptionType;
+import net.minecraft.network.chat.Component;
+
+public final class TagParseCommandSyntaxException extends CommandSyntaxException {
+
+    private static final SimpleCommandExceptionType EXCEPTION_TYPE = new SimpleCommandExceptionType(new LiteralMessage("Error parsing NBT"));
+
+    public TagParseCommandSyntaxException(final String message) {
+        super(EXCEPTION_TYPE, Component.literal(message));
+    }
+}
diff --git a/src/main/java/net/minecraft/commands/arguments/selector/SelectorPattern.java b/src/main/java/net/minecraft/commands/arguments/selector/SelectorPattern.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/commands/arguments/selector/SelectorPattern.java
+++ b/src/main/java/net/minecraft/commands/arguments/selector/SelectorPattern.java
@@ -0,0 +0,0 @@ public record SelectorPattern(String pattern, EntitySelector resolved) {
             EntitySelectorParser entitySelectorParser = new EntitySelectorParser(new StringReader(selector), true);
             return DataResult.success(new SelectorPattern(selector, entitySelectorParser.parse()));
         } catch (CommandSyntaxException var2) {
-            return DataResult.error(() -> "Invalid selector component: " + selector + ": " + var2.getMessage());
+            return DataResult.error(() -> "Invalid selector component"); // Paper - limit selector error message
         }
     }
 
diff --git a/src/main/java/net/minecraft/nbt/TagParser.java b/src/main/java/net/minecraft/nbt/TagParser.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/nbt/TagParser.java
+++ b/src/main/java/net/minecraft/nbt/TagParser.java
@@ -0,0 +0,0 @@ public class TagParser {
     }, CompoundTag::toString);
     public static final Codec<CompoundTag> LENIENT_CODEC = Codec.withAlternative(AS_CODEC, CompoundTag.CODEC);
     private final StringReader reader;
+    private int depth; // Paper
 
     public static CompoundTag parseTag(String string) throws CommandSyntaxException {
         return new TagParser(new StringReader(string)).readSingleStruct();
@@ -0,0 +0,0 @@ public class TagParser {
 
     public CompoundTag readStruct() throws CommandSyntaxException {
         this.expect('{');
+        this.increaseDepth(); // Paper
         CompoundTag compoundTag = new CompoundTag();
         this.reader.skipWhitespace();
 
@@ -0,0 +0,0 @@ public class TagParser {
         }
 
         this.expect('}');
+        this.depth--; // Paper
         return compoundTag;
     }
 
@@ -0,0 +0,0 @@ public class TagParser {
         if (!this.reader.canRead()) {
             throw ERROR_EXPECTED_VALUE.createWithContext(this.reader);
         } else {
+            this.increaseDepth(); // Paper
             ListTag listTag = new ListTag();
             TagType<?> tagType = null;
 
@@ -0,0 +0,0 @@ public class TagParser {
             }
 
             this.expect(']');
+            this.depth--; // Paper
             return listTag;
         }
     }
@@ -0,0 +0,0 @@ public class TagParser {
         this.reader.skipWhitespace();
         this.reader.expect(c);
     }
+
+    private void increaseDepth() throws CommandSyntaxException {
+        this.depth++;
+        if (this.depth > 512) {
+            throw new io.papermc.paper.brigadier.TagParseCommandSyntaxException("NBT tag is too complex, depth > 512");
+        }
+    }
 }
diff --git a/src/main/java/net/minecraft/network/chat/ComponentUtils.java b/src/main/java/net/minecraft/network/chat/ComponentUtils.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/network/chat/ComponentUtils.java
+++ b/src/main/java/net/minecraft/network/chat/ComponentUtils.java
@@ -0,0 +0,0 @@ public class ComponentUtils {
         }
     }
 
+    @io.papermc.paper.annotation.DoNotUse // Paper - validate separators - right now this method is only used for separator evaluation. Error on build if this changes to re-evaluate.
     public static Optional<MutableComponent> updateForEntity(@Nullable CommandSourceStack source, Optional<Component> text, @Nullable Entity sender, int depth) throws CommandSyntaxException {
         return text.isPresent() ? Optional.of(updateForEntity(source, text.get(), sender, depth)) : Optional.empty();
     }
 
+    // Paper start - validate separator
+    public static Optional<MutableComponent> updateSeparatorForEntity(@Nullable CommandSourceStack source, Optional<Component> text, @Nullable Entity sender, int depth) throws CommandSyntaxException {
+        if (text.isEmpty() || !isValidSelector(text.get())) return Optional.empty();
+        return Optional.of(updateForEntity(source, text.get(), sender, depth));
+    }
+    public static boolean isValidSelector(final Component component) {
+        final ComponentContents contents = component.getContents();
+
+        if (contents instanceof net.minecraft.network.chat.contents.NbtContents || contents instanceof net.minecraft.network.chat.contents.SelectorContents) return false;
+        if (contents instanceof final net.minecraft.network.chat.contents.TranslatableContents translatableContents) {
+            for (final Object arg : translatableContents.getArgs()) {
+                if (arg instanceof final Component argumentAsComponent && !isValidSelector(argumentAsComponent)) return false;
+            }
+        }
+
+        return true;
+    }
+    // Paper end - validate separator
+
     public static MutableComponent updateForEntity(@Nullable CommandSourceStack source, Component text, @Nullable Entity sender, int depth) throws CommandSyntaxException {
         if (depth > 100) {
             return text.copy();
diff --git a/src/main/java/net/minecraft/network/chat/contents/NbtContents.java b/src/main/java/net/minecraft/network/chat/contents/NbtContents.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/network/chat/contents/NbtContents.java
+++ b/src/main/java/net/minecraft/network/chat/contents/NbtContents.java
@@ -0,0 +0,0 @@ public class NbtContents implements ComponentContents {
             }).map(Tag::getAsString);
             if (this.interpreting) {
                 Component component = DataFixUtils.orElse(
-                    ComponentUtils.updateForEntity(source, this.separator, sender, depth), ComponentUtils.DEFAULT_NO_STYLE_SEPARATOR
+                    ComponentUtils.updateSeparatorForEntity(source, this.separator, sender, depth), ComponentUtils.DEFAULT_NO_STYLE_SEPARATOR // Paper - validate separator
                 );
                 return stream.flatMap(text -> {
                     try {
@@ -0,0 +0,0 @@ public class NbtContents implements ComponentContents {
                     }
                 }).reduce((accumulator, current) -> accumulator.append(component).append(current)).orElseGet(Component::empty);
             } else {
-                return ComponentUtils.updateForEntity(source, this.separator, sender, depth)
+                return ComponentUtils.updateSeparatorForEntity(source, this.separator, sender, depth) // Paper - validate separator
                     .map(
                         text -> stream.map(Component::literal)
                                 .reduce((accumulator, current) -> accumulator.append(text).append(current))
diff --git a/src/main/java/net/minecraft/network/chat/contents/SelectorContents.java b/src/main/java/net/minecraft/network/chat/contents/SelectorContents.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/network/chat/contents/SelectorContents.java
+++ b/src/main/java/net/minecraft/network/chat/contents/SelectorContents.java
@@ -0,0 +0,0 @@ public record SelectorContents(SelectorPattern selector, Optional<Component> sep
         if (source == null) {
             return Component.empty();
         } else {
-            Optional<? extends Component> optional = ComponentUtils.updateForEntity(source, this.separator, sender, depth);
+            Optional<? extends Component> optional = ComponentUtils.updateSeparatorForEntity(source, this.separator, sender, depth); // Paper - validate separator
             return ComponentUtils.formatList(this.selector.resolved().findEntities(source), optional, Entity::getDisplayName);
         }
     }
diff --git a/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java b/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java
+++ b/src/main/java/net/minecraft/network/chat/contents/TranslatableContents.java
@@ -0,0 +0,0 @@ public class TranslatableContents implements ComponentContents {
 
     @Override
     public <T> Optional<T> visit(FormattedText.ContentConsumer<T> visitor) {
+        // Paper start - Count visited parts
+        try {
+            return this.visit(new TranslatableContentConsumer<>(visitor));
+        } catch (IllegalArgumentException ignored) {
+            return visitor.accept("...");
+        }
+    }
+    private <T> Optional<T> visit(TranslatableContentConsumer<T> visitor) {
+        // Paper end - Count visited parts
         this.decompose();
 
         for (FormattedText formattedText : this.decomposedParts) {
@@ -0,0 +0,0 @@ public class TranslatableContents implements ComponentContents {
 
         return Optional.empty();
     }
+    // Paper start - Count visited parts
+    private static final class TranslatableContentConsumer<T> implements FormattedText.ContentConsumer<T> {
+        private static final IllegalArgumentException EX = new IllegalArgumentException("Too long");
+        private final FormattedText.ContentConsumer<T> visitor;
+        private int visited;
+
+        private TranslatableContentConsumer(FormattedText.ContentConsumer<T> visitor) {
+            this.visitor = visitor;
+        }
+
+        @Override
+        public Optional<T> accept(final String asString) {
+            if (visited++ > 32) {
+                throw EX;
+            }
+            return this.visitor.accept(asString);
+        }
+    }
+    // Paper end - Count visited parts
 
     @Override
     public MutableComponent resolve(@Nullable CommandSourceStack source, @Nullable Entity sender, int depth) throws CommandSyntaxException {
diff --git a/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java b/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java
+++ b/src/main/java/net/minecraft/network/protocol/game/ServerboundCommandSuggestionPacket.java
@@ -0,0 +0,0 @@ public class ServerboundCommandSuggestionPacket implements Packet<ServerGamePack
 
     private ServerboundCommandSuggestionPacket(FriendlyByteBuf buf) {
         this.id = buf.readVarInt();
-        this.command = buf.readUtf(32500);
+        this.command = buf.readUtf(2048); // Paper
     }
 
     private void write(FriendlyByteBuf buf) {
diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
             return;
         }
         // Paper end - Don't suggest if tab-complete is disabled
+        // Paper start
+        final int index;
+        if (packet.getCommand().length() > 64 && ((index = packet.getCommand().indexOf(' ')) == -1 || index >= 64)) {
+            this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM);
+            return;
+        }
+        // Paper end
         // Paper start - AsyncTabCompleteEvent
         TAB_COMPLETE_EXECUTOR.execute(() -> this.handleCustomCommandSuggestions0(packet));
     }
@@ -0,0 +0,0 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
     private void sendServerSuggestions(final ServerboundCommandSuggestionPacket packet, final StringReader stringreader) {
         // Paper end - AsyncTabCompleteEvent
         ParseResults<CommandSourceStack> parseresults = this.server.getCommands().getDispatcher().parse(stringreader, this.player.createCommandSourceStack());
+        // Paper start - Handle non-recoverable exceptions
+        if (!parseresults.getExceptions().isEmpty()
+            && parseresults.getExceptions().values().stream().anyMatch(e -> e instanceof io.papermc.paper.brigadier.TagParseCommandSyntaxException)) {
+            this.disconnect(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM);
+            return;
+        }
+        // Paper end - Handle non-recoverable exceptions
 
         this.server.getCommands().getDispatcher().getCompletionSuggestions(parseresults).thenAccept((suggestions) -> {
             // Paper start - Don't tab-complete namespaced commands if send-namespaced is false
