blob: f7b1d86f5a3c19620668ce082be6080e1b475370 [file] [log] [blame]
// 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;
}
}