/* * Copyright 2014 Square Inc. * * 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 okio; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; /** * An immutable sequence of bytes. * *

Full disclosure: this class provides untrusted input and * output streams with raw access to the underlying byte array. A hostile * stream implementation could keep a reference to the mutable byte string, * violating the immutable guarantee of this class. For this reason a byte * string's immutability guarantee cannot be relied upon for security in applets * and other environments that run both trusted and untrusted code in the same * process. */ public final class ByteString { private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; /** A singleton empty {@code ByteString}. */ public static final ByteString EMPTY = ByteString.of(); final byte[] data; private transient int hashCode; // Lazily computed; 0 if unknown. private transient String utf8; // Lazily computed. ByteString(byte[] data) { this.data = data; // Trusted internal constructor doesn't clone data. } /** * Returns a new byte string containing a clone of the bytes of {@code data}. */ public static ByteString of(byte... data) { return new ByteString(data.clone()); } /** Returns a new byte string containing the {@code UTF-8} bytes of {@code s}. */ public static ByteString encodeUtf8(String s) { ByteString byteString = new ByteString(s.getBytes(Util.UTF_8)); byteString.utf8 = s; return byteString; } /** Constructs a new {@code String} by decoding the bytes as {@code UTF-8}. */ public String utf8() { String result = utf8; // We don't care if we double-allocate in racy code. return result != null ? result : (utf8 = new String(data, Util.UTF_8)); } /** * Returns this byte string encoded as Base64. In violation of the * RFC, the returned string does not wrap lines at 76 columns. */ public String base64() { return Base64.encode(data); } /** * Decodes the Base64-encoded bytes and returns their value as a byte string. * Returns null if {@code base64} is not a Base64-encoded sequence of bytes. */ public static ByteString decodeBase64(String base64) { byte[] decoded = Base64.decode(base64); return decoded != null ? new ByteString(decoded) : null; } /** Returns this byte string encoded in hexadecimal. */ public String hex() { char[] result = new char[data.length * 2]; int c = 0; for (byte b : data) { result[c++] = HEX_DIGITS[(b >> 4) & 0xf]; result[c++] = HEX_DIGITS[b & 0xf]; } return new String(result); } /** Decodes the hex-encoded bytes and returns their value a byte string. */ public static ByteString decodeHex(String hex) { if (hex.length() % 2 != 0) throw new IllegalArgumentException("Unexpected hex string: " + hex); byte[] result = new byte[hex.length() / 2]; for (int i = 0; i < result.length; i++) { int d1 = decodeHexDigit(hex.charAt(i * 2)) << 4; int d2 = decodeHexDigit(hex.charAt(i * 2 + 1)); result[i] = (byte) (d1 + d2); } return of(result); } private static int decodeHexDigit(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; if (c >= 'A' && c <= 'F') return c - 'A' + 10; throw new IllegalArgumentException("Unexpected hex digit: " + c); } /** * Reads {@code count} bytes from {@code in} and returns the result. * * @throws java.io.EOFException if {@code in} has fewer than {@code count} * bytes to read. */ public static ByteString read(InputStream in, int byteCount) throws IOException { byte[] result = new byte[byteCount]; for (int offset = 0, read; offset < byteCount; offset += read) { read = in.read(result, offset, byteCount - offset); if (read == -1) throw new EOFException(); } return new ByteString(result); } /** * Returns a byte string equal to this byte string, but with the bytes 'A' * through 'Z' replaced with the corresponding byte in 'a' through 'z'. * Returns this byte string if it contains no bytes in 'A' through 'Z'. */ public ByteString toAsciiLowercase() { // Search for an uppercase character. If we don't find one, return this. for (int i = 0; i < data.length; i++) { byte c = data[i]; if (c < 'A' || c > 'Z') continue; // If we reach this point, this string is not not lowercase. Create and // return a new byte string. byte[] lowercase = data.clone(); lowercase[i++] = (byte) (c - ('A' - 'a')); for (; i < lowercase.length; i++) { c = lowercase[i]; if (c < 'A' || c > 'Z') continue; lowercase[i] = (byte) (c - ('A' - 'a')); } return new ByteString(lowercase); } return this; } /** Returns the byte at {@code pos}. */ public byte getByte(int pos) { return data[pos]; } /** * Returns the number of bytes in this ByteString. */ public int size() { return data.length; } /** * Returns a byte array containing a copy of the bytes in this {@code ByteString}. */ public byte[] toByteArray() { return data.clone(); } /** Writes the contents of this byte string to {@code out}. */ public void write(OutputStream out) throws IOException { out.write(data); } @Override public boolean equals(Object o) { return o == this || o instanceof ByteString && Arrays.equals(((ByteString) o).data, data); } @Override public int hashCode() { int result = hashCode; return result != 0 ? result : (hashCode = Arrays.hashCode(data)); } @Override public String toString() { if (data.length == 0) { return "ByteString[size=0]"; } if (data.length <= 16) { return String.format("ByteString[size=%s data=%s]", data.length, hex()); } try { return String.format("ByteString[size=%s md5=%s]", data.length, ByteString.of(MessageDigest.getInstance("MD5").digest(data)).hex()); } catch (NoSuchAlgorithmException e) { throw new AssertionError(); } } }