// Copyright 2016 The Bazel Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.testing.junit.runner.util; import java.util.HashMap; import java.util.Map; import javax.annotation.Nullable; /** * Utility class for dealing with escaping XML content and attributes. */ public class XmlEscapers { private XmlEscapers() {} private static final char MIN_ASCII_CONTROL_CHAR = 0x00; private static final char MAX_ASCII_CONTROL_CHAR = 0x1F; public static CharEscaper xmlContentEscaper() { return XML_CONTENT_ESCAPER; } public static CharEscaper xmlAttributeEscaper() { return XML_ATTRIBUTE_ESCAPER; } private static final CharEscaper XML_CONTENT_ESCAPER; private static final CharEscaper XML_ATTRIBUTE_ESCAPER; static { Builder builder = Builder.builder(); builder.setSafeRange(Character.MIN_VALUE, '\uFFFD'); builder.setUnsafeReplacement("\uFFFD"); for (char c = MIN_ASCII_CONTROL_CHAR; c <= MAX_ASCII_CONTROL_CHAR; c++) { if (c != '\t' && c != '\n' && c != '\r') { builder.addEscape(c, "\uFFFD"); } } builder.addEscape('&', "&"); builder.addEscape('<', "<"); builder.addEscape('>', ">"); XML_CONTENT_ESCAPER = builder.build(); builder.addEscape('\'', "'"); builder.addEscape('"', """); builder.addEscape('\t', " "); builder.addEscape('\n', " "); builder.addEscape('\r', " "); XML_ATTRIBUTE_ESCAPER = builder.build(); } /** * A builder for CharEscaper. */ static final class Builder { private final Map<Character, String> replacementMap = new HashMap<>(); private char safeMin = Character.MIN_VALUE; private char safeMax = Character.MAX_VALUE; private String unsafeReplacement = null; static Builder builder() { return new Builder(); } // The constructor is exposed via the builder() method above. private Builder() {} /** * Sets the safe range of characters for the escaper. Characters in this range that have no * explicit replacement are considered 'safe' and remain unescaped in the output. If * {@code safeMax < safeMin} then the safe range is empty. * * @return the builder instance */ Builder setSafeRange(char safeMin, char safeMax) { this.safeMin = safeMin; this.safeMax = safeMax; return this; } /** * Sets the replacement string for any characters outside the 'safe' range that have no explicit * replacement. If {@code unsafeReplacement} is {@code null} then no replacement will occur, if * it is {@code ""} then the unsafe characters are removed from the output. * * @return the builder instance */ Builder setUnsafeReplacement(@Nullable String unsafeReplacement) { this.unsafeReplacement = unsafeReplacement; return this; } /** * Adds a replacement string for the given input character. The specified character will be * replaced by the given string whenever it occurs in the input, irrespective of whether it lies * inside or outside the 'safe' range. * * @return the builder instance * @throws NullPointerException if {@code replacement} is null */ Builder addEscape(char c, String replacement) { if (replacement == null) { throw new NullPointerException(); } // This can replace an existing character (the builder is re-usable). replacementMap.put(c, replacement); return this; } /** * Returns a new CharEscaper based on the current state of the builder. */ CharEscaper build() { return new CharEscaper(replacementMap, safeMin, safeMax) { private final char[] replacementChars = unsafeReplacement != null ? unsafeReplacement.toCharArray() : null; @Override char[] escapeUnsafe(char c) { return replacementChars; } }; } } }