/** * Copyright 2015-2017 The OpenZipkin Authors * * 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 zipkin.internal; final class Buffer { interface Writer<T> { int sizeInBytes(T value); void write(T value, Buffer buffer); } private final byte[] buf; private int pos; Buffer(int size) { buf = new byte[size]; } Buffer writeByte(int v) { buf[pos++] = (byte) v; return this; } Buffer write(byte[] v) { System.arraycopy(v, 0, buf, pos, v.length); pos += v.length; return this; } Buffer writeShort(int v) { writeByte((v >>> 8L) & 0xff); writeByte(v & 0xff); return this; } Buffer writeInt(int v) { buf[pos++] = (byte) ((v >>> 24L) & 0xff); buf[pos++] = (byte) ((v >>> 16L) & 0xff); buf[pos++] = (byte) ((v >>> 8L) & 0xff); buf[pos++] = (byte) (v & 0xff); return this; } Buffer writeLong(long v) { buf[pos++] = (byte) ((v >>> 56L) & 0xff); buf[pos++] = (byte) ((v >>> 48L) & 0xff); buf[pos++] = (byte) ((v >>> 40L) & 0xff); buf[pos++] = (byte) ((v >>> 32L) & 0xff); buf[pos++] = (byte) ((v >>> 24L) & 0xff); buf[pos++] = (byte) ((v >>> 16L) & 0xff); buf[pos++] = (byte) ((v >>> 8L) & 0xff); buf[pos++] = (byte) (v & 0xff); return this; } static int asciiSizeInBytes(String string) { return string.length(); } static int utf8SizeInBytes(String string) { // Adapted from http://stackoverflow.com/questions/8511490/calculating-length-in-utf-8-of-java-string-without-actually-encoding-it int sizeInBytes = 0; for (int i = 0, len = string.length(); i < len; i++) { char ch = string.charAt(i); if (ch < 0x80) { sizeInBytes++; // 7-bit character } else if (ch < 0x800) { sizeInBytes += 2; // 11-bit character } else if (ch < 0xd800 || ch > 0xdfff) { sizeInBytes += 3; // 16-bit character } else { // malformed surrogate logic borrowed from okio.Utf8 int low = i + 1 < len ? string.charAt(i + 1) : 0; if (ch > 0xdbff || low < 0xdc00 || low > 0xdfff) { sizeInBytes++; // A malformed surrogate, which yields '?'. } else { // A 21-bit character sizeInBytes += 4; i++; } } } return sizeInBytes; } /** Writes a length-prefixed string */ Buffer writeLengthPrefixed(String v) { boolean ascii = isAscii(v); if (ascii) { writeInt(v.length()); return writeAscii(v); } else { byte[] temp = v.getBytes(Util.UTF_8); writeInt(temp.length); write(temp); } return this; } Buffer writeAscii(String v) { int length = v.length(); for (int i = 0; i < length; i++) { buf[pos++] = (byte) v.charAt(i); } return this; } static boolean isAscii(String v) { for (int i = 0, length = v.length(); i < length; i++) { if (v.charAt(i) >= 0x80) { return false; } } return true; } /* * Escaping logic adapted from Moshi, which we couldn't use due to language level * * From RFC 7159, "All Unicode characters may be placed within the * quotation marks except for the characters that must be escaped: * quotation mark, reverse solidus, and the control characters * (U+0000 through U+001F)." * * We also escape '\u2028' and '\u2029', which JavaScript interprets as * newline characters. This prevents eval() from failing with a syntax * error. http://code.google.com/p/google-gson/issues/detail?id=341 */ private static final String[] REPLACEMENT_CHARS; static { REPLACEMENT_CHARS = new String[128]; for (int i = 0; i <= 0x1f; i++) { REPLACEMENT_CHARS[i] = String.format("\\u%04x", (int) i); } REPLACEMENT_CHARS['"'] = "\\\""; REPLACEMENT_CHARS['\\'] = "\\\\"; REPLACEMENT_CHARS['\t'] = "\\t"; REPLACEMENT_CHARS['\b'] = "\\b"; REPLACEMENT_CHARS['\n'] = "\\n"; REPLACEMENT_CHARS['\r'] = "\\r"; REPLACEMENT_CHARS['\f'] = "\\f"; } private static final String U2028 = "\\u2028"; private static final String U2029 = "\\u2029"; static boolean needsJsonEscaping(byte[] v) { for (int i = 0; i < v.length; i++) { int current = v[i] & 0xFF; if (i >= 2 && // Is this the end of a u2028 or u2028 UTF-8 codepoint? // 0xE2 0x80 0xA8 == u2028; 0xE2 0x80 0xA9 == u2028 (current == 0xA8 || current == 0xA9) && (v[i - 1] & 0xFF) == 0x80 && (v[i - 2] & 0xFF) == 0xE2) { return true; } else if (current < 0x80 && REPLACEMENT_CHARS[current] != null) { return true; } } return false; // must be a string we don't need to escape. } static int jsonEscapedSizeInBytes(byte[] v) { return needsJsonEscaping(v) ? jsonEscapedSizeInBytes(new String(v, Util.UTF_8)) : v.length; } static int jsonEscapedSizeInBytes(String v) { boolean ascii = true; int escapingOverhead = 0; for (int i = 0, length = v.length(); i < length; i++) { char c = v.charAt(i); if (c == '\u2028' || c == '\u2029') { escapingOverhead += 5; } else if (c >= 0x80) { ascii = false; } else { String maybeReplacement = REPLACEMENT_CHARS[c]; if (maybeReplacement != null) escapingOverhead += maybeReplacement.length() - 1; } } if (ascii) return asciiSizeInBytes(v) + escapingOverhead; return utf8SizeInBytes(v) + escapingOverhead; } Buffer writeJsonEscaped(byte[] v) { return needsJsonEscaping(v) ? writeJsonEscaped(new String(v, Util.UTF_8)) : write(v); } Buffer writeJsonEscaped(String v) { int afterReplacement = 0; int length = v.length(); StringBuilder builder = null; for (int i = 0; i < length; i++) { char c = v.charAt(i); String replacement; if (c < 0x80) { replacement = REPLACEMENT_CHARS[c]; if (replacement == null) continue; } else if (c == '\u2028') { replacement = U2028; } else if (c == '\u2029') { replacement = U2029; } else { continue; } if (afterReplacement < i) { // write characters between the last replacement and now if (builder == null) builder = new StringBuilder(); builder.append(v, afterReplacement, i); } if (builder == null) builder = new StringBuilder(); builder.append(replacement); afterReplacement = i + 1; } if (builder == null) { // then we didn't escape anything return writeUtf8(v); } if (afterReplacement < length) { builder.append(v, afterReplacement, length); } return writeUtf8(builder.toString()); } Buffer writeUtf8(String v) { if (isAscii(v)) return writeAscii(v); byte[] temp = v.getBytes(Util.UTF_8); write(temp); return this; } Buffer writeLowerHex(long v) { writeHexByte((byte) ((v >>> 56L) & 0xff)); writeHexByte((byte) ((v >>> 48L) & 0xff)); writeHexByte((byte) ((v >>> 40L) & 0xff)); writeHexByte((byte) ((v >>> 32L) & 0xff)); writeHexByte((byte) ((v >>> 24L) & 0xff)); writeHexByte((byte) ((v >>> 16L) & 0xff)); writeHexByte((byte) ((v >>> 8L) & 0xff)); writeHexByte((byte) (v & 0xff)); return this; } // the code to get the size of ipv6 is long and basically the same as encoding it. static int ipv6SizeInBytes(byte[] ipv6) { int result = IPV6_SIZE.get().writeIpV6(ipv6).pos; IPV6_SIZE.get().pos = 0; return result; } private static final ThreadLocal<Buffer> IPV6_SIZE = new ThreadLocal<Buffer>() { @Override protected Buffer initialValue() { return new Buffer(39); // maximum length of encoded ipv6 } }; Buffer writeIpV6(byte[] ipv6) { // Compress the longest string of zeros int zeroCompressionIndex = -1; int zeroCompressionLength = -1; int zeroIndex = -1; boolean allZeros = true; for (int i = 0; i < ipv6.length; i += 2) { if (ipv6[i] == 0 && ipv6[i + 1] == 0) { if (zeroIndex < 0) zeroIndex = i; continue; } allZeros = false; if (zeroIndex >= 0) { int zeroLength = i - zeroIndex; if (zeroLength > zeroCompressionLength) { zeroCompressionIndex = zeroIndex; zeroCompressionLength = zeroLength; } zeroIndex = -1; } } // handle all zeros: 0:0:0:0:0:0:0:0 -> :: if (allZeros) { buf[pos++] = ':'; buf[pos++] = ':'; return this; } // handle trailing zeros: 2001:0:0:4:0:0:0:0 -> 2001:0:0:4:: if (zeroCompressionIndex == -1 && zeroIndex != -1) { zeroCompressionIndex = zeroIndex; zeroCompressionLength = 16 - zeroIndex; } int i = 0; while (i < ipv6.length) { if (i == zeroCompressionIndex) { buf[pos++] = ':'; i += zeroCompressionLength; if (i == ipv6.length) buf[pos++] = ':'; continue; } if (i != 0) buf[pos++] = ':'; byte high = ipv6[i++]; byte low = ipv6[i++]; // handle leading zeros: 2001:0:0:4:0000:0:0:8 -> 2001:0:0:4::8 boolean leadingZero; byte val = HEX_DIGITS[(high >> 4) & 0xf]; if (!(leadingZero = val == '0')) buf[pos++] = val; val = HEX_DIGITS[high & 0xf]; if (!(leadingZero = (leadingZero && val == '0'))) buf[pos++] = val; val = HEX_DIGITS[(low >> 4) & 0xf]; if (!(leadingZero && val == '0')) buf[pos++] = val; buf[pos++] = HEX_DIGITS[low & 0xf]; } return this; } /** * Binary search for character width which favors matching lower numbers. * * <p>Adapted from okio.Buffer */ static int asciiSizeInBytes(long v) { if (v == 0) return 1; if (v == Long.MIN_VALUE) return 20; boolean negative = false; if (v < 0) { v = -v; // making this positive allows us to compare using less-than negative = true; } int width = v < 100000000L ? v < 10000L ? v < 100L ? v < 10L ? 1 : 2 : v < 1000L ? 3 : 4 : v < 1000000L ? v < 100000L ? 5 : 6 : v < 10000000L ? 7 : 8 : v < 1000000000000L ? v < 10000000000L ? v < 1000000000L ? 9 : 10 : v < 100000000000L ? 11 : 12 : v < 1000000000000000L ? v < 10000000000000L ? 13 : v < 100000000000000L ? 14 : 15 : v < 100000000000000000L ? v < 10000000000000000L ? 16 : 17 : v < 1000000000000000000L ? 18 : 19; return negative ? width + 1 : width; // conditionally add room for negative sign } Buffer writeAscii(long v) { if (v == 0) return writeByte('0'); if (v == Long.MIN_VALUE) return writeAscii("-9223372036854775808"); int width = asciiSizeInBytes(v); int pos = this.pos += width; // We write backwards from right to left. boolean negative = false; if (v < 0) { negative = true; v = -v; // needs to be positive so we can use this for an array index } while (v != 0) { int digit = (int) (v % 10); buf[--pos] = HEX_DIGITS[digit]; v /= 10; } if (negative) buf[--pos] = '-'; return this; } static final byte[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; void writeHexByte(byte b) { buf[pos++] = HEX_DIGITS[(b >> 4) & 0xf]; buf[pos++] = HEX_DIGITS[b & 0xf]; } static final byte[] URL_MAP = new byte[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_' }; static int base64UrlSizeInBytes(byte[] in) { return (in.length + 2) / 3 * 4; } /** * Adapted from okio.Base64 as JRE 6 doesn't have a base64Url encoder * * <p>Original author: Alexander Y. Kleymenov */ Buffer writeBase64Url(byte[] in) { int end = in.length - in.length % 3; for (int i = 0; i < end; i += 3) { buf[pos++] = URL_MAP[(in[i] & 0xff) >> 2]; buf[pos++] = URL_MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)]; buf[pos++] = URL_MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)]; buf[pos++] = URL_MAP[(in[i + 2] & 0x3f)]; } switch (in.length % 3) { case 1: buf[pos++] = URL_MAP[(in[end] & 0xff) >> 2]; buf[pos++] = URL_MAP[(in[end] & 0x03) << 4]; buf[pos++] = '='; buf[pos++] = '='; break; case 2: buf[pos++] = URL_MAP[(in[end] & 0xff) >> 2]; buf[pos++] = URL_MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)]; buf[pos++] = URL_MAP[((in[end + 1] & 0x0f) << 2)]; buf[pos++] = '='; break; } return this; } byte[] toByteArray() { //assert pos == buf.length; return buf; } }