diff --git a/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/ByteString.java b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/ByteString.java new file mode 100644 index 000000000..6e3d3fea1 --- /dev/null +++ b/okhttp-protocols/src/main/java/com/squareup/okhttp/internal/ByteString.java @@ -0,0 +1,141 @@ +/* + * 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 com.squareup.okhttp.internal; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +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 final byte[] data; + private transient int hashCode; // Lazily computed; 0 if unknown. + private transient String utf8; // Lazily computed. + + /** + * A singleton empty {@code ByteString}. + */ + public static final ByteString EMPTY = new ByteString(Util.EMPTY_BYTE_ARRAY); + + /** + * Returns a new byte string containing the bytes of {@code data}, or + * {@link #EMPTY} if {@code data} is an empty array. + */ + public static ByteString of(byte... data) { + return new ByteString(data.clone()); + } + + /** + * Returns a new byte string containing the bytes of {@code data} + * from {@code offset} to {@code offset + count - 1}, inclusive, or + * {@link #EMPTY} if {@code count} is zero. + */ + public static ByteString of(byte[] data, int offset, int count) { + byte[] bytes = new byte[count]; + System.arraycopy(data, offset, bytes, 0, count); + return new ByteString(bytes); + } + + /** + * Returns a new byte string containing the {@code UTF-8} bytes of {@code s}, + * or {@link #EMPTY} if {@code s} is zero length. + */ + public static ByteString encodeUtf8(String s) { + return new ByteString(s.getBytes(Util.UTF_8)); + } + + /** Constructs a new {@code String} by decoding the bytes as 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 true when {@code s} is not null and its {@code UTF-8} encoded + * bytes are equivalent to the bytes wrapped by this byte string. + */ + public boolean utf8Equals(String s) { + if (s == null) return false; + // TODO: avoid allocation + return utf8().equals(s); + } + + /** + * Reads {@code count} bytes from {@code in} and returns the result. + * + * @throws EOFException if {@code in} has fewer than {@code count} bytes to + * read. + */ + public static ByteString read(InputStream in, int count) throws IOException { + byte[] result = new byte[count]; + for (int c = 0; c < count; ) { + int read = in.read(result, c, count - c); + if (read == -1) throw new EOFException("Expected " + count + "; received " + c); + c += read; + } + return new ByteString(result); + } + + private ByteString(byte[] data) { + this.data = data; // Trusted internal constructor doesn't clone data. + } + + /** + * 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); + } + + /** Writes a subsequence of this byte string to {@code out}. */ + public void write(OutputStream out, int offset, int count) throws IOException { + out.write(data, offset, count); + } + + @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)); + } +} diff --git a/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/ByteStringTest.java b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/ByteStringTest.java new file mode 100644 index 000000000..ab142f0a1 --- /dev/null +++ b/okhttp-protocols/src/test/java/com/squareup/okhttp/internal/ByteStringTest.java @@ -0,0 +1,78 @@ +/* + * 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 com.squareup.okhttp.internal; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.Arrays; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ByteStringTest { + + @Test public void equals() throws Exception { + ByteString byteString = ByteString.of((byte) 0x0, (byte) 0x1, (byte) 0x2); + assertTrue(byteString.equals(byteString)); + assertTrue(byteString.equals(ByteString.of((byte) 0x0, (byte) 0x1, (byte) 0x2))); + assertTrue(ByteString.of().equals(ByteString.EMPTY)); + assertTrue(ByteString.EMPTY.equals(ByteString.of())); + assertFalse(byteString.equals(new Object())); + assertFalse(byteString.equals(ByteString.of((byte) 0x0, (byte) 0x2, (byte) 0x1))); + } + + private final String bronzeHorseman = "На берегу пустынных волн"; + + @Test public void utf8() throws Exception { + ByteString byteString = ByteString.encodeUtf8(bronzeHorseman); + assertByteArraysEquals(byteString.toByteArray(), bronzeHorseman.getBytes("UTF-8")); + assertTrue(byteString.equals(ByteString.of(bronzeHorseman.getBytes("UTF-8")))); + assertTrue(byteString.utf8Equals(bronzeHorseman)); + assertEquals(byteString.utf8(), bronzeHorseman); + } + + @Test public void testHashCode() throws Exception { + ByteString byteString = ByteString.of((byte) 0x1, (byte) 0x2); + assertEquals(byteString.hashCode(), byteString.hashCode()); + assertEquals(byteString.hashCode(), ByteString.of((byte) 0x1, (byte) 0x2).hashCode()); + } + + @Test public void read() throws Exception { + InputStream in = new ByteArrayInputStream("abc".getBytes("UTF-8")); + assertEquals(ByteString.of((byte) 0x61, (byte) 0x62), ByteString.read(in, 2)); + assertEquals(ByteString.of((byte) 0x63), ByteString.read(in, 1)); + assertEquals(ByteString.of(), ByteString.read(in, 0)); + } + + @Test public void write() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteString.of((byte) 0x61, (byte) 0x62, (byte) 0x63).write(out); + assertByteArraysEquals(new byte[] { 0x61, 0x62, 0x63 }, out.toByteArray()); + } + + @Test public void writeWithOffset() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ByteString.of((byte) 0x61, (byte) 0x62, (byte) 0x63).write(out, 1, 2); + assertByteArraysEquals(new byte[] { 0x62, 0x63 }, out.toByteArray()); + } + + private static void assertByteArraysEquals(byte[] a, byte[] b) { + assertEquals(Arrays.toString(a), Arrays.toString(b)); + } +}