// Protocol Buffers - Google's data interchange format // Copyright 2008 Google Inc. All rights reserved. // https://developers.google.com/protocol-buffers/ // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. package com.google.protobuf; import static com.google.protobuf.Internal.UTF_8; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.List; import java.util.NoSuchElementException; import junit.framework.TestCase; /** Tests for {@link NioByteString}. */ public class NioByteStringTest extends TestCase { private static final ByteString EMPTY = new NioByteString(ByteBuffer.wrap(new byte[0])); private static final String CLASSNAME = NioByteString.class.getSimpleName(); private static final byte[] BYTES = ByteStringTest.getTestBytes(1234, 11337766L); private static final int EXPECTED_HASH = ByteString.wrap(BYTES).hashCode(); private final ByteBuffer backingBuffer = ByteBuffer.wrap(BYTES.clone()); private final ByteString testString = new NioByteString(backingBuffer); public void testExpectedType() { String actualClassName = getActualClassName(testString); assertEquals(CLASSNAME + " should match type exactly", CLASSNAME, actualClassName); } protected String getActualClassName(Object object) { String actualClassName = object.getClass().getName(); actualClassName = actualClassName.substring(actualClassName.lastIndexOf('.') + 1); return actualClassName; } public void testByteAt() { boolean stillEqual = true; for (int i = 0; stillEqual && i < BYTES.length; ++i) { stillEqual = (BYTES[i] == testString.byteAt(i)); } assertTrue(CLASSNAME + " must capture the right bytes", stillEqual); } public void testByteIterator() { boolean stillEqual = true; ByteString.ByteIterator iter = testString.iterator(); for (int i = 0; stillEqual && i < BYTES.length; ++i) { stillEqual = (iter.hasNext() && BYTES[i] == iter.nextByte()); } assertTrue(CLASSNAME + " must capture the right bytes", stillEqual); assertFalse(CLASSNAME + " must have exhausted the itertor", iter.hasNext()); try { iter.nextByte(); fail("Should have thrown an exception."); } catch (NoSuchElementException e) { // This is success } } public void testByteIterable() { boolean stillEqual = true; int j = 0; for (byte quantum : testString) { stillEqual = (BYTES[j] == quantum); ++j; } assertTrue(CLASSNAME + " must capture the right bytes as Bytes", stillEqual); assertEquals(CLASSNAME + " iterable character count", BYTES.length, j); } public void testSize() { assertEquals(CLASSNAME + " must have the expected size", BYTES.length, testString.size()); } public void testGetTreeDepth() { assertEquals(CLASSNAME + " must have depth 0", 0, testString.getTreeDepth()); } public void testIsBalanced() { assertTrue(CLASSNAME + " is technically balanced", testString.isBalanced()); } public void testCopyTo_ByteArrayOffsetLength() { int destinationOffset = 50; int length = 100; byte[] destination = new byte[destinationOffset + length]; int sourceOffset = 213; testString.copyTo(destination, sourceOffset, destinationOffset, length); boolean stillEqual = true; for (int i = 0; stillEqual && i < length; ++i) { stillEqual = BYTES[i + sourceOffset] == destination[i + destinationOffset]; } assertTrue(CLASSNAME + ".copyTo(4 arg) must give the expected bytes", stillEqual); } public void testCopyTo_ByteArrayOffsetLengthErrors() { int destinationOffset = 50; int length = 100; byte[] destination = new byte[destinationOffset + length]; try { // Copy one too many bytes testString.copyTo(destination, testString.size() + 1 - length, destinationOffset, length); fail("Should have thrown an exception when copying too many bytes of a " + CLASSNAME); } catch (IndexOutOfBoundsException expected) { // This is success } try { // Copy with illegal negative sourceOffset testString.copyTo(destination, -1, destinationOffset, length); fail("Should have thrown an exception when given a negative sourceOffset in " + CLASSNAME); } catch (IndexOutOfBoundsException expected) { // This is success } try { // Copy with illegal negative destinationOffset testString.copyTo(destination, 0, -1, length); fail( "Should have thrown an exception when given a negative destinationOffset in " + CLASSNAME); } catch (IndexOutOfBoundsException expected) { // This is success } try { // Copy with illegal negative size testString.copyTo(destination, 0, 0, -1); fail("Should have thrown an exception when given a negative size in " + CLASSNAME); } catch (IndexOutOfBoundsException expected) { // This is success } try { // Copy with illegal too-large sourceOffset testString.copyTo(destination, 2 * testString.size(), 0, length); fail( "Should have thrown an exception when the destinationOffset is too large in " + CLASSNAME); } catch (IndexOutOfBoundsException expected) { // This is success } try { // Copy with illegal too-large destinationOffset testString.copyTo(destination, 0, 2 * destination.length, length); fail( "Should have thrown an exception when the destinationOffset is too large in " + CLASSNAME); } catch (IndexOutOfBoundsException expected) { // This is success } } public void testCopyTo_ByteBuffer() { // Same length. ByteBuffer myBuffer = ByteBuffer.allocate(BYTES.length); testString.copyTo(myBuffer); myBuffer.flip(); assertEquals( CLASSNAME + ".copyTo(ByteBuffer) must give back the same bytes", backingBuffer, myBuffer); // Target buffer bigger than required. myBuffer = ByteBuffer.allocate(testString.size() + 1); testString.copyTo(myBuffer); myBuffer.flip(); assertEquals(backingBuffer, myBuffer); // Target buffer has no space. myBuffer = ByteBuffer.allocate(0); try { testString.copyTo(myBuffer); fail("Should have thrown an exception when target ByteBuffer has insufficient capacity"); } catch (BufferOverflowException e) { // Expected. } // Target buffer too small. myBuffer = ByteBuffer.allocate(1); try { testString.copyTo(myBuffer); fail("Should have thrown an exception when target ByteBuffer has insufficient capacity"); } catch (BufferOverflowException e) { // Expected. } } public void testMarkSupported() { InputStream stream = testString.newInput(); assertTrue(CLASSNAME + ".newInput() must support marking", stream.markSupported()); } public void testMarkAndReset() throws IOException { int fraction = testString.size() / 3; InputStream stream = testString.newInput(); stream.mark(testString.size()); // First, mark() the end. skipFully(stream, fraction); // Skip a large fraction, but not all. assertEquals( CLASSNAME + ": after skipping to the 'middle', half the bytes are available", (testString.size() - fraction), stream.available()); stream.reset(); assertEquals( CLASSNAME + ": after resetting, all bytes are available", testString.size(), stream.available()); skipFully(stream, testString.size()); // Skip to the end. assertEquals( CLASSNAME + ": after skipping to the end, no more bytes are available", 0, stream.available()); } /** * Discards {@code n} bytes of data from the input stream. This method will block until the full * amount has been skipped. Does not close the stream. * *

Copied from com.google.common.io.ByteStreams to avoid adding dependency. * * @param in the input stream to read from * @param n the number of bytes to skip * @throws EOFException if this stream reaches the end before skipping all the bytes * @throws IOException if an I/O error occurs, or the stream does not support skipping */ static void skipFully(InputStream in, long n) throws IOException { long toSkip = n; while (n > 0) { long amt = in.skip(n); if (amt == 0) { // Force a blocking read to avoid infinite loop if (in.read() == -1) { long skipped = toSkip - n; throw new EOFException( "reached end of stream after skipping " + skipped + " bytes; " + toSkip + " bytes expected"); } n--; } else { n -= amt; } } } public void testAsReadOnlyByteBuffer() { ByteBuffer byteBuffer = testString.asReadOnlyByteBuffer(); byte[] roundTripBytes = new byte[BYTES.length]; assertTrue(byteBuffer.remaining() == BYTES.length); assertTrue(byteBuffer.isReadOnly()); byteBuffer.get(roundTripBytes); assertTrue( CLASSNAME + ".asReadOnlyByteBuffer() must give back the same bytes", Arrays.equals(BYTES, roundTripBytes)); } public void testAsReadOnlyByteBufferList() { List byteBuffers = testString.asReadOnlyByteBufferList(); int bytesSeen = 0; byte[] roundTripBytes = new byte[BYTES.length]; for (ByteBuffer byteBuffer : byteBuffers) { int thisLength = byteBuffer.remaining(); assertTrue(byteBuffer.isReadOnly()); assertTrue(bytesSeen + thisLength <= BYTES.length); byteBuffer.get(roundTripBytes, bytesSeen, thisLength); bytesSeen += thisLength; } assertTrue(bytesSeen == BYTES.length); assertTrue( CLASSNAME + ".asReadOnlyByteBufferTest() must give back the same bytes", Arrays.equals(BYTES, roundTripBytes)); } public void testToByteArray() { byte[] roundTripBytes = testString.toByteArray(); assertTrue( CLASSNAME + ".toByteArray() must give back the same bytes", Arrays.equals(BYTES, roundTripBytes)); } public void testWriteTo() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); testString.writeTo(bos); byte[] roundTripBytes = bos.toByteArray(); assertTrue( CLASSNAME + ".writeTo() must give back the same bytes", Arrays.equals(BYTES, roundTripBytes)); } public void testWriteToShouldNotExposeInternalBufferToOutputStream() throws IOException { OutputStream os = new OutputStream() { @Override public void write(byte[] b, int off, int len) { Arrays.fill(b, off, off + len, (byte) 0); } @Override public void write(int b) { throw new UnsupportedOperationException(); } }; byte[] original = Arrays.copyOf(BYTES, BYTES.length); testString.writeTo(os); assertTrue( CLASSNAME + ".writeTo() must NOT grant access to underlying buffer", Arrays.equals(original, BYTES)); } public void testWriteToInternalShouldExposeInternalBufferToOutputStream() throws IOException { OutputStream os = new OutputStream() { @Override public void write(byte[] b, int off, int len) { Arrays.fill(b, off, off + len, (byte) 0); } @Override public void write(int b) { throw new UnsupportedOperationException(); } }; testString.writeToInternal(os, 0, testString.size()); byte[] allZeros = new byte[testString.size()]; assertTrue( CLASSNAME + ".writeToInternal() must grant access to underlying buffer", Arrays.equals(allZeros, backingBuffer.array())); } public void testWriteToShouldExposeInternalBufferToByteOutput() throws IOException { ByteOutput out = new ByteOutput() { @Override public void write(byte value) throws IOException { throw new UnsupportedOperationException(); } @Override public void write(byte[] value, int offset, int length) throws IOException { throw new UnsupportedOperationException(); } @Override public void writeLazy(byte[] value, int offset, int length) throws IOException { throw new UnsupportedOperationException(); } @Override public void write(ByteBuffer value) throws IOException { throw new UnsupportedOperationException(); } @Override public void writeLazy(ByteBuffer value) throws IOException { Arrays.fill( value.array(), value.arrayOffset(), value.arrayOffset() + value.limit(), (byte) 0); } }; testString.writeTo(out); byte[] allZeros = new byte[testString.size()]; assertTrue( CLASSNAME + ".writeTo() must grant access to underlying buffer", Arrays.equals(allZeros, backingBuffer.array())); } public void testNewOutput() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); ByteString.Output output = ByteString.newOutput(); testString.writeTo(output); assertEquals("Output Size returns correct result", output.size(), testString.size()); output.writeTo(bos); assertTrue( "Output.writeTo() must give back the same bytes", Arrays.equals(BYTES, bos.toByteArray())); // write the output stream to itself! This should cause it to double output.writeTo(output); assertEquals( "Writing an output stream to itself is successful", testString.concat(testString), output.toByteString()); output.reset(); assertEquals("Output.reset() resets the output", 0, output.size()); assertEquals("Output.reset() resets the output", EMPTY, output.toByteString()); } public void testToString() { String testString = "I love unicode \u1234\u5678 characters"; ByteString unicode = forString(testString); String roundTripString = unicode.toString(UTF_8); assertEquals(CLASSNAME + " unicode must match", testString, roundTripString); } public void testCharsetToString() { String testString = "I love unicode \u1234\u5678 characters"; ByteString unicode = forString(testString); String roundTripString = unicode.toString(UTF_8); assertEquals(CLASSNAME + " unicode must match", testString, roundTripString); } public void testToString_returnsCanonicalEmptyString() { assertSame( CLASSNAME + " must be the same string references", EMPTY.toString(UTF_8), new NioByteString(ByteBuffer.wrap(new byte[0])).toString(UTF_8)); } public void testToString_raisesException() { try { EMPTY.toString("invalid"); fail("Should have thrown an exception."); } catch (UnsupportedEncodingException expected) { // This is success } try { testString.toString("invalid"); fail("Should have thrown an exception."); } catch (UnsupportedEncodingException expected) { // This is success } } public void testEquals() { assertEquals(CLASSNAME + " must not equal null", false, testString.equals(null)); assertEquals(CLASSNAME + " must equal self", testString, testString); assertFalse(CLASSNAME + " must not equal the empty string", testString.equals(EMPTY)); assertEquals(CLASSNAME + " empty strings must be equal", EMPTY, testString.substring(55, 55)); assertEquals( CLASSNAME + " must equal another string with the same value", testString, new NioByteString(backingBuffer)); byte[] mungedBytes = mungedBytes(); assertFalse( CLASSNAME + " must not equal every string with the same length", testString.equals(new NioByteString(ByteBuffer.wrap(mungedBytes)))); } public void testEqualsLiteralByteString() { ByteString literal = ByteString.copyFrom(BYTES); assertEquals(CLASSNAME + " must equal LiteralByteString with same value", literal, testString); assertEquals(CLASSNAME + " must equal LiteralByteString with same value", testString, literal); assertFalse( CLASSNAME + " must not equal the empty string", testString.equals(ByteString.EMPTY)); assertEquals( CLASSNAME + " empty strings must be equal", ByteString.EMPTY, testString.substring(55, 55)); literal = ByteString.copyFrom(mungedBytes()); assertFalse( CLASSNAME + " must not equal every LiteralByteString with the same length", testString.equals(literal)); assertFalse( CLASSNAME + " must not equal every LiteralByteString with the same length", literal.equals(testString)); } public void testEqualsRopeByteString() { ByteString p1 = ByteString.copyFrom(BYTES, 0, 5); ByteString p2 = ByteString.copyFrom(BYTES, 5, BYTES.length - 5); ByteString rope = p1.concat(p2); assertEquals(CLASSNAME + " must equal RopeByteString with same value", rope, testString); assertEquals(CLASSNAME + " must equal RopeByteString with same value", testString, rope); assertFalse( CLASSNAME + " must not equal the empty string", testString.equals(ByteString.EMPTY.concat(ByteString.EMPTY))); assertEquals( CLASSNAME + " empty strings must be equal", ByteString.EMPTY.concat(ByteString.EMPTY), testString.substring(55, 55)); byte[] mungedBytes = mungedBytes(); p1 = ByteString.copyFrom(mungedBytes, 0, 5); p2 = ByteString.copyFrom(mungedBytes, 5, mungedBytes.length - 5); rope = p1.concat(p2); assertFalse( CLASSNAME + " must not equal every RopeByteString with the same length", testString.equals(rope)); assertFalse( CLASSNAME + " must not equal every RopeByteString with the same length", rope.equals(testString)); } private byte[] mungedBytes() { byte[] mungedBytes = new byte[BYTES.length]; System.arraycopy(BYTES, 0, mungedBytes, 0, BYTES.length); mungedBytes[mungedBytes.length - 5] = (byte) (mungedBytes[mungedBytes.length - 5] ^ 0xFF); return mungedBytes; } public void testHashCode() { int hash = testString.hashCode(); assertEquals(CLASSNAME + " must have expected hashCode", EXPECTED_HASH, hash); } public void testPeekCachedHashCode() { ByteString newString = new NioByteString(backingBuffer); assertEquals( CLASSNAME + ".peekCachedHashCode() should return zero at first", 0, newString.peekCachedHashCode()); newString.hashCode(); assertEquals( CLASSNAME + ".peekCachedHashCode should return zero at first", EXPECTED_HASH, newString.peekCachedHashCode()); } public void testPartialHash() { // partialHash() is more strenuously tested elsewhere by testing hashes of substrings. // This test would fail if the expected hash were 1. It's not. int hash = testString.partialHash(testString.size(), 0, testString.size()); assertEquals(CLASSNAME + ".partialHash() must yield expected hashCode", EXPECTED_HASH, hash); } public void testNewInput() throws IOException { InputStream input = testString.newInput(); assertEquals( "InputStream.available() returns correct value", testString.size(), input.available()); boolean stillEqual = true; for (byte referenceByte : BYTES) { int expectedInt = (referenceByte & 0xFF); stillEqual = (expectedInt == input.read()); } assertEquals("InputStream.available() returns correct value", 0, input.available()); assertTrue(CLASSNAME + " must give the same bytes from the InputStream", stillEqual); assertEquals(CLASSNAME + " InputStream must now be exhausted", -1, input.read()); } public void testNewInput_skip() throws IOException { InputStream input = testString.newInput(); int stringSize = testString.size(); int nearEndIndex = stringSize * 2 / 3; long skipped1 = input.skip(nearEndIndex); assertEquals("InputStream.skip()", skipped1, nearEndIndex); assertEquals("InputStream.available()", stringSize - skipped1, input.available()); assertTrue("InputStream.mark() is available", input.markSupported()); input.mark(0); assertEquals( "InputStream.skip(), read()", testString.byteAt(nearEndIndex) & 0xFF, input.read()); assertEquals("InputStream.available()", stringSize - skipped1 - 1, input.available()); long skipped2 = input.skip(stringSize); assertEquals("InputStream.skip() incomplete", skipped2, stringSize - skipped1 - 1); assertEquals("InputStream.skip(), no more input", 0, input.available()); assertEquals("InputStream.skip(), no more input", -1, input.read()); input.reset(); assertEquals("InputStream.reset() succeded", stringSize - skipped1, input.available()); assertEquals( "InputStream.reset(), read()", testString.byteAt(nearEndIndex) & 0xFF, input.read()); } public void testNewCodedInput() throws IOException { CodedInputStream cis = testString.newCodedInput(); byte[] roundTripBytes = cis.readRawBytes(BYTES.length); assertTrue( CLASSNAME + " must give the same bytes back from the CodedInputStream", Arrays.equals(BYTES, roundTripBytes)); assertTrue(CLASSNAME + " CodedInputStream must now be exhausted", cis.isAtEnd()); } /** * Make sure we keep things simple when concatenating with empty. See also {@link * ByteStringTest#testConcat_empty()}. */ public void testConcat_empty() { assertSame( CLASSNAME + " concatenated with empty must give " + CLASSNAME, testString.concat(EMPTY), testString); assertSame( "empty concatenated with " + CLASSNAME + " must give " + CLASSNAME, EMPTY.concat(testString), testString); } public void testJavaSerialization() throws Exception { ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(out); oos.writeObject(testString); oos.close(); byte[] pickled = out.toByteArray(); InputStream in = new ByteArrayInputStream(pickled); ObjectInputStream ois = new ObjectInputStream(in); Object o = ois.readObject(); assertTrue("Didn't get a ByteString back", o instanceof ByteString); assertEquals("Should get an equal ByteString back", testString, o); } private static ByteString forString(String str) { return new NioByteString(ByteBuffer.wrap(str.getBytes(UTF_8))); } }