// 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.Collections; import java.util.Map; /** * An escaper that uses an array to quickly look up replacement characters for a given {@code char} * value. An additional safe range is provided that determines whether {@code char} values without * specific replacements are to be considered safe and left unescaped or should be escaped in a * general way. */ public abstract class CharEscaper { // The replacement array. private final char[][] replacements; // The number of elements in the replacement array. private final int replacementsLength; // The first character in the safe range. private final char safeMin; // The last character in the safe range. private final char safeMax; // The multiplier for padding to use when growing the escape buffer. private static final int DEST_PAD_MULTIPLIER = 2; public CharEscaper(Map<Character, String> replacementMap, char safeMin, char safeMax) { this.replacements = createReplacementArray(replacementMap); this.replacementsLength = replacements.length; this.safeMin = safeMin; this.safeMax = safeMax; } public final String escape(String s) { if (s == null) { throw new NullPointerException(); } for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if ((c < replacementsLength && replacements[c] != null) || c > safeMax || c < safeMin) { return escapeSlow(s, i); } } return s; } /** * A thread-local destination buffer to keep us from creating new buffers. The starting size is * 1024 characters. */ private static final ThreadLocal<char[]> DEST_TL = new ThreadLocal<char[]>() { @Override protected char[] initialValue() { return new char[1024]; } }; /** * Returns the escaped form of a given literal string, starting at the given index. This method is * called by the {@link #escape(String)} method when it discovers that escaping is required. * * @param s the literal string to be escaped * @param index the index to start escaping from * @return the escaped form of {@code string} * @throws NullPointerException if {@code string} is null */ final String escapeSlow(String s, int index) { int slen = s.length(); // Get a destination buffer and setup some loop variables. char[] dest = DEST_TL.get(); int destSize = dest.length; int destIndex = 0; int lastEscape = 0; // Loop through the rest of the string, replacing when needed into the // destination buffer, which gets grown as needed as well. for (; index < slen; index++) { // Get a replacement for the current character. char[] r = escape(s.charAt(index)); // If no replacement is needed, just continue. if (r == null) { continue; } int rlen = r.length; int charsSkipped = index - lastEscape; // This is the size needed to add the replacement, not the full size // needed by the string. We only regrow when we absolutely must, and // when we do grow, grow enough to avoid excessive growing. Grow. int sizeNeeded = destIndex + charsSkipped + rlen; if (destSize < sizeNeeded) { destSize = sizeNeeded + DEST_PAD_MULTIPLIER * (slen - index); dest = growBuffer(dest, destIndex, destSize); } // If we have skipped any characters, we need to copy them now. if (charsSkipped > 0) { s.getChars(lastEscape, index, dest, destIndex); destIndex += charsSkipped; } // Copy the replacement string into the dest buffer as needed. if (rlen > 0) { System.arraycopy(r, 0, dest, destIndex, rlen); destIndex += rlen; } lastEscape = index + 1; } // Copy leftover characters if there are any. int charsLeft = slen - lastEscape; if (charsLeft > 0) { int sizeNeeded = destIndex + charsLeft; if (destSize < sizeNeeded) { // Regrow and copy, expensive! No padding as this is the final copy. dest = growBuffer(dest, destIndex, sizeNeeded); } s.getChars(lastEscape, slen, dest, destIndex); destIndex = sizeNeeded; } return new String(dest, 0, destIndex); } final char[] escape(char c) { if (c < replacementsLength) { char[] chars = replacements[c]; if (chars != null) { return chars; } } if (c >= safeMin && c <= safeMax) { return null; } return escapeUnsafe(c); } abstract char[] escapeUnsafe(char c); /** * Helper method to grow the character buffer as needed, this only happens once in a while so it's * ok if it's in a method call. If the index passed in is 0 then no copying will be done. */ private static char[] growBuffer(char[] dest, int index, int size) { char[] copy = new char[size]; if (index > 0) { System.arraycopy(dest, 0, copy, 0, index); } return copy; } private static char[][] createReplacementArray(Map<Character, String> map) { if (map == null) { throw new NullPointerException(); } if (map.isEmpty()) { return new char[0][0]; } char max = Collections.max(map.keySet()); char[][] replacements = new char[max + 1][]; for (char c : map.keySet()) { replacements[c] = map.get(c).toCharArray(); } return replacements; } }