From ccf452d2b076f311931356abb9b10a8fdff97d81 Mon Sep 17 00:00:00 2001 From: zml Date: Sun, 6 Nov 2022 21:08:51 -0800 Subject: [PATCH] fix(text-minimessage): Properly handle escaped tag opens Fixes GH-821 --- .../minimessage/internal/parser/Token.java | 14 ++++++++++ .../internal/parser/TokenParser.java | 5 ++++ .../minimessage/MiniMessageParserTest.java | 21 +++++++++++++++ .../MiniMessageSerializerTest.java | 26 +++++++++++++------ 4 files changed, 58 insertions(+), 8 deletions(-) diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/Token.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/Token.java index 380bfa2e34..7a7dce6e20 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/Token.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/Token.java @@ -24,6 +24,7 @@ package net.kyori.adventure.text.minimessage.internal.parser; import java.util.List; +import java.util.Objects; import java.util.stream.Stream; import net.kyori.adventure.internal.Internals; import net.kyori.examination.Examinable; @@ -126,6 +127,19 @@ public CharSequence get(final CharSequence message) { ); } + @Override + public boolean equals(final Object other) { + if (this == other) return true; + if (!(other instanceof Token)) return false; + final Token that = (Token) other; + return this.startIndex == that.startIndex && this.endIndex == that.endIndex && this.type == that.type; + } + + @Override + public int hashCode() { + return Objects.hash(this.startIndex, this.endIndex, this.type); + } + @Override public String toString() { return Internals.toString(this); diff --git a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java index 040741ca70..7c1f663e95 100644 --- a/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java +++ b/text-minimessage/src/main/java/net/kyori/adventure/text/minimessage/internal/parser/TokenParser.java @@ -193,6 +193,11 @@ public static void parseString(final String message, final MatchedTokenConsumer< escaped = currentStringChar == nextCodePoint || nextCodePoint == ESCAPE; break; case TAG: + // Escape characters are not valid in tag names, so we aren't a tag token + if (nextCodePoint == TAG_START) { + escaped = true; + state = FirstPassState.NORMAL; + } break; } diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java index bff3456d48..33b2678e9b 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageParserTest.java @@ -23,9 +23,14 @@ */ package net.kyori.adventure.text.minimessage; +import java.util.Collections; +import java.util.List; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.TextComponent; import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.minimessage.internal.parser.Token; +import net.kyori.adventure.text.minimessage.internal.parser.TokenParser; +import net.kyori.adventure.text.minimessage.internal.parser.TokenType; import net.kyori.adventure.text.minimessage.tag.Tag; import net.kyori.adventure.text.minimessage.tag.resolver.ArgumentQueue; import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; @@ -54,6 +59,7 @@ import static net.kyori.adventure.text.minimessage.tag.resolver.Placeholder.parsed; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertIterableEquals; import static org.junit.jupiter.api.Assertions.assertThrows; public class MiniMessageParserTest extends AbstractTest { @@ -300,6 +306,21 @@ void testNonTerminatingQuote() { this.assertParsedEquals(expected4, input4); } + // https://github.com/KyoriPowered/adventure/issues/821 + @Test + void testEscapeIncompleteTags() { + final String input = "< a"; + final String escaped = PARSER.escapeTags(input); + + assertEquals("<\\ a", escaped); + + final List expectedTokens = Collections.singletonList(new Token(0, escaped.length(), TokenType.TEXT)); + assertIterableEquals(expectedTokens, TokenParser.tokenize(escaped)); + + final Component expected = text("< a"); + this.assertParsedEquals(expected, escaped); + } + // GH-68, GH-93 @Test void testAngleBracketsShit() { diff --git a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageSerializerTest.java b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageSerializerTest.java index 39f6f016f8..9b9df72f38 100644 --- a/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageSerializerTest.java +++ b/text-minimessage/src/test/java/net/kyori/adventure/text/minimessage/MiniMessageSerializerTest.java @@ -29,6 +29,7 @@ import net.kyori.adventure.text.format.TextDecoration; import org.junit.jupiter.api.Test; +import static net.kyori.adventure.text.Component.text; import static net.kyori.adventure.text.format.Style.style; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -36,11 +37,11 @@ public class MiniMessageSerializerTest extends AbstractTest { @Test void testSerializeNestedStyles() { // These are mostly arbitrary, but I don't want to test every single combination - final Component component = Component.text() - .append(Component.text("b+i+u", style(TextDecoration.BOLD, TextDecoration.ITALIC, TextDecoration.UNDERLINED))) - .append(Component.text("color+insert", style(NamedTextColor.RED)).insertion("meow")) - .append(Component.text("st+font", style(TextDecoration.STRIKETHROUGH).font(Key.key("uniform")))) - .append(Component.text("empty")) + final Component component = text() + .append(text("b+i+u", style(TextDecoration.BOLD, TextDecoration.ITALIC, TextDecoration.UNDERLINED))) + .append(text("color+insert", style(NamedTextColor.RED)).insertion("meow")) + .append(text("st+font", style(TextDecoration.STRIKETHROUGH).font(Key.key("uniform")))) + .append(text("empty")) .build(); final String expected = "b+i+u" + "color+insert" + @@ -56,16 +57,25 @@ void testTagsClosedInStrictMode() { final MiniMessage serializer = MiniMessage.builder().strict(true).build(); final String expected = "helloredbold"; - final Component input = Component.text() + final Component input = text() .content("hello") .append( - Component.text("red", NamedTextColor.RED) + text("red", NamedTextColor.RED) .append( - Component.text("bold", style(TextDecoration.BOLD)) + text("bold", style(TextDecoration.BOLD)) ) ) .build(); assertEquals(expected, serializer.serialize(input)); } + + @Test + void testDoubleOpenRoundTrippedEscaped() { + final String expected = "\\<\\ a"; // this is valid but there is a redundant serialization + final Component component = text("< a"); + this.assertSerializedEquals(expected, component); + this.assertParsedEquals(component, expected); + } + }