From e935d60d1d00fba5bfd633d46e6d16ae9743ba5c Mon Sep 17 00:00:00 2001 From: CraftBukkit/Spigot Date: Sun, 17 Apr 2022 18:57:35 +1000 Subject: [PATCH] SPIGOT-6972: Root command nodes can leak to client By: md_5 --- .../mojang/brigadier/CommandDispatcher.java | 708 ++++++++++++++++++ .../mojang/brigadier/tree/CommandNode.java | 30 +- 2 files changed, 722 insertions(+), 16 deletions(-) create mode 100644 paper-server/src/main/java/com/mojang/brigadier/CommandDispatcher.java diff --git a/paper-server/src/main/java/com/mojang/brigadier/CommandDispatcher.java b/paper-server/src/main/java/com/mojang/brigadier/CommandDispatcher.java new file mode 100644 index 0000000000..0f13604cf1 --- /dev/null +++ b/paper-server/src/main/java/com/mojang/brigadier/CommandDispatcher.java @@ -0,0 +1,708 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +package com.mojang.brigadier; + +// CHECKSTYLE:OFF +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.context.SuggestionContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import com.mojang.brigadier.tree.RootCommandNode; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; +import java.util.stream.Collectors; + + +/** + * The core command dispatcher, for registering, parsing, and executing commands. + * + * @param a custom "source" type, such as a user or originator of a command + */ +public class CommandDispatcher { + /** + * The string required to separate individual arguments in an input string + * + * @see #ARGUMENT_SEPARATOR_CHAR + */ + public static final String ARGUMENT_SEPARATOR = " "; + + /** + * The char required to separate individual arguments in an input string + * + * @see #ARGUMENT_SEPARATOR + */ + public static final char ARGUMENT_SEPARATOR_CHAR = ' '; + + private static final String USAGE_OPTIONAL_OPEN = "["; + private static final String USAGE_OPTIONAL_CLOSE = "]"; + private static final String USAGE_REQUIRED_OPEN = "("; + private static final String USAGE_REQUIRED_CLOSE = ")"; + private static final String USAGE_OR = "|"; + + private final RootCommandNode root; + + private final Predicate> hasCommand = new Predicate>() { + @Override + public boolean test(final CommandNode input) { + return input != null && (input.getCommand() != null || input.getChildren().stream().anyMatch(hasCommand)); + } + }; + private ResultConsumer consumer = (c, s, r) -> { + }; + + /** + * Create a new {@link CommandDispatcher} with the specified root node. + * + *

This is often useful to copy existing or pre-defined command trees.

+ * + * @param root the existing {@link RootCommandNode} to use as the basis for this tree + */ + public CommandDispatcher(final RootCommandNode root) { + this.root = root; + } + + /** + * Creates a new {@link CommandDispatcher} with an empty command tree. + */ + public CommandDispatcher() { + this(new RootCommandNode<>()); + } + + /** + * Utility method for registering new commands. + * + *

This is a shortcut for calling {@link RootCommandNode#addChild(CommandNode)} after building the provided {@code command}.

+ * + *

As {@link RootCommandNode} can only hold literals, this method will only allow literal arguments.

+ * + * @param command a literal argument builder to add to this command tree + * @return the node added to this tree + */ + public LiteralCommandNode register(final LiteralArgumentBuilder command) { + final LiteralCommandNode build = command.build(); + root.addChild(build); + return build; + } + + /** + * Sets a callback to be informed of the result of every command. + * + * @param consumer the new result consumer to be called + */ + public void setConsumer(final ResultConsumer consumer) { + this.consumer = consumer; + } + + /** + * Parses and executes a given command. + * + *

This is a shortcut to first {@link #parse(StringReader, Object)} and then {@link #execute(ParseResults)}.

+ * + *

It is recommended to parse and execute as separate steps, as parsing is often the most expensive step, and easiest to cache.

+ * + *

If this command returns a value, then it successfully executed something. If it could not parse the command, or the execution was a failure, + * then an exception will be thrown. Most exceptions will be of type {@link CommandSyntaxException}, but it is possible that a {@link RuntimeException} + * may bubble up from the result of a command. The meaning behind the returned result is arbitrary, and will depend + * entirely on what command was performed.

+ * + *

If the command passes through a node that is {@link CommandNode#isFork()} then it will be 'forked'. + * A forked command will not bubble up any {@link CommandSyntaxException}s, and the 'result' returned will turn into + * 'amount of successful commands executes'.

+ * + *

After each and any command is ran, a registered callback given to {@link #setConsumer(ResultConsumer)} + * will be notified of the result and success of the command. You can use that method to gather more meaningful + * results than this method will return, especially when a command forks.

+ * + * @param input a command string to parse & execute + * @param source a custom "source" object, usually representing the originator of this command + * @return a numeric result from a "command" that was performed + * @throws CommandSyntaxException if the command failed to parse or execute + * @throws RuntimeException if the command failed to execute and was not handled gracefully + * @see #parse(String, Object) + * @see #parse(StringReader, Object) + * @see #execute(ParseResults) + * @see #execute(StringReader, Object) + */ + public int execute(final String input, final S source) throws CommandSyntaxException { + return execute(new StringReader(input), source); + } + + /** + * Parses and executes a given command. + * + *

This is a shortcut to first {@link #parse(StringReader, Object)} and then {@link #execute(ParseResults)}.

+ * + *

It is recommended to parse and execute as separate steps, as parsing is often the most expensive step, and easiest to cache.

+ * + *

If this command returns a value, then it successfully executed something. If it could not parse the command, or the execution was a failure, + * then an exception will be thrown. Most exceptions will be of type {@link CommandSyntaxException}, but it is possible that a {@link RuntimeException} + * may bubble up from the result of a command. The meaning behind the returned result is arbitrary, and will depend + * entirely on what command was performed.

+ * + *

If the command passes through a node that is {@link CommandNode#isFork()} then it will be 'forked'. + * A forked command will not bubble up any {@link CommandSyntaxException}s, and the 'result' returned will turn into + * 'amount of successful commands executes'.

+ * + *

After each and any command is ran, a registered callback given to {@link #setConsumer(ResultConsumer)} + * will be notified of the result and success of the command. You can use that method to gather more meaningful + * results than this method will return, especially when a command forks.

+ * + * @param input a command string to parse & execute + * @param source a custom "source" object, usually representing the originator of this command + * @return a numeric result from a "command" that was performed + * @throws CommandSyntaxException if the command failed to parse or execute + * @throws RuntimeException if the command failed to execute and was not handled gracefully + * @see #parse(String, Object) + * @see #parse(StringReader, Object) + * @see #execute(ParseResults) + * @see #execute(String, Object) + */ + public int execute(final StringReader input, final S source) throws CommandSyntaxException { + final ParseResults parse = parse(input, source); + return execute(parse); + } + + /** + * Executes a given pre-parsed command. + * + *

If this command returns a value, then it successfully executed something. If the execution was a failure, + * then an exception will be thrown. + * Most exceptions will be of type {@link CommandSyntaxException}, but it is possible that a {@link RuntimeException} + * may bubble up from the result of a command. The meaning behind the returned result is arbitrary, and will depend + * entirely on what command was performed.

+ * + *

If the command passes through a node that is {@link CommandNode#isFork()} then it will be 'forked'. + * A forked command will not bubble up any {@link CommandSyntaxException}s, and the 'result' returned will turn into + * 'amount of successful commands executes'.

+ * + *

After each and any command is ran, a registered callback given to {@link #setConsumer(ResultConsumer)} + * will be notified of the result and success of the command. You can use that method to gather more meaningful + * results than this method will return, especially when a command forks.

+ * + * @param parse the result of a successful {@link #parse(StringReader, Object)} + * @return a numeric result from a "command" that was performed. + * @throws CommandSyntaxException if the command failed to parse or execute + * @throws RuntimeException if the command failed to execute and was not handled gracefully + * @see #parse(String, Object) + * @see #parse(StringReader, Object) + * @see #execute(String, Object) + * @see #execute(StringReader, Object) + */ + public int execute(final ParseResults parse) throws CommandSyntaxException { + if (parse.getReader().canRead()) { + if (parse.getExceptions().size() == 1) { + throw parse.getExceptions().values().iterator().next(); + } else if (parse.getContext().getRange().isEmpty()) { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(parse.getReader()); + } else { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownArgument().createWithContext(parse.getReader()); + } + } + + int result = 0; + int successfulForks = 0; + boolean forked = false; + boolean foundCommand = false; + final String command = parse.getReader().getString(); + final CommandContext original = parse.getContext().build(command); + List> contexts = Collections.singletonList(original); + ArrayList> next = null; + + while (contexts != null) { + final int size = contexts.size(); + for (int i = 0; i < size; i++) { + final CommandContext context = contexts.get(i); + final CommandContext child = context.getChild(); + if (child != null) { + forked |= context.isForked(); + if (child.hasNodes()) { + foundCommand = true; + final RedirectModifier modifier = context.getRedirectModifier(); + if (modifier == null) { + if (next == null) { + next = new ArrayList<>(1); + } + next.add(child.copyFor(context.getSource())); + } else { + try { + final Collection results = modifier.apply(context); + if (!results.isEmpty()) { + if (next == null) { + next = new ArrayList<>(results.size()); + } + for (final S source : results) { + next.add(child.copyFor(source)); + } + } + } catch (final CommandSyntaxException ex) { + consumer.onCommandComplete(context, false, 0); + if (!forked) { + throw ex; + } + } + } + } + } else if (context.getCommand() != null) { + foundCommand = true; + try { + final int value = context.getCommand().run(context); + result += value; + consumer.onCommandComplete(context, true, value); + successfulForks++; + } catch (final CommandSyntaxException ex) { + consumer.onCommandComplete(context, false, 0); + if (!forked) { + throw ex; + } + } + } + } + + contexts = next; + next = null; + } + + if (!foundCommand) { + consumer.onCommandComplete(original, false, 0); + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherUnknownCommand().createWithContext(parse.getReader()); + } + + return forked ? successfulForks : result; + } + + /** + * Parses a given command. + * + *

The result of this method can be cached, and it is advised to do so where appropriate. Parsing is often the + * most expensive step, and this allows you to essentially "precompile" a command if it will be ran often.

+ * + *

If the command passes through a node that is {@link CommandNode#isFork()} then the resulting context will be marked as 'forked'. + * Forked contexts may contain child contexts, which may be modified by the {@link RedirectModifier} attached to the fork.

+ * + *

Parsing a command can never fail, you will always be provided with a new {@link ParseResults}. + * However, that does not mean that it will always parse into a valid command. You should inspect the returned results + * to check for validity. If its {@link ParseResults#getReader()} {@link StringReader#canRead()} then it did not finish + * parsing successfully. You can use that position as an indicator to the user where the command stopped being valid. + * You may inspect {@link ParseResults#getExceptions()} if you know the parse failed, as it will explain why it could + * not find any valid commands. It may contain multiple exceptions, one for each "potential node" that it could have visited, + * explaining why it did not go down that node.

+ * + *

When you eventually call {@link #execute(ParseResults)} with the result of this method, the above error checking + * will occur. You only need to inspect it yourself if you wish to handle that yourself.

+ * + * @param command a command string to parse + * @param source a custom "source" object, usually representing the originator of this command + * @return the result of parsing this command + * @see #parse(StringReader, Object) + * @see #execute(ParseResults) + * @see #execute(String, Object) + */ + public ParseResults parse(final String command, final S source) { + return parse(new StringReader(command), source); + } + + /** + * Parses a given command. + * + *

The result of this method can be cached, and it is advised to do so where appropriate. Parsing is often the + * most expensive step, and this allows you to essentially "precompile" a command if it will be ran often.

+ * + *

If the command passes through a node that is {@link CommandNode#isFork()} then the resulting context will be marked as 'forked'. + * Forked contexts may contain child contexts, which may be modified by the {@link RedirectModifier} attached to the fork.

+ * + *

Parsing a command can never fail, you will always be provided with a new {@link ParseResults}. + * However, that does not mean that it will always parse into a valid command. You should inspect the returned results + * to check for validity. If its {@link ParseResults#getReader()} {@link StringReader#canRead()} then it did not finish + * parsing successfully. You can use that position as an indicator to the user where the command stopped being valid. + * You may inspect {@link ParseResults#getExceptions()} if you know the parse failed, as it will explain why it could + * not find any valid commands. It may contain multiple exceptions, one for each "potential node" that it could have visited, + * explaining why it did not go down that node.

+ * + *

When you eventually call {@link #execute(ParseResults)} with the result of this method, the above error checking + * will occur. You only need to inspect it yourself if you wish to handle that yourself.

+ * + * @param command a command string to parse + * @param source a custom "source" object, usually representing the originator of this command + * @return the result of parsing this command + * @see #parse(String, Object) + * @see #execute(ParseResults) + * @see #execute(String, Object) + */ + public ParseResults parse(final StringReader command, final S source) { + final CommandContextBuilder context = new CommandContextBuilder<>(this, source, root, command.getCursor()); + return parseNodes(root, command, context); + } + + private ParseResults parseNodes(final CommandNode node, final StringReader originalReader, final CommandContextBuilder contextSoFar) { + final S source = contextSoFar.getSource(); + Map, CommandSyntaxException> errors = null; + List> potentials = null; + final int cursor = originalReader.getCursor(); + + for (final CommandNode child : node.getRelevantNodes(originalReader)) { + if (!child.canUse(source)) { + continue; + } + final CommandContextBuilder context = contextSoFar.copy(); + final StringReader reader = new StringReader(originalReader); + try { + try { + child.parse(reader, context); + } catch (final RuntimeException ex) { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherParseException().createWithContext(reader, ex.getMessage()); + } + if (reader.canRead()) { + if (reader.peek() != ARGUMENT_SEPARATOR_CHAR) { + throw CommandSyntaxException.BUILT_IN_EXCEPTIONS.dispatcherExpectedArgumentSeparator().createWithContext(reader); + } + } + } catch (final CommandSyntaxException ex) { + if (errors == null) { + errors = new LinkedHashMap<>(); + } + errors.put(child, ex); + reader.setCursor(cursor); + continue; + } + + context.withCommand(child.getCommand()); + if (reader.canRead(child.getRedirect() == null ? 2 : 1)) { + reader.skip(); + if (child.getRedirect() != null) { + final CommandContextBuilder childContext = new CommandContextBuilder<>(this, source, child.getRedirect(), reader.getCursor()); + final ParseResults parse = parseNodes(child.getRedirect(), reader, childContext); + context.withChild(parse.getContext()); + return new ParseResults<>(context, parse.getReader(), parse.getExceptions()); + } else { + final ParseResults parse = parseNodes(child, reader, context); + if (potentials == null) { + potentials = new ArrayList<>(1); + } + potentials.add(parse); + } + } else { + if (potentials == null) { + potentials = new ArrayList<>(1); + } + potentials.add(new ParseResults<>(context, reader, Collections.emptyMap())); + } + } + + if (potentials != null) { + if (potentials.size() > 1) { + potentials.sort((a, b) -> { + if (!a.getReader().canRead() && b.getReader().canRead()) { + return -1; + } + if (a.getReader().canRead() && !b.getReader().canRead()) { + return 1; + } + if (a.getExceptions().isEmpty() && !b.getExceptions().isEmpty()) { + return -1; + } + if (!a.getExceptions().isEmpty() && b.getExceptions().isEmpty()) { + return 1; + } + return 0; + }); + } + return potentials.get(0); + } + + return new ParseResults<>(contextSoFar, originalReader, errors == null ? Collections.emptyMap() : errors); + } + + /** + * Gets all possible executable commands following the given node. + * + *

You may use {@link #getRoot()} as a target to get all usage data for the entire command tree.

+ * + *

The returned syntax will be in "simple" form: {@code } and {@code literal}. "Optional" nodes will be + * listed as multiple entries: the parent node, and the child nodes. + * For example, a required literal "foo" followed by an optional param "int" will be two nodes:

+ *
    + *
  • {@code foo}
  • + *
  • {@code foo }
  • + *
+ * + *

The path to the specified node will not be prepended to the output, as there can theoretically be many + * ways to reach a given node. It will only give you paths relative to the specified node, not absolute from root.

+ * + * @param node target node to get child usage strings for + * @param source a custom "source" object, usually representing the originator of this command + * @param restricted if true, commands that the {@code source} cannot access will not be mentioned + * @return array of full usage strings under the target node + */ + public String[] getAllUsage(final CommandNode node, final S source, final boolean restricted) { + final ArrayList result = new ArrayList<>(); + getAllUsage(node, source, result, "", restricted); + return result.toArray(new String[result.size()]); + } + + private void getAllUsage(final CommandNode node, final S source, final ArrayList result, final String prefix, final boolean restricted) { + if (restricted && !node.canUse(source)) { + return; + } + + if (node.getCommand() != null) { + result.add(prefix); + } + + if (node.getRedirect() != null) { + final String redirect = node.getRedirect() == root ? "..." : "-> " + node.getRedirect().getUsageText(); + result.add(prefix.isEmpty() ? node.getUsageText() + ARGUMENT_SEPARATOR + redirect : prefix + ARGUMENT_SEPARATOR + redirect); + } else if (!node.getChildren().isEmpty()) { + for (final CommandNode child : node.getChildren()) { + getAllUsage(child, source, result, prefix.isEmpty() ? child.getUsageText() : prefix + ARGUMENT_SEPARATOR + child.getUsageText(), restricted); + } + } + } + + /** + * Gets the possible executable commands from a specified node. + * + *

You may use {@link #getRoot()} as a target to get usage data for the entire command tree.

+ * + *

The returned syntax will be in "smart" form: {@code }, {@code literal}, {@code [optional]} and {@code (either|or)}. + * These forms may be mixed and matched to provide as much information about the child nodes as it can, without being too verbose. + * For example, a required literal "foo" followed by an optional param "int" can be compressed into one string:

+ *
    + *
  • {@code foo []}
  • + *
+ * + *

The path to the specified node will not be prepended to the output, as there can theoretically be many + * ways to reach a given node. It will only give you paths relative to the specified node, not absolute from root.

+ * + *

The returned usage will be restricted to only commands that the provided {@code source} can use.

+ * + * @param node target node to get child usage strings for + * @param source a custom "source" object, usually representing the originator of this command + * @return array of full usage strings under the target node + */ + public Map, String> getSmartUsage(final CommandNode node, final S source) { + final Map, String> result = new LinkedHashMap<>(); + + final boolean optional = node.getCommand() != null; + for (final CommandNode child : node.getChildren()) { + final String usage = getSmartUsage(child, source, optional, false); + if (usage != null) { + result.put(child, usage); + } + } + return result; + } + + private String getSmartUsage(final CommandNode node, final S source, final boolean optional, final boolean deep) { + if (!node.canUse(source)) { + return null; + } + + final String self = optional ? USAGE_OPTIONAL_OPEN + node.getUsageText() + USAGE_OPTIONAL_CLOSE : node.getUsageText(); + final boolean childOptional = node.getCommand() != null; + final String open = childOptional ? USAGE_OPTIONAL_OPEN : USAGE_REQUIRED_OPEN; + final String close = childOptional ? USAGE_OPTIONAL_CLOSE : USAGE_REQUIRED_CLOSE; + + if (!deep) { + if (node.getRedirect() != null) { + final String redirect = node.getRedirect() == root ? "..." : "-> " + node.getRedirect().getUsageText(); + return self + ARGUMENT_SEPARATOR + redirect; + } else { + final Collection> children = node.getChildren().stream().filter(c -> c.canUse(source)).collect(Collectors.toList()); + if (children.size() == 1) { + final String usage = getSmartUsage(children.iterator().next(), source, childOptional, childOptional); + if (usage != null) { + return self + ARGUMENT_SEPARATOR + usage; + } + } else if (children.size() > 1) { + final Set childUsage = new LinkedHashSet<>(); + for (final CommandNode child : children) { + final String usage = getSmartUsage(child, source, childOptional, true); + if (usage != null) { + childUsage.add(usage); + } + } + if (childUsage.size() == 1) { + final String usage = childUsage.iterator().next(); + return self + ARGUMENT_SEPARATOR + (childOptional ? USAGE_OPTIONAL_OPEN + usage + USAGE_OPTIONAL_CLOSE : usage); + } else if (childUsage.size() > 1) { + final StringBuilder builder = new StringBuilder(open); + int count = 0; + for (final CommandNode child : children) { + if (count > 0) { + builder.append(USAGE_OR); + } + builder.append(child.getUsageText()); + count++; + } + if (count > 0) { + builder.append(close); + return self + ARGUMENT_SEPARATOR + builder.toString(); + } + } + } + } + } + + return self; + } + + /** + * Gets suggestions for a parsed input string on what comes next. + * + *

As it is ultimately up to custom argument types to provide suggestions, it may be an asynchronous operation, + * for example getting in-game data or player names etc. As such, this method returns a future and no guarantees + * are made to when or how the future completes.

+ * + *

The suggestions provided will be in the context of the end of the parsed input string, but may suggest + * new or replacement strings for earlier in the input string. For example, if the end of the string was + * {@code foobar} but an argument preferred it to be {@code minecraft:foobar}, it will suggest a replacement for that + * whole segment of the input.

+ * + * @param parse the result of a {@link #parse(StringReader, Object)} + * @return a future that will eventually resolve into a {@link Suggestions} object + */ + public CompletableFuture getCompletionSuggestions(final ParseResults parse) { + return getCompletionSuggestions(parse, parse.getReader().getTotalLength()); + } + + public CompletableFuture getCompletionSuggestions(final ParseResults parse, int cursor) { + final CommandContextBuilder context = parse.getContext(); + + final SuggestionContext nodeBeforeCursor = context.findSuggestionContext(cursor); + final CommandNode parent = nodeBeforeCursor.parent; + final int start = Math.min(nodeBeforeCursor.startPos, cursor); + + final String fullInput = parse.getReader().getString(); + final String truncatedInput = fullInput.substring(0, cursor); + final String truncatedInputLowerCase = truncatedInput.toLowerCase(Locale.ROOT); + @SuppressWarnings("unchecked") final CompletableFuture[] futures = new CompletableFuture[parent.getChildren().size()]; + int i = 0; + for (final CommandNode node : parent.getChildren()) { + CompletableFuture future = Suggestions.empty(); + try { + if (node.canUse(parse.getContext().getSource())) future = node.listSuggestions(context.build(truncatedInput), new SuggestionsBuilder(truncatedInput, truncatedInputLowerCase, start)); // CraftBukkit + } catch (final CommandSyntaxException ignored) { + } + futures[i++] = future; + } + + final CompletableFuture result = new CompletableFuture<>(); + CompletableFuture.allOf(futures).thenRun(() -> { + final List suggestions = new ArrayList<>(); + for (final CompletableFuture future : futures) { + suggestions.add(future.join()); + } + result.complete(Suggestions.merge(fullInput, suggestions)); + }); + + return result; + } + + /** + * Gets the root of this command tree. + * + *

This is often useful as a target of a {@link com.mojang.brigadier.builder.ArgumentBuilder#redirect(CommandNode)}, + * {@link #getAllUsage(CommandNode, Object, boolean)} or {@link #getSmartUsage(CommandNode, Object)}. + * You may also use it to clone the command tree via {@link #CommandDispatcher(RootCommandNode)}.

+ * + * @return root of the command tree + */ + public RootCommandNode getRoot() { + return root; + } + + /** + * Finds a valid path to a given node on the command tree. + * + *

There may theoretically be multiple paths to a node on the tree, especially with the use of forking or redirecting. + * As such, this method makes no guarantees about which path it finds. It will not look at forks or redirects, + * and find the first instance of the target node on the tree.

+ * + *

The only guarantee made is that for the same command tree and the same version of this library, the result of + * this method will always be a valid input for {@link #findNode(Collection)}, which should return the same node + * as provided to this method.

+ * + * @param target the target node you are finding a path for + * @return a path to the resulting node, or an empty list if it was not found + */ + public Collection getPath(final CommandNode target) { + final List>> nodes = new ArrayList<>(); + addPaths(root, nodes, new ArrayList<>()); + + for (final List> list : nodes) { + if (list.get(list.size() - 1) == target) { + final List result = new ArrayList<>(list.size()); + for (final CommandNode node : list) { + if (node != root) { + result.add(node.getName()); + } + } + return result; + } + } + + return Collections.emptyList(); + } + + /** + * Finds a node by its path + * + *

Paths may be generated with {@link #getPath(CommandNode)}, and are guaranteed (for the same tree, and the + * same version of this library) to always produce the same valid node by this method.

+ * + *

If a node could not be found at the specified path, then {@code null} will be returned.

+ * + * @param path a generated path to a node + * @return the node at the given path, or null if not found + */ + public CommandNode findNode(final Collection path) { + CommandNode node = root; + for (final String name : path) { + node = node.getChild(name); + if (node == null) { + return null; + } + } + return node; + } + + /** + * Scans the command tree for potential ambiguous commands. + * + *

This is a shortcut for {@link CommandNode#findAmbiguities(AmbiguityConsumer)} on {@link #getRoot()}.

+ * + *

Ambiguities are detected by testing every {@link CommandNode#getExamples()} on one node verses every sibling + * node. This is not fool proof, and relies a lot on the providers of the used argument types to give good examples.

+ * + * @param consumer a callback to be notified of potential ambiguities + */ + public void findAmbiguities(final AmbiguityConsumer consumer) { + root.findAmbiguities(consumer); + } + + private void addPaths(final CommandNode node, final List>> result, final List> parents) { + final List> current = new ArrayList<>(parents); + current.add(node); + result.add(current); + + for (final CommandNode child : node.getChildren()) { + addPaths(child, result, current); + } + } +} diff --git a/paper-server/src/main/java/com/mojang/brigadier/tree/CommandNode.java b/paper-server/src/main/java/com/mojang/brigadier/tree/CommandNode.java index f0f25fa40d..9be5c58f97 100644 --- a/paper-server/src/main/java/com/mojang/brigadier/tree/CommandNode.java +++ b/paper-server/src/main/java/com/mojang/brigadier/tree/CommandNode.java @@ -1,9 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + package com.mojang.brigadier.tree; // CHECKSTYLE:OFF -import com.google.common.collect.ComparisonChain; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; import com.mojang.brigadier.AmbiguityConsumer; import com.mojang.brigadier.Command; import com.mojang.brigadier.RedirectModifier; @@ -17,19 +17,19 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.function.Predicate; -import java.util.stream.Collectors; import net.minecraft.commands.CommandListenerWrapper; // CraftBukkit public abstract class CommandNode implements Comparable> { - private Map> children = Maps.newLinkedHashMap(); - private Map> literals = Maps.newLinkedHashMap(); - private Map> arguments = Maps.newLinkedHashMap(); + private final Map> children = new LinkedHashMap<>(); + private final Map> literals = new LinkedHashMap<>(); + private final Map> arguments = new LinkedHashMap<>(); private final Predicate requirement; private final CommandNode redirect; private final RedirectModifier modifier; @@ -107,12 +107,10 @@ public abstract class CommandNode implements Comparable> { arguments.put(node.getName(), (ArgumentCommandNode) node); } } - - children = children.entrySet().stream().sorted(Map.Entry.comparingByValue()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); } public void findAmbiguities(final AmbiguityConsumer consumer) { - Set matches = Sets.newHashSet(); + Set matches = new HashSet<>(); for (final CommandNode child : children.values()) { for (final CommandNode sibling : children.values()) { @@ -128,7 +126,7 @@ public abstract class CommandNode implements Comparable> { if (matches.size() > 0) { consumer.ambiguous(this, child, sibling, matches); - matches = Sets.newHashSet(); + matches = new HashSet<>(); } } @@ -193,11 +191,11 @@ public abstract class CommandNode implements Comparable> { @Override public int compareTo(final CommandNode o) { - return ComparisonChain - .start() - .compareTrueFirst(this instanceof LiteralCommandNode, o instanceof LiteralCommandNode) - .compare(getSortedKey(), o.getSortedKey()) - .result(); + if (this instanceof LiteralCommandNode == o instanceof LiteralCommandNode) { + return getSortedKey().compareTo(o.getSortedKey()); + } + + return (o instanceof LiteralCommandNode) ? 1 : -1; } public boolean isFork() {