/* Copyright (c) 2011 Danish Maritime Authority. * * 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. */ // Protocol Buffers - Google's data interchange format // Copyright 2008 Google Inc. All rights reserved. // http://code.google.com/p/protobuf/ // // 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 net.maritimecloud.util; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.Random; import junit.framework.TestCase; /** * Test methods with implementations in {@link Binary}, plus do some top-level "integration" tests. * * @author carlanton@google.com (Carl Haverl) */ // CHECKSTYLE:OFF public class ByteStringTest extends TestCase { private static final String UTF_16 = "UTF-16"; static byte[] getTestBytes(int size, long seed) { Random random = new Random(seed); byte[] result = new byte[size]; random.nextBytes(result); return result; } private byte[] getTestBytes(int size) { return getTestBytes(size, 445566L); } private byte[] getTestBytes() { return getTestBytes(1000); } // Compare the entire left array with a subset of the right array. private boolean isArrayRange(byte[] left, byte[] right, int rightOffset, int length) { boolean stillEqual = left.length == length; for (int i = 0; stillEqual && i < length; ++i) { stillEqual = left[i] == right[rightOffset + i]; } return stillEqual; } // Returns true only if the given two arrays have identical contents. private boolean isArray(byte[] left, byte[] right) { return left.length == right.length && isArrayRange(left, right, 0, left.length); } public void testSubstring_BeginIndex() { byte[] bytes = getTestBytes(); Binary substring = Binary.copyFrom(bytes).substring(500); assertTrue("substring must contain the tail of the string", isArrayRange(substring.toByteArray(), bytes, 500, bytes.length - 500)); } public void testCopyFrom_BytesOffsetSize() { byte[] bytes = getTestBytes(); Binary byteString = Binary.copyFrom(bytes, 500, 200); assertTrue("copyFrom sub-range must contain the expected bytes", isArrayRange(byteString.toByteArray(), bytes, 500, 200)); } public void testCopyFrom_Bytes() { byte[] bytes = getTestBytes(); Binary byteString = Binary.copyFrom(bytes); assertTrue("copyFrom must contain the expected bytes", isArray(byteString.toByteArray(), bytes)); } public void testCopyFrom_ByteBufferSize() { byte[] bytes = getTestBytes(); ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length); byteBuffer.put(bytes); byteBuffer.position(500); Binary byteString = Binary.copyFrom(byteBuffer, 200); assertTrue("copyFrom byteBuffer sub-range must contain the expected bytes", isArrayRange(byteString.toByteArray(), bytes, 500, 200)); } public void testCopyFrom_ByteBuffer() { byte[] bytes = getTestBytes(); ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length); byteBuffer.put(bytes); byteBuffer.position(500); Binary byteString = Binary.copyFrom(byteBuffer); assertTrue("copyFrom byteBuffer sub-range must contain the expected bytes", isArrayRange(byteString.toByteArray(), bytes, 500, bytes.length - 500)); } public void testCopyFrom_StringEncoding() throws UnsupportedEncodingException { String testString = "I love unicode \u1234\u5678 characters"; Binary byteString = Binary.copyFrom(testString, UTF_16); byte[] testBytes = testString.getBytes(UTF_16); assertTrue("copyFrom string must respect the charset", isArrayRange(byteString.toByteArray(), testBytes, 0, testBytes.length)); } public void testCopyFrom_Utf8() throws UnsupportedEncodingException { String testString = "I love unicode \u1234\u5678 characters"; Binary byteString = Binary.copyFromUtf8(testString); byte[] testBytes = testString.getBytes("UTF-8"); assertTrue("copyFromUtf8 string must respect the charset", isArrayRange(byteString.toByteArray(), testBytes, 0, testBytes.length)); } public void testCopyFrom_Iterable() { byte[] testBytes = getTestBytes(77777, 113344L); final List<Binary> pieces = makeConcretePieces(testBytes); // Call copyFrom() on a Collection Binary byteString = Binary.copyFrom(pieces); assertTrue("copyFrom a List must contain the expected bytes", isArrayRange(byteString.toByteArray(), testBytes, 0, testBytes.length)); // Call copyFrom on an iteration that's not a collection Binary byteStringAlt = Binary.copyFrom(new Iterable<Binary>() { public Iterator<Binary> iterator() { return pieces.iterator(); } }); assertEquals("copyFrom from an Iteration must contain the expected bytes", byteString, byteStringAlt); } public void testCopyTo_TargetOffset() { byte[] bytes = getTestBytes(); Binary byteString = Binary.copyFrom(bytes); byte[] target = new byte[bytes.length + 1000]; byteString.copyTo(target, 400); assertTrue("copyFrom byteBuffer sub-range must contain the expected bytes", isArrayRange(bytes, target, 400, bytes.length)); } public void testReadFrom_emptyStream() throws IOException { Binary byteString = Binary.readFrom(new ByteArrayInputStream(new byte[0])); assertSame("reading an empty stream must result in the EMPTY constant " + "byte string", Binary.EMPTY, byteString); } public void testReadFrom_smallStream() throws IOException { assertReadFrom(getTestBytes(10)); } public void testReadFrom_mutating() throws IOException { byte[] capturedArray = null; EvilInputStream eis = new EvilInputStream(); Binary byteString = Binary.readFrom(eis); capturedArray = eis.capturedArray; byte[] originalValue = byteString.toByteArray(); for (int x = 0; x < capturedArray.length; ++x) { capturedArray[x] = (byte) 0; } byte[] newValue = byteString.toByteArray(); assertTrue("copyFrom byteBuffer must not grant access to underlying array", Arrays.equals(originalValue, newValue)); } // Tests sizes that are near the rope copy-out threshold. public void testReadFrom_mediumStream() throws IOException { assertReadFrom(getTestBytes(Binary.CONCATENATE_BY_COPY_SIZE - 1)); assertReadFrom(getTestBytes(Binary.CONCATENATE_BY_COPY_SIZE)); assertReadFrom(getTestBytes(Binary.CONCATENATE_BY_COPY_SIZE + 1)); assertReadFrom(getTestBytes(200)); } // Tests sizes that are over multi-segment rope threshold. public void testReadFrom_largeStream() throws IOException { assertReadFrom(getTestBytes(0x100)); assertReadFrom(getTestBytes(0x101)); assertReadFrom(getTestBytes(0x110)); assertReadFrom(getTestBytes(0x1000)); assertReadFrom(getTestBytes(0x1001)); assertReadFrom(getTestBytes(0x1010)); assertReadFrom(getTestBytes(0x10000)); assertReadFrom(getTestBytes(0x10001)); assertReadFrom(getTestBytes(0x10010)); } // Tests sizes that are near the read buffer size. public void testReadFrom_byteBoundaries() throws IOException { final int min = Binary.MIN_READ_FROM_CHUNK_SIZE; final int max = Binary.MAX_READ_FROM_CHUNK_SIZE; assertReadFrom(getTestBytes(min - 1)); assertReadFrom(getTestBytes(min)); assertReadFrom(getTestBytes(min + 1)); assertReadFrom(getTestBytes(min * 2 - 1)); assertReadFrom(getTestBytes(min * 2)); assertReadFrom(getTestBytes(min * 2 + 1)); assertReadFrom(getTestBytes(min * 4 - 1)); assertReadFrom(getTestBytes(min * 4)); assertReadFrom(getTestBytes(min * 4 + 1)); assertReadFrom(getTestBytes(min * 8 - 1)); assertReadFrom(getTestBytes(min * 8)); assertReadFrom(getTestBytes(min * 8 + 1)); assertReadFrom(getTestBytes(max - 1)); assertReadFrom(getTestBytes(max)); assertReadFrom(getTestBytes(max + 1)); assertReadFrom(getTestBytes(max * 2 - 1)); assertReadFrom(getTestBytes(max * 2)); assertReadFrom(getTestBytes(max * 2 + 1)); } // Tests that IOExceptions propagate through ByteString.readFrom(). public void testReadFrom_IOExceptions() { try { Binary.readFrom(new FailStream()); fail("readFrom must throw the underlying IOException"); } catch (IOException e) { assertEquals("readFrom must throw the expected exception", "synthetic failure", e.getMessage()); } } // Tests that ByteString.readFrom works with streams that don't // always fill their buffers. public void testReadFrom_reluctantStream() throws IOException { final byte[] data = getTestBytes(0x1000); Binary byteString = Binary.readFrom(new ReluctantStream(data)); assertTrue("readFrom byte stream must contain the expected bytes", isArray(byteString.toByteArray(), data)); // Same test as above, but with some specific chunk sizes. assertReadFromReluctantStream(data, 100); assertReadFromReluctantStream(data, 248); assertReadFromReluctantStream(data, 249); assertReadFromReluctantStream(data, 250); assertReadFromReluctantStream(data, 251); assertReadFromReluctantStream(data, 0x1000); assertReadFromReluctantStream(data, 0x1001); } // Fails unless ByteString.readFrom reads the bytes correctly from a // reluctant stream with the given chunkSize parameter. private void assertReadFromReluctantStream(byte[] bytes, int chunkSize) throws IOException { Binary b = Binary.readFrom(new ReluctantStream(bytes), chunkSize); assertTrue("readFrom byte stream must contain the expected bytes", isArray(b.toByteArray(), bytes)); } // Tests that ByteString.readFrom works with streams that implement // available(). public void testReadFrom_available() throws IOException { final byte[] data = getTestBytes(0x1001); Binary byteString = Binary.readFrom(new AvailableStream(data)); assertTrue("readFrom byte stream must contain the expected bytes", isArray(byteString.toByteArray(), data)); } // Fails unless ByteString.readFrom reads the bytes correctly. private void assertReadFrom(byte[] bytes) throws IOException { Binary byteString = Binary.readFrom(new ByteArrayInputStream(bytes)); assertTrue("readFrom byte stream must contain the expected bytes", isArray(byteString.toByteArray(), bytes)); } public void testToStringUtf8() throws UnsupportedEncodingException { String testString = "I love unicode \u1234\u5678 characters"; byte[] testBytes = testString.getBytes("UTF-8"); Binary byteString = Binary.copyFrom(testBytes); assertEquals("copyToStringUtf8 must respect the charset", testString, byteString.toStringUtf8()); } public void testNewOutput_InitialCapacity() throws IOException { byte[] bytes = getTestBytes(); Binary.Output output = Binary.newOutput(bytes.length + 100); output.write(bytes); Binary byteString = output.toBinary(); assertTrue("String built from newOutput(int) must contain the expected bytes", isArrayRange(bytes, byteString.toByteArray(), 0, bytes.length)); } // Test newOutput() using a variety of buffer sizes and a variety of (fixed) // write sizes public void testNewOutput_ArrayWrite() { byte[] bytes = getTestBytes(); int length = bytes.length; int[] bufferSizes = { 128, 256, length / 2, length - 1, length, length + 1, 2 * length, 3 * length }; int[] writeSizes = { 1, 4, 5, 7, 23, bytes.length }; for (int bufferSize : bufferSizes) { for (int writeSize : writeSizes) { // Test writing the entire output writeSize bytes at a time. Binary.Output output = Binary.newOutput(bufferSize); for (int i = 0; i < length; i += writeSize) { output.write(bytes, i, Math.min(writeSize, length - i)); } Binary byteString = output.toBinary(); assertTrue("String built from newOutput() must contain the expected bytes", isArrayRange(bytes, byteString.toByteArray(), 0, bytes.length)); } } } // Test newOutput() using a variety of buffer sizes, but writing all the // characters using write(byte); public void testNewOutput_WriteChar() { byte[] bytes = getTestBytes(); int length = bytes.length; int[] bufferSizes = { 0, 1, 128, 256, length / 2, length - 1, length, length + 1, 2 * length, 3 * length }; for (int bufferSize : bufferSizes) { Binary.Output output = Binary.newOutput(bufferSize); for (byte byteValue : bytes) { output.write(byteValue); } Binary byteString = output.toBinary(); assertTrue("String built from newOutput() must contain the expected bytes", isArrayRange(bytes, byteString.toByteArray(), 0, bytes.length)); } } // Test newOutput() in which we write the bytes using a variety of methods // and sizes, and in which we repeatedly call toByteString() in the middle. public void testNewOutput_Mixed() { Random rng = new Random(1); byte[] bytes = getTestBytes(); int length = bytes.length; int[] bufferSizes = { 0, 1, 128, 256, length / 2, length - 1, length, length + 1, 2 * length, 3 * length }; for (int bufferSize : bufferSizes) { // Test writing the entire output using a mixture of write sizes and // methods; Binary.Output output = Binary.newOutput(bufferSize); int position = 0; while (position < bytes.length) { if (rng.nextBoolean()) { int count = 1 + rng.nextInt(bytes.length - position); output.write(bytes, position, count); position += count; } else { output.write(bytes[position]); position++; } assertEquals("size() returns the right value", position, output.size()); assertTrue("newOutput() substring must have correct bytes", isArrayRange(output.toBinary().toByteArray(), bytes, 0, position)); } Binary byteString = output.toBinary(); assertTrue("String built from newOutput() must contain the expected bytes", isArrayRange(bytes, byteString.toByteArray(), 0, bytes.length)); } } public void testNewOutputEmpty() { // Make sure newOutput() correctly builds empty byte strings Binary byteString = Binary.newOutput().toBinary(); assertEquals(Binary.EMPTY, byteString); } public void testNewOutput_Mutating() throws IOException { Binary.Output os = Binary.newOutput(5); os.write(new byte[] { 1, 2, 3, 4, 5 }); EvilOutputStream eos = new EvilOutputStream(); os.writeTo(eos); byte[] capturedArray = eos.capturedArray; Binary byteString = os.toBinary(); byte[] oldValue = byteString.toByteArray(); Arrays.fill(capturedArray, (byte) 0); byte[] newValue = byteString.toByteArray(); assertTrue("Output must not provide access to the underlying byte array", Arrays.equals(oldValue, newValue)); } // // public void testNewCodedBuilder() throws IOException { // byte[] bytes = getTestBytes(); // ByteString.CodedBuilder builder = ByteString.newCodedBuilder(bytes.length); // builder.getCodedOutput().writeRawBytes(bytes); // ByteString byteString = builder.build(); // assertTrue("String built from newCodedBuilder() must contain the expected bytes", // isArrayRange(bytes, byteString.toByteArray(), 0, bytes.length)); // } public void testSubstringParity() { byte[] bigBytes = getTestBytes(2048 * 1024, 113344L); int start = 512 * 1024 - 3333; int end = 512 * 1024 + 7777; Binary concreteSubstring = Binary.copyFrom(bigBytes).substring(start, end); boolean ok = true; for (int i = start; ok && i < end; ++i) { ok = bigBytes[i] == concreteSubstring.byteAt(i - start); } assertTrue("Concrete substring didn't capture the right bytes", ok); Binary literalString = Binary.copyFrom(bigBytes, start, end - start); assertTrue("Substring must be equal to literal string", concreteSubstring.equals(literalString)); assertEquals("Substring must have same hashcode as literal string", literalString.hashCode(), concreteSubstring.hashCode()); } public void testCompositeSubstring() { byte[] referenceBytes = getTestBytes(77748, 113344L); List<Binary> pieces = makeConcretePieces(referenceBytes); Binary listString = Binary.copyFrom(pieces); int from = 1000; int to = 40000; Binary compositeSubstring = listString.substring(from, to); byte[] substringBytes = compositeSubstring.toByteArray(); boolean stillEqual = true; for (int i = 0; stillEqual && i < to - from; ++i) { stillEqual = referenceBytes[from + i] == substringBytes[i]; } assertTrue("Substring must return correct bytes", stillEqual); stillEqual = true; for (int i = 0; stillEqual && i < to - from; ++i) { stillEqual = referenceBytes[from + i] == compositeSubstring.byteAt(i); } assertTrue("Substring must support byteAt() correctly", stillEqual); Binary literalSubstring = Binary.copyFrom(referenceBytes, from, to - from); assertTrue("Composite substring must equal a literal substring over the same bytes", compositeSubstring.equals(literalSubstring)); assertTrue("Literal substring must equal a composite substring over the same bytes", literalSubstring.equals(compositeSubstring)); assertEquals("We must get the same hashcodes for composite and literal substrings", literalSubstring.hashCode(), compositeSubstring.hashCode()); assertFalse("We can't be equal to a proper substring", compositeSubstring.equals(literalSubstring.substring(0, literalSubstring.size() - 1))); } public void testCopyFromList() { byte[] referenceBytes = getTestBytes(77748, 113344L); Binary literalString = Binary.copyFrom(referenceBytes); List<Binary> pieces = makeConcretePieces(referenceBytes); Binary listString = Binary.copyFrom(pieces); assertTrue("Composite string must be equal to literal string", listString.equals(literalString)); assertEquals("Composite string must have same hashcode as literal string", literalString.hashCode(), listString.hashCode()); } public void testConcat() { byte[] referenceBytes = getTestBytes(77748, 113344L); Binary literalString = Binary.copyFrom(referenceBytes); List<Binary> pieces = makeConcretePieces(referenceBytes); Iterator<Binary> iter = pieces.iterator(); Binary concatenatedString = iter.next(); while (iter.hasNext()) { concatenatedString = concatenatedString.concat(iter.next()); } assertTrue("Concatenated string must be equal to literal string", concatenatedString.equals(literalString)); assertEquals("Concatenated string must have same hashcode as literal string", literalString.hashCode(), concatenatedString.hashCode()); } /** * Test the Rope implementation can deal with Empty nodes, even though we guard against them. See also * {@link LiteralByteStringTest#testConcat_empty()}. */ public void testConcat_empty() { byte[] referenceBytes = getTestBytes(7748, 113344L); Binary literalString = Binary.copyFrom(referenceBytes); Binary duo = RopeBinary.newInstanceForTest(literalString, literalString); Binary temp = RopeBinary.newInstanceForTest(RopeBinary.newInstanceForTest(literalString, Binary.EMPTY), RopeBinary.newInstanceForTest(Binary.EMPTY, literalString)); Binary quintet = RopeBinary.newInstanceForTest(temp, Binary.EMPTY); assertTrue("String with concatenated nulls must equal simple concatenate", duo.equals(quintet)); assertEquals("String with concatenated nulls have same hashcode as simple concatenate", duo.hashCode(), quintet.hashCode()); Binary.ByteIterator duoIter = duo.iterator(); Binary.ByteIterator quintetIter = quintet.iterator(); boolean stillEqual = true; while (stillEqual && quintetIter.hasNext()) { stillEqual = duoIter.nextByte() == quintetIter.nextByte(); } assertTrue("We must get the same characters by iterating", stillEqual); assertFalse("Iterator must be exhausted", duoIter.hasNext()); try { duoIter.nextByte(); fail("Should have thrown an exception."); } catch (NoSuchElementException e) { // This is success } try { quintetIter.nextByte(); fail("Should have thrown an exception."); } catch (NoSuchElementException e) { // This is success } // Test that even if we force empty strings in as rope leaves in this // configuration, we always get a (possibly Bounded) LiteralByteString // for a length 1 substring. // // It is possible, using the testing factory method to create deeply nested // trees of empty leaves, to make a string that will fail this test. for (int i = 1; i < duo.size(); ++i) { assertTrue("Substrings of size() < 2 must not be RopeByteStrings", duo.substring(i - 1, i) instanceof LiteralBinary); } for (int i = 1; i < quintet.size(); ++i) { assertTrue("Substrings of size() < 2 must not be RopeByteStrings", quintet.substring(i - 1, i) instanceof LiteralBinary); } } public void testStartsWith() { byte[] bytes = getTestBytes(1000, 1234L); Binary string = Binary.copyFrom(bytes); Binary prefix = Binary.copyFrom(bytes, 0, 500); Binary suffix = Binary.copyFrom(bytes, 400, 600); assertTrue(string.startsWith(Binary.EMPTY)); assertTrue(string.startsWith(string)); assertTrue(string.startsWith(prefix)); assertFalse(string.startsWith(suffix)); assertFalse(prefix.startsWith(suffix)); assertFalse(suffix.startsWith(prefix)); assertFalse(Binary.EMPTY.startsWith(prefix)); assertTrue(Binary.EMPTY.startsWith(Binary.EMPTY)); } static List<Binary> makeConcretePieces(byte[] referenceBytes) { List<Binary> pieces = new ArrayList<>(); // Starting length should be small enough that we'll do some concatenating by // copying if we just concatenate all these pieces together. for (int start = 0, length = 16; start < referenceBytes.length; start += length) { length = (length << 1) - 1; if (start + length > referenceBytes.length) { length = referenceBytes.length - start; } pieces.add(Binary.copyFrom(referenceBytes, start, length)); } return pieces; } // A stream that fails when read. static final class FailStream extends InputStream { @Override public int read() throws IOException { throw new IOException("synthetic failure"); } } // A stream that simulates blocking by only producing 250 characters // per call to read(byte[]). private static class ReluctantStream extends InputStream { protected final byte[] data; protected int pos; public ReluctantStream(byte[] data) { this.data = data; } @Override public int read() { if (pos == data.length) { return -1; } else { return data[pos++]; } } @Override public int read(byte[] buf) { return read(buf, 0, buf.length); } @Override public int read(byte[] buf, int offset, int size) { if (pos == data.length) { return -1; } int count = Math.min(Math.min(size, data.length - pos), 250); System.arraycopy(data, pos, buf, offset, count); pos += count; return count; } } // Same as above, but also implements available(). private static final class AvailableStream extends ReluctantStream { public AvailableStream(byte[] data) { super(data); } @Override public int available() { return Math.min(250, data.length - pos); } } // A stream which exposes the byte array passed into read(byte[], int, int). static class EvilInputStream extends InputStream { public byte[] capturedArray; @Override public int read(byte[] buf, int off, int len) { if (capturedArray != null) { return -1; } else { capturedArray = buf; for (int x = 0; x < len; ++x) { buf[x] = (byte) x; } return len; } } @Override public int read() { // Purposefully do nothing. return -1; } } // A stream which exposes the byte array passed into write(byte[], int, int). static class EvilOutputStream extends OutputStream { public byte[] capturedArray; @Override public void write(byte[] buf, int off, int len) { if (capturedArray == null) { capturedArray = buf; } } @Override public void write(int ignored) { // Purposefully do nothing. } } } // CHECKSTYLE:ON