PercentCodec.java
package io.jstach.rainbowgum;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.BitSet;
/**
* Percent encoding (<a href="https://www.rfc-editor.org/rfc/rfc3986#section-2.1">RFC
* 3986</a>), the encoding Rainbow Gum uses for URI query properties and for key values
* written with
* {@link io.jstach.rainbowgum.format.KeyValuesFormatterBuilder.Format#PERCENT}, so that
* key values read from a log decode back to exactly what was logged.
*/
public final class PercentCodec {
private PercentCodec() {
}
static final BitSet GEN_DELIMS = new BitSet(256);
static final BitSet SUB_DELIMS = new BitSet(256);
static final BitSet UNRESERVED = new BitSet(256);
static final BitSet URIC = new BitSet(256);
static {
GEN_DELIMS.set(':');
GEN_DELIMS.set('/');
GEN_DELIMS.set('?');
GEN_DELIMS.set('#');
GEN_DELIMS.set('[');
GEN_DELIMS.set(']');
GEN_DELIMS.set('@');
SUB_DELIMS.set('!');
SUB_DELIMS.set('$');
SUB_DELIMS.set('&');
SUB_DELIMS.set('\'');
SUB_DELIMS.set('(');
SUB_DELIMS.set(')');
SUB_DELIMS.set('*');
SUB_DELIMS.set('+');
SUB_DELIMS.set(',');
SUB_DELIMS.set(';');
SUB_DELIMS.set('=');
for (int i = 'a'; i <= 'z'; i++) {
UNRESERVED.set(i);
}
for (int i = 'A'; i <= 'Z'; i++) {
UNRESERVED.set(i);
}
// numeric characters
for (int i = '0'; i <= '9'; i++) {
UNRESERVED.set(i);
}
UNRESERVED.set('-');
UNRESERVED.set('.');
UNRESERVED.set('_');
UNRESERVED.set('~');
URIC.or(SUB_DELIMS);
URIC.or(UNRESERVED);
}
private static final int RADIX = 16;
static void encode(final StringBuilder buf, final CharSequence content, Charset charset, final BitSet safechars) {
final CharBuffer cb = CharBuffer.wrap(content);
final ByteBuffer bb = charset.encode(cb);
while (bb.hasRemaining()) {
final int b = bb.get() & 0xff;
if (safechars.get(b)) {
buf.append((char) b);
}
else {
buf.append("%");
final char hex1 = Character.toUpperCase(Character.forDigit((b >> 4) & 0xF, RADIX));
final char hex2 = Character.toUpperCase(Character.forDigit(b & 0xF, RADIX));
buf.append(hex1);
buf.append(hex2);
}
}
}
/**
* Appends the content percent encoded: every byte of its encoding in the charset
* other than the unreserved characters <code>A-Z a-z 0-9 - . _ ~</code> is written as
* <code>%XX</code> with upper case hex digits.
* @param buf output.
* @param content text to encode.
* @param charset charset of the bytes to encode, usually UTF-8.
*/
public static void encode(final StringBuilder buf, final CharSequence content, final Charset charset) {
if (charset.equals(StandardCharsets.UTF_8)) {
encodeUtf8(buf, content);
}
else {
encode(buf, content, charset, UNRESERVED);
}
}
/*
* UTF-8, the charset key values are always encoded with, straight from the chars: no
* CharBuffer view (whose per char charAt dispatch is what made CharBuffer.wrap slow
* in DirectByteBufferBuffer) and no ByteBuffer per call. A lone surrogate is written
* as ? (%3F), the same replacement Charset.encode uses, so the output is identical to
* the general path.
*/
static void encodeUtf8(final StringBuilder buf, final CharSequence content) {
final int length = content.length();
for (int i = 0; i < length; i++) {
char c = content.charAt(i);
if (c < 0x80 && UNRESERVED_ASCII[c]) {
// copy a run of unreserved characters at once, the common case for key
// values
final int start = i;
while (i + 1 < length && (c = content.charAt(i + 1)) < 0x80 && UNRESERVED_ASCII[c]) {
i++;
}
buf.append(content, start, i + 1);
}
else if (c < 0x80) {
appendByte(buf, c);
}
else if (c < 0x800) {
appendByte(buf, 0xC0 | (c >> 6));
appendByte(buf, 0x80 | (c & 0x3F));
}
else if (Character.isSurrogate(c)) {
if (Character.isHighSurrogate(c) && i + 1 < length && Character.isLowSurrogate(content.charAt(i + 1))) {
final int cp = Character.toCodePoint(c, content.charAt(++i));
appendByte(buf, 0xF0 | (cp >> 18));
appendByte(buf, 0x80 | ((cp >> 12) & 0x3F));
appendByte(buf, 0x80 | ((cp >> 6) & 0x3F));
appendByte(buf, 0x80 | (cp & 0x3F));
}
else {
appendByte(buf, '?');
}
}
else {
appendByte(buf, 0xE0 | (c >> 12));
appendByte(buf, 0x80 | ((c >> 6) & 0x3F));
appendByte(buf, 0x80 | (c & 0x3F));
}
}
}
private static final char[] HEX = "0123456789ABCDEF".toCharArray();
private static final boolean[] UNRESERVED_ASCII = new boolean[0x80];
static {
for (int i = 0; i < 0x80; i++) {
UNRESERVED_ASCII[i] = UNRESERVED.get(i);
}
}
private static void appendByte(final StringBuilder buf, final int b) {
buf.append('%').append(HEX[(b >> 4) & 0xF]).append(HEX[b & 0xF]);
}
/**
* Percent encodes the content, see
* {@link #encode(StringBuilder, CharSequence, Charset)}.
* @param content text to encode.
* @param charset charset of the bytes to encode, usually UTF-8.
* @return encoded text.
*/
public static String encode(final CharSequence content, final Charset charset) {
final StringBuilder buf = new StringBuilder();
encode(buf, content, charset);
return buf.toString();
}
/**
* Decodes <code>%XX</code> sequences into bytes and the bytes into text with the
* charset. A <code>%</code> not followed by two hex digits is kept as is, and
* <code>+</code> is not a space.
* @param content percent encoded text.
* @param charset charset of the encoded bytes, usually UTF-8.
* @return decoded text.
*/
public static String decode(final CharSequence content, Charset charset) {
final ByteBuffer bb = ByteBuffer.allocate(content.length());
final CharBuffer cb = CharBuffer.wrap(content);
while (cb.hasRemaining()) {
final char c = cb.get();
if (c == '%' && cb.remaining() >= 2) {
final char uc = cb.get();
final char lc = cb.get();
final int u = Character.digit(uc, RADIX);
final int l = Character.digit(lc, RADIX);
if (u != -1 && l != -1) {
bb.put((byte) ((u << 4) + l));
}
else {
bb.put((byte) '%');
bb.put((byte) uc);
bb.put((byte) lc);
}
}
else {
bb.put((byte) c);
}
}
bb.flip();
return charset.decode(bb).toString();
}
}