From 9882ec67f2f85188697557cc0dc307ebff56f3cf Mon Sep 17 00:00:00 2001 From: Basil Crow Date: Sat, 30 Mar 2024 06:26:04 -0700 Subject: [PATCH] Replace usages of `org.apache.commons.lang.StringEscapeUtils` with Guava (#9094) --- .../ConsoleAnnotationOutputStream.java | 4 +- .../console/PlainTextConsoleOutputStream.java | 4 +- .../java/jenkins/util/SourceCodeEscapers.java | 51 +++++++++++++++++++ .../jenkins/util/SourceCodeEscapersTest.java | 42 +++++++++++++++ 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/jenkins/util/SourceCodeEscapers.java create mode 100644 core/src/test/java/jenkins/util/SourceCodeEscapersTest.java diff --git a/core/src/main/java/hudson/console/ConsoleAnnotationOutputStream.java b/core/src/main/java/hudson/console/ConsoleAnnotationOutputStream.java index 256211a7267b..b63210c702f3 100644 --- a/core/src/main/java/hudson/console/ConsoleAnnotationOutputStream.java +++ b/core/src/main/java/hudson/console/ConsoleAnnotationOutputStream.java @@ -36,8 +36,8 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import jenkins.util.SourceCodeEscapers; import org.apache.commons.io.output.ProxyWriter; -import org.apache.commons.lang.StringEscapeUtils; import org.kohsuke.stapler.framework.io.WriterOutputStream; /** @@ -122,7 +122,7 @@ public ConsoleAnnotator annotate(T context, MarkupText text) { } } catch (IOException | ClassNotFoundException e) { // if we failed to resurrect an annotation, ignore it. - LOGGER.log(Level.FINE, "Failed to resurrect annotation from \"" + StringEscapeUtils.escapeJava(new String(in, next, rest, Charset.defaultCharset())) + "\"", e); + LOGGER.log(Level.FINE, "Failed to resurrect annotation from \"" + SourceCodeEscapers.javaCharEscaper().escape(new String(in, next, rest, Charset.defaultCharset())) + "\"", e); } int bytesUsed = rest - b.available(); // bytes consumed by annotations diff --git a/core/src/main/java/hudson/console/PlainTextConsoleOutputStream.java b/core/src/main/java/hudson/console/PlainTextConsoleOutputStream.java index 70bc85b7b110..77a4de774490 100644 --- a/core/src/main/java/hudson/console/PlainTextConsoleOutputStream.java +++ b/core/src/main/java/hudson/console/PlainTextConsoleOutputStream.java @@ -31,7 +31,7 @@ import java.nio.charset.Charset; import java.util.logging.Level; import java.util.logging.Logger; -import org.apache.commons.lang.StringEscapeUtils; +import jenkins.util.SourceCodeEscapers; /** * Filters out console notes. @@ -73,7 +73,7 @@ protected void eol(byte[] in, int sz) throws IOException { try { ConsoleNote.skip(new DataInputStream(b)); } catch (IOException x) { - LOGGER.log(Level.FINE, "Failed to skip annotation from \"" + StringEscapeUtils.escapeJava(new String(in, next, rest, Charset.defaultCharset())) + "\"", x); + LOGGER.log(Level.FINE, "Failed to skip annotation from \"" + SourceCodeEscapers.javaCharEscaper().escape(new String(in, next, rest, Charset.defaultCharset())) + "\"", x); } int bytesUsed = rest - b.available(); // bytes consumed by annotations diff --git a/core/src/main/java/jenkins/util/SourceCodeEscapers.java b/core/src/main/java/jenkins/util/SourceCodeEscapers.java new file mode 100644 index 000000000000..d66e00d7dda3 --- /dev/null +++ b/core/src/main/java/jenkins/util/SourceCodeEscapers.java @@ -0,0 +1,51 @@ +package jenkins.util; + +import com.google.common.escape.ArrayBasedCharEscaper; +import com.google.common.escape.CharEscaper; +import com.google.common.escape.Escaper; +import java.util.Map; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; + +@Restricted(NoExternalUse.class) +public final class SourceCodeEscapers { + + // Suppress default constructor for noninstantiability + private SourceCodeEscapers() { + throw new AssertionError(); + } + + private static final Map REPLACEMENTS = Map.of( + '\b', "\\b", + '\f', "\\f", + '\n', "\\n", + '\r', "\\r", + '\t', "\\t", + '\"', "\\\"", + '\\', "\\\\"); + + private static final char PRINTABLE_ASCII_MIN = 0x20; + + private static final char PRINTABLE_ASCII_MAX = 0x7E; + + private static final CharEscaper JAVA_CHAR_ESCAPER = new JavaCharEscaper(); + + /** + * Returns an {@link Escaper} instance that escapes special characters in a string so it can + * safely be included in either a Java character literal or string literal. + */ + public static CharEscaper javaCharEscaper() { + return JAVA_CHAR_ESCAPER; + } + + private static class JavaCharEscaper extends ArrayBasedCharEscaper { + JavaCharEscaper() { + super(REPLACEMENTS, PRINTABLE_ASCII_MIN, PRINTABLE_ASCII_MAX); + } + + @Override + protected char[] escapeUnsafe(char c) { + return String.format("\\u%04X", (int) c).toCharArray(); + } + } +} diff --git a/core/src/test/java/jenkins/util/SourceCodeEscapersTest.java b/core/src/test/java/jenkins/util/SourceCodeEscapersTest.java new file mode 100644 index 000000000000..05b7a97d47be --- /dev/null +++ b/core/src/test/java/jenkins/util/SourceCodeEscapersTest.java @@ -0,0 +1,42 @@ +package jenkins.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; + +public class SourceCodeEscapersTest { + + @Test + public void testJavaCharEscaper() { + assertThrows(NullPointerException.class, () -> SourceCodeEscapers.javaCharEscaper() + .escape(null)); + assertEquals("", SourceCodeEscapers.javaCharEscaper().escape("")); + + assertEquals("foo", SourceCodeEscapers.javaCharEscaper().escape("foo")); + assertEquals("\\t", SourceCodeEscapers.javaCharEscaper().escape("\t")); + assertEquals("\\\\", SourceCodeEscapers.javaCharEscaper().escape("\\")); + assertEquals("'", SourceCodeEscapers.javaCharEscaper().escape("'")); + assertEquals("\\\\\\b\\t\\r", SourceCodeEscapers.javaCharEscaper().escape("\\\b\t\r")); + assertEquals("\\u1234", SourceCodeEscapers.javaCharEscaper().escape("\u1234")); + assertEquals("\\u0234", SourceCodeEscapers.javaCharEscaper().escape("\u0234")); + assertEquals("\\u00EF", SourceCodeEscapers.javaCharEscaper().escape("\u00ef")); + assertEquals("\\u0001", SourceCodeEscapers.javaCharEscaper().escape("\u0001")); + assertEquals("\\uABCD", SourceCodeEscapers.javaCharEscaper().escape("\uabcd")); + + assertEquals( + "He didn't say, \\\"stop!\\\"", + SourceCodeEscapers.javaCharEscaper().escape("He didn't say, \"stop!\"")); + assertEquals( + "This space is non-breaking:\\u00A0", + SourceCodeEscapers.javaCharEscaper().escape("This space is non-breaking:\u00a0")); + assertEquals( + "\\uABCD\\u1234\\u012C", SourceCodeEscapers.javaCharEscaper().escape("\uABCD\u1234\u012C")); + assertEquals( + "\\uD83D\\uDC80\\uD83D\\uDD14", + SourceCodeEscapers.javaCharEscaper().escape("\ud83d\udc80\ud83d\udd14")); + assertEquals( + "String with a slash (/) in it", + SourceCodeEscapers.javaCharEscaper().escape("String with a slash (/) in it")); + } +}