diff --git a/src/main/java/com/mojang/brigadier/CommandDispatcher.java b/src/main/java/com/mojang/brigadier/CommandDispatcher.java index bee21f5..aab12a5 100644 --- a/src/main/java/com/mojang/brigadier/CommandDispatcher.java +++ b/src/main/java/com/mojang/brigadier/CommandDispatcher.java @@ -25,6 +25,7 @@ public class CommandDispatcher { public static final ParameterizedCommandExceptionType ERROR_UNKNOWN_ARGUMENT = new ParameterizedCommandExceptionType("command.unknown.argument", "Incorrect argument for command, couldn't parse: ${argument}", "argument"); public static final String 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 = "("; diff --git a/src/main/java/com/mojang/brigadier/arguments/StringArgumentType.java b/src/main/java/com/mojang/brigadier/arguments/StringArgumentType.java new file mode 100644 index 0000000..bb170d8 --- /dev/null +++ b/src/main/java/com/mojang/brigadier/arguments/StringArgumentType.java @@ -0,0 +1,134 @@ +package com.mojang.brigadier.arguments; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.context.FixedParsedArgument; +import com.mojang.brigadier.context.ParsedArgument; +import com.mojang.brigadier.exceptions.CommandException; +import com.mojang.brigadier.exceptions.ParameterizedCommandExceptionType; +import com.mojang.brigadier.exceptions.SimpleCommandExceptionType; + +import java.util.Set; + +public class StringArgumentType implements CommandArgumentType { + public static final ParameterizedCommandExceptionType ERROR_INVALID_ESCAPE = new ParameterizedCommandExceptionType("argument.string.escape.invalid", "Unknown or invalid escape sequence: ${input}", "input"); + public static final SimpleCommandExceptionType ERROR_UNEXPECTED_ESCAPE = new SimpleCommandExceptionType("argument.string.escape.unexpected", "Unexpected escape sequence, please quote the whole argument"); + public static final SimpleCommandExceptionType ERROR_UNEXPECTED_START_OF_QUOTE = new SimpleCommandExceptionType("argument.string.quote.unexpected_start", "Unexpected start-of-quote character (\"), please quote the whole argument"); + public static final SimpleCommandExceptionType ERROR_UNEXPECTED_END_OF_QUOTE = new SimpleCommandExceptionType("argument.string.quote.unexpected_end", "Unexpected end-of-quote character (\"), it must be at the end or followed by a space (' ') for the next argument"); + public static final SimpleCommandExceptionType ERROR_EXPECTED_END_OF_QUOTE = new SimpleCommandExceptionType("argument.string.quote.expected_end", "Expected end-of-quote character (\") but found no more input"); + private final StringType type; + + private StringArgumentType(StringType type) { + this.type = type; + } + + public static StringArgumentType word() { + return new StringArgumentType(StringType.SINGLE_WORLD); + } + + public static StringArgumentType string() { + return new StringArgumentType(StringType.QUOTABLE_PHRASE); + } + + public static StringArgumentType greedyString() { + return new StringArgumentType(StringType.GREEDY_PHRASE); + } + + public static String getString(CommandContext context, String name) { + return context.getArgument(name, String.class); + } + + @Override + public ParsedArgument parse(String command) throws CommandException { + if (type == StringType.GREEDY_PHRASE) { + return new FixedParsedArgument<>(command, command); + } else if (type == StringType.SINGLE_WORLD) { + int index = command.indexOf(CommandDispatcher.ARGUMENT_SEPARATOR); + if (index > 0) { + final String word = command.substring(0, index); + return new FixedParsedArgument<>(word, word); + } else { + return new FixedParsedArgument<>(command, command); + } + } else { + StringBuilder result = new StringBuilder(); + int i = 0; + boolean escaped = false; + boolean quoted = false; + while (i < command.length()) { + char c = command.charAt(i); + if (escaped) { + if (c == '"' || c == '\\') { + result.append(c); + } else { + throw ERROR_INVALID_ESCAPE.create("\\" + c); + } + escaped = false; + } else if (c == '\\') { + if (quoted) { + escaped = true; + } else { + throw ERROR_UNEXPECTED_ESCAPE.create(); + } + } else if (c == '"') { + if (i == 0) { + quoted = true; + } else if (!quoted) { + throw ERROR_UNEXPECTED_START_OF_QUOTE.create(); + } else if (i == command.length() - 1 || command.charAt(i + 1) == CommandDispatcher.ARGUMENT_SEPARATOR_CHAR) { + i++; + break; + } else { + throw ERROR_UNEXPECTED_END_OF_QUOTE.create(); + } + } else if (!quoted && c == CommandDispatcher.ARGUMENT_SEPARATOR_CHAR) { + break; + } else if (quoted && i == command.length() - 1) { + throw ERROR_EXPECTED_END_OF_QUOTE.create(); + } else { + result.append(c); + } + + i++; + } + return new FixedParsedArgument<>(command.substring(0, i), result.toString()); + } + } + + @Override + public void listSuggestions(String command, Set output) { + } + + @Override + public String toString() { + return "string()"; + } + + public static String escapeIfRequired(String input) { + if (input.contains("\\") || input.contains("\"") || input.contains(CommandDispatcher.ARGUMENT_SEPARATOR)) { + return escape(input); + } + return input; + } + + private static String escape(String input) { + StringBuilder result = new StringBuilder("\""); + + for (int i = 0; i < input.length(); i++) { + final char c = input.charAt(i); + if (c == '\\' || c == '"') { + result.append('\\'); + } + result.append(c); + } + + result.append("\""); + return result.toString(); + } + + public enum StringType { + SINGLE_WORLD, + QUOTABLE_PHRASE, + GREEDY_PHRASE, + } +} diff --git a/src/test/java/com/mojang/brigadier/arguments/StringArgumentTypeTest.java b/src/test/java/com/mojang/brigadier/arguments/StringArgumentTypeTest.java new file mode 100644 index 0000000..6bead93 --- /dev/null +++ b/src/test/java/com/mojang/brigadier/arguments/StringArgumentTypeTest.java @@ -0,0 +1,270 @@ +package com.mojang.brigadier.arguments; + +import com.google.common.collect.Sets; +import com.mojang.brigadier.context.ParsedArgument; +import com.mojang.brigadier.exceptions.CommandException; +import com.sun.xml.internal.ws.api.ComponentEx; +import org.junit.Test; + +import java.util.Collections; +import java.util.Set; + +import static com.mojang.brigadier.arguments.StringArgumentType.ERROR_EXPECTED_END_OF_QUOTE; +import static com.mojang.brigadier.arguments.StringArgumentType.ERROR_INVALID_ESCAPE; +import static com.mojang.brigadier.arguments.StringArgumentType.ERROR_UNEXPECTED_END_OF_QUOTE; +import static com.mojang.brigadier.arguments.StringArgumentType.ERROR_UNEXPECTED_ESCAPE; +import static com.mojang.brigadier.arguments.StringArgumentType.ERROR_UNEXPECTED_START_OF_QUOTE; +import static com.mojang.brigadier.arguments.StringArgumentType.escapeIfRequired; +import static com.mojang.brigadier.arguments.StringArgumentType.greedyString; +import static com.mojang.brigadier.arguments.StringArgumentType.string; +import static com.mojang.brigadier.arguments.StringArgumentType.word; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasToString; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +public class StringArgumentTypeTest { + private StringArgumentType type; + + @Test + public void testParseWord() throws Exception { + type = word(); + ParsedArgument result = type.parse("hello world"); + + assertThat(result.getRaw(), is("hello")); + assertThat(result.getResult(), is("hello")); + } + + @Test + public void testParseWord_empty() throws Exception { + type = word(); + ParsedArgument result = type.parse(""); + + assertThat(result.getRaw(), is("")); + assertThat(result.getResult(), is("")); + } + + @Test + public void testParseWord_simple() throws Exception { + type = word(); + ParsedArgument result = type.parse("hello"); + + assertThat(result.getRaw(), is("hello")); + assertThat(result.getResult(), is("hello")); + } + + @Test + public void testParseString() throws Exception { + type = string(); + ParsedArgument result = type.parse("hello world"); + + assertThat(result.getRaw(), is("hello")); + assertThat(result.getResult(), is("hello")); + } + + @Test + public void testParseGreedyString() throws Exception { + type = greedyString(); + ParsedArgument result = type.parse("hello world"); + + assertThat(result.getRaw(), is("hello world")); + assertThat(result.getResult(), is("hello world")); + } + + @Test + public void testParse() throws Exception { + type = string(); + ParsedArgument result = type.parse("hello"); + + assertThat(result.getRaw(), is("hello")); + assertThat(result.getResult(), is("hello")); + } + + @Test + public void testParseWordQuoted() throws Exception { + type = word(); + ParsedArgument result = type.parse("\"hello \\\" world\""); + + assertThat(result.getRaw(), is("\"hello")); + assertThat(result.getResult(), is("\"hello")); + } + + @Test + public void testParseQuoted() throws Exception { + type = string(); + ParsedArgument result = type.parse("\"hello \\\" world\""); + + assertThat(result.getRaw(), is("\"hello \\\" world\"")); + assertThat(result.getResult(), is("hello \" world")); + } + + @Test + public void testParseQuotedWithRemaining() throws Exception { + type = string(); + ParsedArgument result = type.parse("\"hello \\\" world\" with remaining"); + + assertThat(result.getRaw(), is("\"hello \\\" world\"")); + assertThat(result.getResult(), is("hello \" world")); + } + + @Test + public void testParseNotQuoted() throws Exception { + type = string(); + ParsedArgument result = type.parse("hello world"); + + assertThat(result.getRaw(), is("hello")); + assertThat(result.getResult(), is("hello")); + } + + @Test + public void testParseInvalidQuote_earlyUnquote() throws Exception { + try { + type = string(); + type.parse("\"hello \"world"); + fail(); + } catch (CommandException e) { + assertThat(e.getType(), is(ERROR_UNEXPECTED_END_OF_QUOTE)); + assertThat(e.getData(), is(equalTo(Collections.emptyMap()))); + } + } + + @Test + public void testParseQuote_earlyUnquoteWithRemaining() throws Exception { + type = string(); + ParsedArgument result = type.parse("\"hello\" world"); + + assertThat(result.getRaw(), is("\"hello\"")); + assertThat(result.getResult(), is("hello")); + } + + @Test + public void testParseInvalidQuote_lateQuote() throws Exception { + try { + type = string(); + type.parse("hello\" world\""); + fail(); + } catch (CommandException e) { + assertThat(e.getType(), is(ERROR_UNEXPECTED_START_OF_QUOTE)); + assertThat(e.getData(), is(equalTo(Collections.emptyMap()))); + } + } + + @Test + public void testParseQuote_lateQuoteWithRemaining() throws Exception { + type = string(); + ParsedArgument result = type.parse("hello \"world\""); + + assertThat(result.getRaw(), is("hello")); + assertThat(result.getResult(), is("hello")); + } + + @Test + public void testParseInvalidQuote_middleQuote() throws Exception { + try { + type = string(); + type.parse("hel\"lo"); + fail(); + } catch (CommandException e) { + assertThat(e.getType(), is(ERROR_UNEXPECTED_START_OF_QUOTE)); + assertThat(e.getData(), is(equalTo(Collections.emptyMap()))); + } + } + + @Test + public void testParseInvalidQuote_noUnquote() throws Exception { + try { + type = string(); + type.parse("\"hello world"); + fail(); + } catch (CommandException e) { + assertThat(e.getType(), is(ERROR_EXPECTED_END_OF_QUOTE)); + assertThat(e.getData(), is(equalTo(Collections.emptyMap()))); + } + } + + @Test + public void testParseEmpty() throws Exception { + type = string(); + ParsedArgument result = type.parse(""); + + assertThat(result.getRaw(), is("")); + assertThat(result.getResult(), is("")); + } + + @Test + public void testParseInvalidEscape_onlyEscape() throws Exception { + try { + type = string(); + type.parse("\\"); + fail(); + } catch (CommandException e) { + assertThat(e.getType(), is(ERROR_UNEXPECTED_ESCAPE)); + assertThat(e.getData(), is(equalTo(Collections.emptyMap()))); + } + } + + @Test + public void testParseInvalidEscape_unknownSequence() throws Exception { + try { + type = string(); + type.parse("\"\\n\""); + fail(); + } catch (CommandException e) { + assertThat(e.getType(), is(ERROR_INVALID_ESCAPE)); + assertThat(e.getData(), is(equalTo(Collections.singletonMap("input", "\\n")))); + } + } + + @Test + public void testParseInvalidEscape_notQuoted() throws Exception { + try { + type = string(); + type.parse("hel\\\\o"); + fail(); + } catch (CommandException e) { + assertThat(e.getType(), is(ERROR_UNEXPECTED_ESCAPE)); + assertThat(e.getData(), is(equalTo(Collections.emptyMap()))); + } + } + + @Test + public void testSuggestions() throws Exception { + type = string(); + Set set = Sets.newHashSet(); + type.listSuggestions("", set); + assertThat(set, is(empty())); + } + + @Test + public void testToString() throws Exception { + assertThat(string(), hasToString("string()")); + } + + @Test + public void testEscapeIfRequired_notRequired() throws Exception { + assertThat(escapeIfRequired("hello!"), is(equalTo("hello!"))); + assertThat(escapeIfRequired(""), is(equalTo(""))); + } + + @Test + public void testEscapeIfRequired_multipleWords() throws Exception { + assertThat(escapeIfRequired("hello world"), is(equalTo("\"hello world\""))); + } + + @Test + public void testEscapeIfRequired_quote() throws Exception { + assertThat(escapeIfRequired("hello \"world\"!"), is(equalTo("\"hello \\\"world\\\"!\""))); + } + + @Test + public void testEscapeIfRequired_escapes() throws Exception { + assertThat(escapeIfRequired("\\"), is(equalTo("\"\\\\\""))); + } + + @Test + public void testEscapeIfRequired_singleQuote() throws Exception { + assertThat(escapeIfRequired("\""), is(equalTo("\"\\\"\""))); + } +} \ No newline at end of file