diff options
Diffstat (limited to 'java')
44 files changed, 2388 insertions, 854 deletions
diff --git a/java/pom.xml b/java/pom.xml index 7b99c035..aa5d30ea 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -295,15 +295,19 @@ <include>**/MapFieldLite.java</include> <include>**/MessageLite.java</include> <include>**/MessageLiteOrBuilder.java</include> + <include>**/MessageLiteToString.java</include> <include>**/MutabilityOracle.java</include> + <include>**/NioByteString.java</include> <include>**/Parser.java</include> <include>**/ProtobufArrayList.java</include> <include>**/ProtocolStringList.java</include> <include>**/RopeByteString.java</include> <include>**/SmallSortedMap.java</include> + <include>**/TextFormatEscaper.java</include> <include>**/UninitializedMessageException.java</include> <include>**/UnknownFieldSetLite.java</include> <include>**/UnmodifiableLazyStringList.java</include> + <include>**/UnsafeByteStrings.java</include> <include>**/Utf8.java</include> <include>**/WireFormat.java</include> </includes> @@ -316,6 +320,7 @@ <testInclude>**/LazyMessageLiteTest.java</testInclude> <testInclude>**/LiteTest.java</testInclude> <testInclude>**/LongArrayListTest.java</testInclude> + <testInclude>**/NioByteStringTest.java</testInclude> <testInclude>**/ProtobufArrayListTest.java</testInclude> <testInclude>**/UnknownFieldSetLiteTest.java</testInclude> </testIncludes> diff --git a/java/src/main/java/com/google/protobuf/AbstractMessage.java b/java/src/main/java/com/google/protobuf/AbstractMessage.java index cc89173a..9f418f2b 100644 --- a/java/src/main/java/com/google/protobuf/AbstractMessage.java +++ b/java/src/main/java/com/google/protobuf/AbstractMessage.java @@ -30,6 +30,7 @@ package com.google.protobuf; +import com.google.protobuf.Descriptors.EnumValueDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.OneofDescriptor; import com.google.protobuf.Internal.EnumLite; @@ -161,10 +162,18 @@ public abstract class AbstractMessage extends AbstractMessageLite Descriptors.Descriptor descriptor = entry.getDescriptorForType(); Descriptors.FieldDescriptor key = descriptor.findFieldByName("key"); Descriptors.FieldDescriptor value = descriptor.findFieldByName("value"); - result.put(entry.getField(key), entry.getField(value)); + Object fieldValue = entry.getField(value); + if (fieldValue instanceof EnumValueDescriptor) { + fieldValue = ((EnumValueDescriptor) fieldValue).getNumber(); + } + result.put(entry.getField(key), fieldValue); while (iterator.hasNext()) { entry = (Message) iterator.next(); - result.put(entry.getField(key), entry.getField(value)); + fieldValue = entry.getField(value); + if (fieldValue instanceof EnumValueDescriptor) { + fieldValue = ((EnumValueDescriptor) fieldValue).getNumber(); + } + result.put(entry.getField(key), fieldValue); } return result; } diff --git a/java/src/main/java/com/google/protobuf/BooleanArrayList.java b/java/src/main/java/com/google/protobuf/BooleanArrayList.java index 45492d2f..70e042f5 100644 --- a/java/src/main/java/com/google/protobuf/BooleanArrayList.java +++ b/java/src/main/java/com/google/protobuf/BooleanArrayList.java @@ -68,10 +68,17 @@ final class BooleanArrayList private int size; /** - * Constructs a new mutable {@code BooleanArrayList}. + * Constructs a new mutable {@code BooleanArrayList} with default capacity. */ BooleanArrayList() { - array = new boolean[DEFAULT_CAPACITY]; + this(DEFAULT_CAPACITY); + } + + /** + * Constructs a new mutable {@code BooleanArrayList} with the provided capacity. + */ + BooleanArrayList(int capacity) { + array = new boolean[capacity]; size = 0; } diff --git a/java/src/main/java/com/google/protobuf/BoundedByteString.java b/java/src/main/java/com/google/protobuf/BoundedByteString.java index b4c3fb1b..934c9030 100644 --- a/java/src/main/java/com/google/protobuf/BoundedByteString.java +++ b/java/src/main/java/com/google/protobuf/BoundedByteString.java @@ -33,7 +33,6 @@ package com.google.protobuf; import java.io.IOException; import java.io.InvalidObjectException; import java.io.ObjectInputStream; -import java.util.NoSuchElementException; /** * This class is used to represent the substring of a {@link ByteString} over a @@ -47,7 +46,7 @@ import java.util.NoSuchElementException; * * @author carlanton@google.com (Carl Haverl) */ -class BoundedByteString extends LiteralByteString { +final class BoundedByteString extends LiteralByteString { private final int bytesOffset; private final int bytesLength; @@ -65,16 +64,7 @@ class BoundedByteString extends LiteralByteString { */ BoundedByteString(byte[] bytes, int offset, int length) { super(bytes); - if (offset < 0) { - throw new IllegalArgumentException("Offset too small: " + offset); - } - if (length < 0) { - throw new IllegalArgumentException("Length too small: " + offset); - } - if ((long) offset + length > bytes.length) { - throw new IllegalArgumentException( - "Offset+Length too large: " + offset + "+" + length); - } + checkRange(offset, offset + length, bytes.length); this.bytesOffset = offset; this.bytesLength = length; @@ -94,14 +84,7 @@ class BoundedByteString extends LiteralByteString { public byte byteAt(int index) { // We must check the index ourselves as we cannot rely on Java array index // checking for substrings. - if (index < 0) { - throw new ArrayIndexOutOfBoundsException("Index too small: " + index); - } - if (index >= size()) { - throw new ArrayIndexOutOfBoundsException( - "Index too large: " + index + ", " + size()); - } - + checkIndex(index, size()); return bytes[bytesOffset + index]; } @@ -119,8 +102,8 @@ class BoundedByteString extends LiteralByteString { // ByteString -> byte[] @Override - protected void copyToInternal(byte[] target, int sourceOffset, - int targetOffset, int numberToCopy) { + protected void copyToInternal(byte[] target, int sourceOffset, int targetOffset, + int numberToCopy) { System.arraycopy(bytes, getOffsetIntoBytes() + sourceOffset, target, targetOffset, numberToCopy); } @@ -134,47 +117,8 @@ class BoundedByteString extends LiteralByteString { return new LiteralByteString(toByteArray()); } - private void readObject(ObjectInputStream in) throws IOException { + private void readObject(@SuppressWarnings("unused") ObjectInputStream in) throws IOException { throw new InvalidObjectException( "BoundedByteStream instances are not to be serialized directly"); } - - // ================================================================= - // ByteIterator - - @Override - public ByteIterator iterator() { - return new BoundedByteIterator(); - } - - private class BoundedByteIterator implements ByteIterator { - - private int position; - private final int limit; - - private BoundedByteIterator() { - position = getOffsetIntoBytes(); - limit = position + size(); - } - - public boolean hasNext() { - return (position < limit); - } - - public Byte next() { - // Boxing calls Byte.valueOf(byte), which does not instantiate. - return nextByte(); - } - - public byte nextByte() { - if (position >= limit) { - throw new NoSuchElementException(); - } - return bytes[position++]; - } - - public void remove() { - throw new UnsupportedOperationException(); - } - } } diff --git a/java/src/main/java/com/google/protobuf/ByteString.java b/java/src/main/java/com/google/protobuf/ByteString.java index b092bc36..68f20d51 100644 --- a/java/src/main/java/com/google/protobuf/ByteString.java +++ b/java/src/main/java/com/google/protobuf/ByteString.java @@ -83,6 +83,13 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { */ public static final ByteString EMPTY = new LiteralByteString(new byte[0]); + /** + * Cached hash value. Intentionally accessed via a data race, which + * is safe because of the Java Memory Model's "no out-of-thin-air values" + * guarantees for ints. A value of 0 implies that the hash has not been set. + */ + private int hash = 0; + // This constructor is here to prevent subclassing outside of this package, ByteString() {} @@ -105,7 +112,38 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * * @return the iterator */ - public abstract ByteIterator iterator(); + @Override + public final ByteIterator iterator() { + return new ByteIterator() { + private int position = 0; + private final int limit = size(); + + @Override + public boolean hasNext() { + return position < limit; + } + + @Override + public Byte next() { + // Boxing calls Byte.valueOf(byte), which does not instantiate. + return nextByte(); + } + + @Override + public byte nextByte() { + try { + return byteAt(position++); + } catch (ArrayIndexOutOfBoundsException e) { + throw new NoSuchElementException(e.getMessage()); + } + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } /** * This interface extends {@code Iterator<Byte>}, so that we can return an @@ -134,7 +172,7 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * * @return true if this is zero bytes long */ - public boolean isEmpty() { + public final boolean isEmpty() { return size() == 0; } @@ -150,7 +188,7 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * @throws IndexOutOfBoundsException if {@code beginIndex < 0} or * {@code beginIndex > size()}. */ - public ByteString substring(int beginIndex) { + public final ByteString substring(int beginIndex) { return substring(beginIndex, size()); } @@ -175,7 +213,7 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * argument is a prefix of the byte sequence represented by * this string; <code>false</code> otherwise. */ - public boolean startsWith(ByteString prefix) { + public final boolean startsWith(ByteString prefix) { return size() >= prefix.size() && substring(0, prefix.size()).equals(prefix); } @@ -189,7 +227,7 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * argument is a suffix of the byte sequence represented by * this string; <code>false</code> otherwise. */ - public boolean endsWith(ByteString suffix) { + public final boolean endsWith(ByteString suffix) { return size() >= suffix.size() && substring(size() - suffix.size()).equals(suffix); } @@ -309,8 +347,7 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { */ public static ByteString readFrom(InputStream streamToDrain) throws IOException { - return readFrom( - streamToDrain, MIN_READ_FROM_CHUNK_SIZE, MAX_READ_FROM_CHUNK_SIZE); + return readFrom(streamToDrain, MIN_READ_FROM_CHUNK_SIZE, MAX_READ_FROM_CHUNK_SIZE); } /** @@ -383,10 +420,10 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { if (bytesRead == 0) { return null; - } else { - // Always make a copy since InputStream could steal a reference to buf. - return ByteString.copyFrom(buf, 0, bytesRead); } + + // Always make a copy since InputStream could steal a reference to buf. + return ByteString.copyFrom(buf, 0, bytesRead); } // ================================================================= @@ -402,12 +439,10 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * @param other string to concatenate * @return a new {@code ByteString} instance */ - public ByteString concat(ByteString other) { - int thisSize = size(); - int otherSize = other.size(); - if ((long) thisSize + otherSize >= Integer.MAX_VALUE) { + public final ByteString concat(ByteString other) { + if (Integer.MAX_VALUE - size() < other.size()) { throw new IllegalArgumentException("ByteString would be too long: " + - thisSize + "+" + otherSize); + size() + "+" + other.size()); } return RopeByteString.concatenate(this, other); @@ -426,29 +461,29 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * @return new {@code ByteString} */ public static ByteString copyFrom(Iterable<ByteString> byteStrings) { - Collection<ByteString> collection; + // Determine the size; + final int size; if (!(byteStrings instanceof Collection)) { - collection = new ArrayList<ByteString>(); - for (ByteString byteString : byteStrings) { - collection.add(byteString); + int tempSize = 0; + for (Iterator<ByteString> iter = byteStrings.iterator(); iter.hasNext(); + iter.next(), ++tempSize) { } + size = tempSize; } else { - collection = (Collection<ByteString>) byteStrings; + size = ((Collection<ByteString>) byteStrings).size(); } - ByteString result; - if (collection.isEmpty()) { - result = EMPTY; - } else { - result = balancedConcat(collection.iterator(), collection.size()); + + if (size == 0) { + return EMPTY; } - return result; + + return balancedConcat(byteStrings.iterator(), size); } // Internal function used by copyFrom(Iterable<ByteString>). // Create a balanced concatenation of the next "length" elements from the // iterable. - private static ByteString balancedConcat(Iterator<ByteString> iterator, - int length) { + private static ByteString balancedConcat(Iterator<ByteString> iterator, int length) { assert length >= 1; ByteString result; if (length == 1) { @@ -486,25 +521,10 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * @throws IndexOutOfBoundsException if an offset or size is negative or too * large */ - public void copyTo(byte[] target, int sourceOffset, int targetOffset, + public final void copyTo(byte[] target, int sourceOffset, int targetOffset, int numberToCopy) { - if (sourceOffset < 0) { - throw new IndexOutOfBoundsException("Source offset < 0: " + sourceOffset); - } - if (targetOffset < 0) { - throw new IndexOutOfBoundsException("Target offset < 0: " + targetOffset); - } - if (numberToCopy < 0) { - throw new IndexOutOfBoundsException("Length < 0: " + numberToCopy); - } - if (sourceOffset + numberToCopy > size()) { - throw new IndexOutOfBoundsException( - "Source end offset < 0: " + (sourceOffset + numberToCopy)); - } - if (targetOffset + numberToCopy > target.length) { - throw new IndexOutOfBoundsException( - "Target end offset < 0: " + (targetOffset + numberToCopy)); - } + checkRange(sourceOffset, sourceOffset + numberToCopy, size()); + checkRange(targetOffset, targetOffset + numberToCopy, target.length); if (numberToCopy > 0) { copyToInternal(target, sourceOffset, targetOffset, numberToCopy); } @@ -534,8 +554,8 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * * @return copied bytes */ - public byte[] toByteArray() { - int size = size(); + public final byte[] toByteArray() { + final int size = size(); if (size == 0) { return Internal.EMPTY_BYTE_ARRAY; } @@ -548,6 +568,10 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * Writes the complete contents of this byte string to * the specified output stream argument. * + * <p>It is assumed that the {@link OutputStream} will not modify the contents passed it + * it. It may be possible for a malicious {@link OutputStream} to corrupt + * the data underlying the {@link ByteString}. + * * @param out the output stream to which to write the data. * @throws IOException if an I/O error occurs. */ @@ -563,30 +587,20 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * @throws IndexOutOfBoundsException if an offset or size is negative or too * large */ - void writeTo(OutputStream out, int sourceOffset, int numberToWrite) + final void writeTo(OutputStream out, int sourceOffset, int numberToWrite) throws IOException { - if (sourceOffset < 0) { - throw new IndexOutOfBoundsException("Source offset < 0: " + sourceOffset); - } - if (numberToWrite < 0) { - throw new IndexOutOfBoundsException("Length < 0: " + numberToWrite); - } - if (sourceOffset + numberToWrite > size()) { - throw new IndexOutOfBoundsException( - "Source end offset exceeded: " + (sourceOffset + numberToWrite)); - } + checkRange(sourceOffset, sourceOffset + numberToWrite, size()); if (numberToWrite > 0) { writeToInternal(out, sourceOffset, numberToWrite); } - } /** * Internal version of {@link #writeTo(OutputStream,int,int)} that assumes * all error checking has already been done. */ - abstract void writeToInternal(OutputStream out, int sourceOffset, - int numberToWrite) throws IOException; + abstract void writeToInternal(OutputStream out, int sourceOffset, int numberToWrite) + throws IOException; /** * Constructs a read-only {@code java.nio.ByteBuffer} whose content @@ -618,7 +632,7 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * @return new string * @throws UnsupportedEncodingException if charset isn't recognized */ - public String toString(String charsetName) + public final String toString(String charsetName) throws UnsupportedEncodingException { try { return toString(Charset.forName(charsetName)); @@ -636,7 +650,7 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * @param charset encode using this charset * @return new string */ - public String toString(Charset charset) { + public final String toString(Charset charset) { return size() == 0 ? "" : toStringInternal(charset); } @@ -657,7 +671,7 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * * @return new string using UTF-8 encoding */ - public String toStringUtf8() { + public final String toStringUtf8() { return toString(Internal.UTF_8); } @@ -716,13 +730,51 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { public abstract boolean equals(Object o); /** - * Return a non-zero hashCode depending only on the sequence of bytes - * in this ByteString. + * Base class for leaf {@link ByteString}s (i.e. non-ropes). + */ + abstract static class LeafByteString extends ByteString { + @Override + protected final int getTreeDepth() { + return 0; + } + + @Override + protected final boolean isBalanced() { + return true; + } + + /** + * Check equality of the substring of given length of this object starting at + * zero with another {@code ByteString} substring starting at offset. + * + * @param other what to compare a substring in + * @param offset offset into other + * @param length number of bytes to compare + * @return true for equality of substrings, else false. + */ + abstract boolean equalsRange(ByteString other, int offset, int length); + } + + /** + * Compute the hashCode using the traditional algorithm from {@link + * ByteString}. * - * @return hashCode value for this object + * @return hashCode value */ @Override - public abstract int hashCode(); + public final int hashCode() { + int h = hash; + + if (h == 0) { + int size = size(); + h = partialHash(size, 0, size); + if (h == 0) { + h = 1; + } + hash = h; + } + return h; + } // ================================================================= // Input stream @@ -1034,7 +1086,9 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { * * @return value of cached hash code or 0 if not computed yet */ - protected abstract int peekCachedHashCode(); + protected final int peekCachedHashCode() { + return hash; + } /** * Compute the hash across the value bytes starting with the given hash, and @@ -1049,8 +1103,49 @@ public abstract class ByteString implements Iterable<Byte>, Serializable { */ protected abstract int partialHash(int h, int offset, int length); + /** + * Checks that the given index falls within the specified array size. + * + * @param index the index position to be tested + * @param size the length of the array + * @throws ArrayIndexOutOfBoundsException if the index does not fall within the array. + */ + static void checkIndex(int index, int size) { + if ((index | (size - (index + 1))) < 0) { + if (index < 0) { + throw new ArrayIndexOutOfBoundsException("Index < 0: " + index); + } + throw new ArrayIndexOutOfBoundsException("Index > length: " + index + ", " + size); + } + } + + /** + * Checks that the given range falls within the bounds of an array + * + * @param startIndex the start index of the range (inclusive) + * @param endIndex the end index of the range (exclusive) + * @param size the size of the array. + * @return the length of the range. + * @throws ArrayIndexOutOfBoundsException some or all of the range falls outside of the array. + */ + static int checkRange(int startIndex, int endIndex, int size) { + final int length = endIndex - startIndex; + if ((startIndex | endIndex | length | (size - endIndex)) < 0) { + if (startIndex < 0) { + throw new IndexOutOfBoundsException("Beginning index: " + startIndex + " < 0"); + } + if (endIndex < startIndex) { + throw new IndexOutOfBoundsException( + "Beginning index larger than ending index: " + startIndex + ", " + endIndex); + } + // endIndex >= size + throw new IndexOutOfBoundsException("End index: " + endIndex + " >= " + size); + } + return length; + } + @Override - public String toString() { + public final String toString() { return String.format("<ByteString@%s size=%d>", Integer.toHexString(System.identityHashCode(this)), size()); } diff --git a/java/src/main/java/com/google/protobuf/CodedInputStream.java b/java/src/main/java/com/google/protobuf/CodedInputStream.java index d201f7c5..adc91536 100644 --- a/java/src/main/java/com/google/protobuf/CodedInputStream.java +++ b/java/src/main/java/com/google/protobuf/CodedInputStream.java @@ -1056,20 +1056,6 @@ public final class CodedInputStream { private RefillCallback refillCallback = null; /** - * Ensures that at least {@code n} bytes are available in the buffer, reading - * more bytes from the input if necessary to make it so. Caller must ensure - * that the requested space is less than BUFFER_SIZE. - * - * @throws InvalidProtocolBufferException The end of the stream or the current - * limit was reached. - */ - private void ensureAvailable(int n) throws IOException { - if (bufferSize - bufferPos < n) { - refillBuffer(n); - } - } - - /** * Reads more bytes from the input, making at least {@code n} bytes available * in the buffer. Caller must ensure that the requested space is not yet * available, and that the requested space is less than BUFFER_SIZE. @@ -1180,86 +1166,97 @@ public final class CodedInputStream { } } - if (totalBytesRetired + bufferPos + size > currentLimit) { + // Verify that the message size so far has not exceeded sizeLimit. + int currentMessageSize = totalBytesRetired + bufferPos + size; + if (currentMessageSize > sizeLimit) { + throw InvalidProtocolBufferException.sizeLimitExceeded(); + } + + // Verify that the message size so far has not exceeded currentLimit. + if (currentMessageSize > currentLimit) { // Read to the end of the stream anyway. skipRawBytes(currentLimit - totalBytesRetired - bufferPos); - // Then fail. throw InvalidProtocolBufferException.truncatedMessage(); } - if (size < BUFFER_SIZE) { - // Reading more bytes than are in the buffer, but not an excessive number - // of bytes. We can safely allocate the resulting array ahead of time. + // We need the input stream to proceed. + if (input == null) { + throw InvalidProtocolBufferException.truncatedMessage(); + } + + final int originalBufferPos = bufferPos; + final int bufferedBytes = bufferSize - bufferPos; + + // Mark the current buffer consumed. + totalBytesRetired += bufferSize; + bufferPos = 0; + bufferSize = 0; - // First copy what we have. + // Determine the number of bytes we need to read from the input stream. + int sizeLeft = size - bufferedBytes; + // TODO(nathanmittler): Consider using a value larger than BUFFER_SIZE. + if (sizeLeft < BUFFER_SIZE || sizeLeft <= input.available()) { + // Either the bytes we need are known to be available, or the required buffer is + // within an allowed threshold - go ahead and allocate the buffer now. final byte[] bytes = new byte[size]; - int pos = bufferSize - bufferPos; - System.arraycopy(buffer, bufferPos, bytes, 0, pos); - bufferPos = bufferSize; - // We want to refill the buffer and then copy from the buffer into our - // byte array rather than reading directly into our byte array because - // the input may be unbuffered. - ensureAvailable(size - pos); - System.arraycopy(buffer, 0, bytes, pos, size - pos); - bufferPos = size - pos; + // Copy all of the buffered bytes to the result buffer. + System.arraycopy(buffer, originalBufferPos, bytes, 0, bufferedBytes); - return bytes; - } else { - // The size is very large. For security reasons, we can't allocate the - // entire byte array yet. The size comes directly from the input, so a - // maliciously-crafted message could provide a bogus very large size in - // order to trick the app into allocating a lot of memory. We avoid this - // by allocating and reading only a small chunk at a time, so that the - // malicious message must actually *be* extremely large to cause - // problems. Meanwhile, we limit the allowed size of a message elsewhere. - - // Remember the buffer markers since we'll have to copy the bytes out of - // it later. - final int originalBufferPos = bufferPos; - final int originalBufferSize = bufferSize; - - // Mark the current buffer consumed. - totalBytesRetired += bufferSize; - bufferPos = 0; - bufferSize = 0; - - // Read all the rest of the bytes we need. - int sizeLeft = size - (originalBufferSize - originalBufferPos); - final List<byte[]> chunks = new ArrayList<byte[]>(); - - while (sizeLeft > 0) { - final byte[] chunk = new byte[Math.min(sizeLeft, BUFFER_SIZE)]; - int pos = 0; - while (pos < chunk.length) { - final int n = (input == null) ? -1 : - input.read(chunk, pos, chunk.length - pos); - if (n == -1) { - throw InvalidProtocolBufferException.truncatedMessage(); - } - totalBytesRetired += n; - pos += n; + // Fill the remaining bytes from the input stream. + int pos = bufferedBytes; + while (pos < bytes.length) { + int n = input.read(bytes, pos, size - pos); + if (n == -1) { + throw InvalidProtocolBufferException.truncatedMessage(); } - sizeLeft -= chunk.length; - chunks.add(chunk); + totalBytesRetired += n; + pos += n; } - // OK, got everything. Now concatenate it all into one buffer. - final byte[] bytes = new byte[size]; - - // Start by copying the leftover bytes from this.buffer. - int pos = originalBufferSize - originalBufferPos; - System.arraycopy(buffer, originalBufferPos, bytes, 0, pos); + return bytes; + } - // And now all the chunks. - for (final byte[] chunk : chunks) { - System.arraycopy(chunk, 0, bytes, pos, chunk.length); - pos += chunk.length; + // The size is very large. For security reasons, we can't allocate the + // entire byte array yet. The size comes directly from the input, so a + // maliciously-crafted message could provide a bogus very large size in + // order to trick the app into allocating a lot of memory. We avoid this + // by allocating and reading only a small chunk at a time, so that the + // malicious message must actually *be* extremely large to cause + // problems. Meanwhile, we limit the allowed size of a message elsewhere. + final List<byte[]> chunks = new ArrayList<byte[]>(); + + while (sizeLeft > 0) { + // TODO(nathanmittler): Consider using a value larger than BUFFER_SIZE. + final byte[] chunk = new byte[Math.min(sizeLeft, BUFFER_SIZE)]; + int pos = 0; + while (pos < chunk.length) { + final int n = input.read(chunk, pos, chunk.length - pos); + if (n == -1) { + throw InvalidProtocolBufferException.truncatedMessage(); + } + totalBytesRetired += n; + pos += n; } + sizeLeft -= chunk.length; + chunks.add(chunk); + } - // Done. - return bytes; + // OK, got everything. Now concatenate it all into one buffer. + final byte[] bytes = new byte[size]; + + // Start by copying the leftover bytes from this.buffer. + System.arraycopy(buffer, originalBufferPos, bytes, 0, bufferedBytes); + + // And now all the chunks. + int pos = bufferedBytes; + for (final byte[] chunk : chunks) { + System.arraycopy(chunk, 0, bytes, pos, chunk.length); + pos += chunk.length; } + + // Done. + return bytes; } /** diff --git a/java/src/main/java/com/google/protobuf/CodedOutputStream.java b/java/src/main/java/com/google/protobuf/CodedOutputStream.java index 291bd20a..d8ebad21 100644 --- a/java/src/main/java/com/google/protobuf/CodedOutputStream.java +++ b/java/src/main/java/com/google/protobuf/CodedOutputStream.java @@ -53,7 +53,7 @@ import java.util.logging.Logger; * @author kneton@google.com Kenton Varda */ public final class CodedOutputStream { - + private static final Logger logger = Logger.getLogger(CodedOutputStream.class.getName()); // TODO(dweis): Consider migrating to a ByteBuffer. @@ -243,19 +243,6 @@ public final class CodedOutputStream { } - /** - * Write a group represented by an {@link UnknownFieldSet}. - * - * @deprecated UnknownFieldSet now implements MessageLite, so you can just - * call {@link #writeGroup}. - */ - @Deprecated - public void writeUnknownGroup(final int fieldNumber, - final MessageLite value) - throws IOException { - writeGroup(fieldNumber, value); - } - /** Write an embedded message field, including tag, to the stream. */ public void writeMessage(final int fieldNumber, final MessageLite value) throws IOException { @@ -428,7 +415,7 @@ public final class CodedOutputStream { try { efficientWriteStringNoTag(value); } catch (UnpairedSurrogateException e) { - logger.log(Level.WARNING, + logger.log(Level.WARNING, "Converting ill-formed UTF-16. Your Protocol Buffer will not round trip correctly!", e); inefficientWriteStringNoTag(value); } @@ -449,10 +436,10 @@ public final class CodedOutputStream { * Write a {@code string} field to the stream efficiently. If the {@code string} is malformed, * this method rolls back its changes and throws an {@link UnpairedSurrogateException} with the * intent that the caller will catch and retry with {@link #inefficientWriteStringNoTag(String)}. - * + * * @param value the string to write to the stream - * - * @throws UnpairedSurrogateException when {@code value} is ill-formed UTF-16. + * + * @throws UnpairedSurrogateException when {@code value} is ill-formed UTF-16. */ private void efficientWriteStringNoTag(final String value) throws IOException { // UTF-8 byte length of the string is at least its UTF-16 code unit length (value.length()), @@ -510,18 +497,6 @@ public final class CodedOutputStream { } - /** - * Write a group represented by an {@link UnknownFieldSet}. - * - * @deprecated UnknownFieldSet now implements MessageLite, so you can just - * call {@link #writeGroupNoTag}. - */ - @Deprecated - public void writeUnknownGroupNoTag(final MessageLite value) - throws IOException { - writeGroupNoTag(value); - } - /** Write an embedded message field to the stream. */ public void writeMessageNoTag(final MessageLite value) throws IOException { writeRawVarint32(value.getSerializedSize()); @@ -685,20 +660,6 @@ public final class CodedOutputStream { } /** - * Compute the number of bytes that would be needed to encode a - * {@code group} field represented by an {@code UnknownFieldSet}, including - * tag. - * - * @deprecated UnknownFieldSet now implements MessageLite, so you can just - * call {@link #computeGroupSize}. - */ - @Deprecated - public static int computeUnknownGroupSize(final int fieldNumber, - final MessageLite value) { - return computeGroupSize(fieldNumber, value); - } - - /** * Compute the number of bytes that would be needed to encode an * embedded message field, including tag. */ @@ -927,19 +888,6 @@ public final class CodedOutputStream { } /** - * Compute the number of bytes that would be needed to encode a - * {@code group} field represented by an {@code UnknownFieldSet}, including - * tag. - * - * @deprecated UnknownFieldSet now implements MessageLite, so you can just - * call {@link #computeUnknownGroupSizeNoTag}. - */ - @Deprecated - public static int computeUnknownGroupSizeNoTag(final MessageLite value) { - return computeGroupSizeNoTag(value); - } - - /** * Compute the number of bytes that would be needed to encode an embedded * message field. */ @@ -1295,10 +1243,10 @@ public final class CodedOutputStream { * negative. */ public static int computeRawVarint32Size(final int value) { - if ((value & (0xffffffff << 7)) == 0) return 1; - if ((value & (0xffffffff << 14)) == 0) return 2; - if ((value & (0xffffffff << 21)) == 0) return 3; - if ((value & (0xffffffff << 28)) == 0) return 4; + if ((value & (~0 << 7)) == 0) return 1; + if ((value & (~0 << 14)) == 0) return 2; + if ((value & (~0 << 21)) == 0) return 3; + if ((value & (~0 << 28)) == 0) return 4; return 5; } @@ -1316,17 +1264,16 @@ public final class CodedOutputStream { } /** Compute the number of bytes that would be needed to encode a varint. */ - public static int computeRawVarint64Size(final long value) { - if ((value & (0xffffffffffffffffL << 7)) == 0) return 1; - if ((value & (0xffffffffffffffffL << 14)) == 0) return 2; - if ((value & (0xffffffffffffffffL << 21)) == 0) return 3; - if ((value & (0xffffffffffffffffL << 28)) == 0) return 4; - if ((value & (0xffffffffffffffffL << 35)) == 0) return 5; - if ((value & (0xffffffffffffffffL << 42)) == 0) return 6; - if ((value & (0xffffffffffffffffL << 49)) == 0) return 7; - if ((value & (0xffffffffffffffffL << 56)) == 0) return 8; - if ((value & (0xffffffffffffffffL << 63)) == 0) return 9; - return 10; + public static int computeRawVarint64Size(long value) { + // handle two popular special cases up front ... + if ((value & (~0L << 7)) == 0L) return 1; + if (value < 0L) return 10; + // ... leaving us with 8 remaining, which we can divide and conquer + int n = 2; + if ((value & (~0L << 35)) != 0L) { n += 4; value >>>= 28; } + if ((value & (~0L << 21)) != 0L) { n += 2; value >>>= 14; } + if ((value & (~0L << 14)) != 0L) { n += 1; } + return n; } /** Write a little-endian 32-bit integer. */ diff --git a/java/src/main/java/com/google/protobuf/Descriptors.java b/java/src/main/java/com/google/protobuf/Descriptors.java index 7cfc47f7..5e15cfbe 100644 --- a/java/src/main/java/com/google/protobuf/Descriptors.java +++ b/java/src/main/java/com/google/protobuf/Descriptors.java @@ -889,6 +889,11 @@ public final class Descriptors { */ public String getFullName() { return fullName; } + /** Get the JSON name of this field. */ + public String getJsonName() { + return jsonName; + } + /** * Get the field's java type. This is just for convenience. Every * {@code FieldDescriptorProto.Type} maps to exactly one Java type. @@ -1079,6 +1084,7 @@ public final class Descriptors { private FieldDescriptorProto proto; private final String fullName; + private final String jsonName; private final FileDescriptor file; private final Descriptor extensionScope; @@ -1157,6 +1163,38 @@ public final class Descriptors { private final Object defaultDefault; } + // TODO(xiaofeng): Implement it consistently across different languages. See b/24751348. + private static String fieldNameToLowerCamelCase(String name) { + StringBuilder result = new StringBuilder(name.length()); + boolean isNextUpperCase = false; + for (int i = 0; i < name.length(); i++) { + Character ch = name.charAt(i); + if (Character.isLowerCase(ch)) { + if (isNextUpperCase) { + result.append(Character.toUpperCase(ch)); + } else { + result.append(ch); + } + isNextUpperCase = false; + } else if (Character.isUpperCase(ch)) { + if (i == 0) { + // Force first letter to lower-case. + result.append(Character.toLowerCase(ch)); + } else { + // Capital letters after the first are left as-is. + result.append(ch); + } + isNextUpperCase = false; + } else if (Character.isDigit(ch)) { + result.append(ch); + isNextUpperCase = false; + } else { + isNextUpperCase = true; + } + } + return result.toString(); + } + private FieldDescriptor(final FieldDescriptorProto proto, final FileDescriptor file, final Descriptor parent, @@ -1167,6 +1205,11 @@ public final class Descriptors { this.proto = proto; fullName = computeFullName(file, parent, proto.getName()); this.file = file; + if (proto.hasJsonName()) { + jsonName = proto.getJsonName(); + } else { + jsonName = fieldNameToLowerCamelCase(proto.getName()); + } if (proto.hasType()) { type = Type.valueOf(proto.getType()); diff --git a/java/src/main/java/com/google/protobuf/DoubleArrayList.java b/java/src/main/java/com/google/protobuf/DoubleArrayList.java index 90ebe109..bcc9d6ee 100644 --- a/java/src/main/java/com/google/protobuf/DoubleArrayList.java +++ b/java/src/main/java/com/google/protobuf/DoubleArrayList.java @@ -68,10 +68,17 @@ final class DoubleArrayList private int size; /** - * Constructs a new mutable {@code DoubleArrayList}. + * Constructs a new mutable {@code DoubleArrayList} with default capacity. */ DoubleArrayList() { - array = new double[DEFAULT_CAPACITY]; + this(DEFAULT_CAPACITY); + } + + /** + * Constructs a new mutable {@code DoubleArrayList} with the provided capacity. + */ + DoubleArrayList(int capacity) { + array = new double[capacity]; size = 0; } diff --git a/java/src/main/java/com/google/protobuf/FloatArrayList.java b/java/src/main/java/com/google/protobuf/FloatArrayList.java index 293eaff6..033b5eed 100644 --- a/java/src/main/java/com/google/protobuf/FloatArrayList.java +++ b/java/src/main/java/com/google/protobuf/FloatArrayList.java @@ -67,10 +67,17 @@ final class FloatArrayList extends AbstractProtobufList<Float> implements FloatL private int size; /** - * Constructs a new mutable {@code FloatArrayList}. + * Constructs a new mutable {@code FloatArrayList} with default capacity. */ FloatArrayList() { - array = new float[DEFAULT_CAPACITY]; + this(DEFAULT_CAPACITY); + } + + /** + * Constructs a new mutable {@code FloatArrayList} with the provided capacity. + */ + FloatArrayList(int capacity) { + array = new float[capacity]; size = 0; } diff --git a/java/src/main/java/com/google/protobuf/GeneratedMessageLite.java b/java/src/main/java/com/google/protobuf/GeneratedMessageLite.java index 4316efee..81e1862c 100644 --- a/java/src/main/java/com/google/protobuf/GeneratedMessageLite.java +++ b/java/src/main/java/com/google/protobuf/GeneratedMessageLite.java @@ -102,6 +102,11 @@ public abstract class GeneratedMessageLite< * @return {@code true} unless the tag is an end-group tag. */ protected boolean parseUnknownField(int tag, CodedInputStream input) throws IOException { + // This will avoid the allocation of unknown fields when a group tag is encountered. + if (WireFormat.getTagWireType(tag) == WireFormat.WIRETYPE_END_GROUP) { + return false; + } + ensureUnknownFieldsInitialized(); return unknownFields.mergeFieldFrom(tag, input); } @@ -1173,6 +1178,10 @@ public abstract class GeneratedMessageLite< return new IntArrayList(); } + protected static IntList newIntListWithCapacity(int capacity) { + return new IntArrayList(capacity); + } + protected static IntList newIntList(List<Integer> toCopy) { return new IntArrayList(toCopy); } @@ -1180,10 +1189,14 @@ public abstract class GeneratedMessageLite< protected static IntList emptyIntList() { return IntArrayList.emptyList(); } - + protected static LongList newLongList() { return new LongArrayList(); } + + protected static LongList newLongListWithCapacity(int capacity) { + return new LongArrayList(capacity); + } protected static LongList newLongList(List<Long> toCopy) { return new LongArrayList(toCopy); @@ -1197,6 +1210,10 @@ public abstract class GeneratedMessageLite< return new FloatArrayList(); } + protected static FloatList newFloatListWithCapacity(int capacity) { + return new FloatArrayList(capacity); + } + protected static FloatList newFloatList(List<Float> toCopy) { return new FloatArrayList(toCopy); } @@ -1209,6 +1226,10 @@ public abstract class GeneratedMessageLite< return new DoubleArrayList(); } + protected static DoubleList newDoubleListWithCapacity(int capacity) { + return new DoubleArrayList(capacity); + } + protected static DoubleList newDoubleList(List<Double> toCopy) { return new DoubleArrayList(toCopy); } @@ -1221,6 +1242,10 @@ public abstract class GeneratedMessageLite< return new BooleanArrayList(); } + protected static BooleanList newBooleanListWithCapacity(int capacity) { + return new BooleanArrayList(capacity); + } + protected static BooleanList newBooleanList(List<Boolean> toCopy) { return new BooleanArrayList(toCopy); } @@ -1237,6 +1262,10 @@ public abstract class GeneratedMessageLite< return new ProtobufArrayList<E>(toCopy); } + protected static <E> ProtobufList<E> newProtobufListWithCapacity(int capacity) { + return new ProtobufArrayList<E>(capacity); + } + protected static <E> ProtobufList<E> emptyProtobufList() { return ProtobufArrayList.emptyList(); } diff --git a/java/src/main/java/com/google/protobuf/IntArrayList.java b/java/src/main/java/com/google/protobuf/IntArrayList.java index f7609cc9..f4e68ed8 100644 --- a/java/src/main/java/com/google/protobuf/IntArrayList.java +++ b/java/src/main/java/com/google/protobuf/IntArrayList.java @@ -67,10 +67,17 @@ final class IntArrayList extends AbstractProtobufList<Integer> implements IntLis private int size; /** - * Constructs a new mutable {@code IntArrayList}. + * Constructs a new mutable {@code IntArrayList} with default capacity. */ IntArrayList() { - array = new int[DEFAULT_CAPACITY]; + this(DEFAULT_CAPACITY); + } + + /** + * Constructs a new mutable {@code IntArrayList} with the provided capacity. + */ + IntArrayList(int capacity) { + array = new int[capacity]; size = 0; } diff --git a/java/src/main/java/com/google/protobuf/LiteralByteString.java b/java/src/main/java/com/google/protobuf/LiteralByteString.java index c5a8512a..a18c2792 100644 --- a/java/src/main/java/com/google/protobuf/LiteralByteString.java +++ b/java/src/main/java/com/google/protobuf/LiteralByteString.java @@ -36,9 +36,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.charset.Charset; -import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import java.util.NoSuchElementException; /** * This class implements a {@link com.google.protobuf.ByteString} backed by a @@ -49,8 +48,7 @@ import java.util.NoSuchElementException; * * @author carlanton@google.com (Carl Haverl) */ -class LiteralByteString extends ByteString { - +class LiteralByteString extends ByteString.LeafByteString { private static final long serialVersionUID = 1L; protected final byte[] bytes; @@ -82,77 +80,56 @@ class LiteralByteString extends ByteString { // ByteString -> substring @Override - public ByteString substring(int beginIndex, int endIndex) { - if (beginIndex < 0) { - throw new IndexOutOfBoundsException( - "Beginning index: " + beginIndex + " < 0"); - } - if (endIndex > size()) { - throw new IndexOutOfBoundsException("End index: " + endIndex + " > " + - size()); - } - int substringLength = endIndex - beginIndex; - if (substringLength < 0) { - throw new IndexOutOfBoundsException( - "Beginning index larger than ending index: " + beginIndex + ", " - + endIndex); - } + public final ByteString substring(int beginIndex, int endIndex) { + final int length = checkRange(beginIndex, endIndex, size()); - ByteString result; - if (substringLength == 0) { - result = ByteString.EMPTY; - } else { - result = new BoundedByteString(bytes, getOffsetIntoBytes() + beginIndex, - substringLength); + if (length == 0) { + return ByteString.EMPTY; } - return result; + + return new BoundedByteString(bytes, getOffsetIntoBytes() + beginIndex, length); } // ================================================================= // ByteString -> byte[] @Override - protected void copyToInternal(byte[] target, int sourceOffset, - int targetOffset, int numberToCopy) { + protected void copyToInternal( + byte[] target, int sourceOffset, int targetOffset, int numberToCopy) { // Optimized form, not for subclasses, since we don't call // getOffsetIntoBytes() or check the 'numberToCopy' parameter. + // TODO(nathanmittler): Is not calling getOffsetIntoBytes really saving that much? System.arraycopy(bytes, sourceOffset, target, targetOffset, numberToCopy); } @Override - public void copyTo(ByteBuffer target) { - target.put(bytes, getOffsetIntoBytes(), size()); // Copies bytes + public final void copyTo(ByteBuffer target) { + target.put(bytes, getOffsetIntoBytes(), size()); // Copies bytes + } + + @Override + public final ByteBuffer asReadOnlyByteBuffer() { + return ByteBuffer.wrap(bytes, getOffsetIntoBytes(), size()).asReadOnlyBuffer(); } @Override - public ByteBuffer asReadOnlyByteBuffer() { - ByteBuffer byteBuffer = - ByteBuffer.wrap(bytes, getOffsetIntoBytes(), size()); - return byteBuffer.asReadOnlyBuffer(); + public final List<ByteBuffer> asReadOnlyByteBufferList() { + return Collections.singletonList(asReadOnlyByteBuffer()); } @Override - public List<ByteBuffer> asReadOnlyByteBufferList() { - // Return the ByteBuffer generated by asReadOnlyByteBuffer() as a singleton - List<ByteBuffer> result = new ArrayList<ByteBuffer>(1); - result.add(asReadOnlyByteBuffer()); - return result; - } - - @Override - public void writeTo(OutputStream outputStream) throws IOException { + public final void writeTo(OutputStream outputStream) throws IOException { outputStream.write(toByteArray()); } @Override - void writeToInternal(OutputStream outputStream, int sourceOffset, - int numberToWrite) throws IOException { - outputStream.write(bytes, getOffsetIntoBytes() + sourceOffset, - numberToWrite); + final void writeToInternal(OutputStream outputStream, int sourceOffset, int numberToWrite) + throws IOException { + outputStream.write(bytes, getOffsetIntoBytes() + sourceOffset, numberToWrite); } @Override - protected String toStringInternal(Charset charset) { + protected final String toStringInternal(Charset charset) { return new String(bytes, getOffsetIntoBytes(), size(), charset); } @@ -160,13 +137,13 @@ class LiteralByteString extends ByteString { // UTF-8 decoding @Override - public boolean isValidUtf8() { + public final boolean isValidUtf8() { int offset = getOffsetIntoBytes(); return Utf8.isValidUtf8(bytes, offset, offset + size()); } @Override - protected int partialIsValidUtf8(int state, int offset, int length) { + protected final int partialIsValidUtf8(int state, int offset, int length) { int index = getOffsetIntoBytes() + offset; return Utf8.partialIsValidUtf8(state, bytes, index, index + length); } @@ -175,7 +152,7 @@ class LiteralByteString extends ByteString { // equals() and hashCode() @Override - public boolean equals(Object other) { + public final boolean equals(Object other) { if (other == this) { return true; } @@ -194,19 +171,16 @@ class LiteralByteString extends ByteString { LiteralByteString otherAsLiteral = (LiteralByteString) other; // If we know the hash codes and they are not equal, we know the byte // strings are not equal. - if (hash != 0 - && otherAsLiteral.hash != 0 - && hash != otherAsLiteral.hash) { + int thisHash = peekCachedHashCode(); + int thatHash = otherAsLiteral.peekCachedHashCode(); + if (thisHash != 0 && thatHash != 0 && thisHash != thatHash) { return false; } return equalsRange((LiteralByteString) other, 0, size()); - } else if (other instanceof RopeByteString) { - return other.equals(this); } else { - throw new IllegalArgumentException( - "Has a new type of ByteString been created? Found " - + other.getClass()); + // RopeByteString and NioByteString. + return other.equals(this); } } @@ -219,65 +193,36 @@ class LiteralByteString extends ByteString { * @param length number of bytes to compare * @return true for equality of substrings, else false. */ - boolean equalsRange(LiteralByteString other, int offset, int length) { + @Override + final boolean equalsRange(ByteString other, int offset, int length) { if (length > other.size()) { - throw new IllegalArgumentException( - "Length too large: " + length + size()); + throw new IllegalArgumentException("Length too large: " + length + size()); } if (offset + length > other.size()) { throw new IllegalArgumentException( - "Ran off end of other: " + offset + ", " + length + ", " + - other.size()); + "Ran off end of other: " + offset + ", " + length + ", " + other.size()); } - byte[] thisBytes = bytes; - byte[] otherBytes = other.bytes; - int thisLimit = getOffsetIntoBytes() + length; - for (int thisIndex = getOffsetIntoBytes(), otherIndex = - other.getOffsetIntoBytes() + offset; - (thisIndex < thisLimit); ++thisIndex, ++otherIndex) { - if (thisBytes[thisIndex] != otherBytes[otherIndex]) { - return false; - } - } - return true; - } - - /** - * Cached hash value. Intentionally accessed via a data race, which - * is safe because of the Java Memory Model's "no out-of-thin-air values" - * guarantees for ints. - */ - private int hash = 0; - - /** - * Compute the hashCode using the traditional algorithm from {@link - * ByteString}. - * - * @return hashCode value - */ - @Override - public int hashCode() { - int h = hash; - - if (h == 0) { - int size = size(); - h = partialHash(size, 0, size); - if (h == 0) { - h = 1; + if (other instanceof LiteralByteString) { + LiteralByteString lbsOther = (LiteralByteString) other; + byte[] thisBytes = bytes; + byte[] otherBytes = lbsOther.bytes; + int thisLimit = getOffsetIntoBytes() + length; + for ( + int thisIndex = getOffsetIntoBytes(), otherIndex = lbsOther.getOffsetIntoBytes() + offset; + (thisIndex < thisLimit); ++thisIndex, ++otherIndex) { + if (thisBytes[thisIndex] != otherBytes[otherIndex]) { + return false; + } } - hash = h; + return true; } - return h; - } - @Override - protected int peekCachedHashCode() { - return hash; + return other.substring(offset, offset + length).equals(substring(0, length)); } @Override - protected int partialHash(int h, int offset, int length) { + protected final int partialHash(int h, int offset, int length) { return hashCode(h, bytes, getOffsetIntoBytes() + offset, length); } @@ -297,70 +242,20 @@ class LiteralByteString extends ByteString { // Input stream @Override - public InputStream newInput() { - return new ByteArrayInputStream(bytes, getOffsetIntoBytes(), - size()); // No copy + public final InputStream newInput() { + return new ByteArrayInputStream(bytes, getOffsetIntoBytes(), size()); // No copy } @Override - public CodedInputStream newCodedInput() { + public final CodedInputStream newCodedInput() { // We trust CodedInputStream not to modify the bytes, or to give anyone // else access to them. return CodedInputStream.newInstance(this); } // ================================================================= - // ByteIterator - - @Override - public ByteIterator iterator() { - return new LiteralByteIterator(); - } - - private class LiteralByteIterator implements ByteIterator { - private int position; - private final int limit; - - private LiteralByteIterator() { - position = 0; - limit = size(); - } - - public boolean hasNext() { - return (position < limit); - } - - public Byte next() { - // Boxing calls Byte.valueOf(byte), which does not instantiate. - return nextByte(); - } - - public byte nextByte() { - try { - return bytes[position++]; - } catch (ArrayIndexOutOfBoundsException e) { - throw new NoSuchElementException(e.getMessage()); - } - } - - public void remove() { - throw new UnsupportedOperationException(); - } - } - - // ================================================================= // Internal methods - @Override - protected int getTreeDepth() { - return 0; - } - - @Override - protected boolean isBalanced() { - return true; - } - /** * Offset into {@code bytes[]} to use, non-zero for substrings. * diff --git a/java/src/main/java/com/google/protobuf/LongArrayList.java b/java/src/main/java/com/google/protobuf/LongArrayList.java index 298617ff..ebe62029 100644 --- a/java/src/main/java/com/google/protobuf/LongArrayList.java +++ b/java/src/main/java/com/google/protobuf/LongArrayList.java @@ -67,10 +67,17 @@ final class LongArrayList extends AbstractProtobufList<Long> implements LongList private int size; /** - * Constructs a new mutable {@code LongArrayList}. + * Constructs a new mutable {@code LongArrayList} with default capacity. */ LongArrayList() { - array = new long[DEFAULT_CAPACITY]; + this(DEFAULT_CAPACITY); + } + + /** + * Constructs a new mutable {@code LongArrayList} with the provided capacity. + */ + LongArrayList(int capacity) { + array = new long[capacity]; size = 0; } diff --git a/java/src/main/java/com/google/protobuf/MapFieldLite.java b/java/src/main/java/com/google/protobuf/MapFieldLite.java index c17fa7b1..16d3e6d2 100644 --- a/java/src/main/java/com/google/protobuf/MapFieldLite.java +++ b/java/src/main/java/com/google/protobuf/MapFieldLite.java @@ -30,6 +30,8 @@ package com.google.protobuf; +import com.google.protobuf.Internal.EnumLite; + import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -44,7 +46,7 @@ import java.util.Set; * This class is a protobuf implementation detail. Users shouldn't use this * class directly. */ -public class MapFieldLite<K, V> implements MutabilityOracle { +public final class MapFieldLite<K, V> implements MutabilityOracle { private MutatabilityAwareMap<K, V> mapData; private boolean isMutable; @@ -136,8 +138,9 @@ public class MapFieldLite<K, V> implements MutabilityOracle { if (a instanceof byte[]) { return LiteralByteString.hashCode((byte[]) a); } - if (a instanceof Internal.EnumLite) { - return Internal.hashEnum((Internal.EnumLite) a); + // Enums should be stored as integers internally. + if (a instanceof EnumLite) { + throw new UnsupportedOperationException(); } return a.hashCode(); } diff --git a/java/src/main/java/com/google/protobuf/MessageLiteToString.java b/java/src/main/java/com/google/protobuf/MessageLiteToString.java new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/java/src/main/java/com/google/protobuf/MessageLiteToString.java diff --git a/java/src/main/java/com/google/protobuf/NioByteString.java b/java/src/main/java/com/google/protobuf/NioByteString.java new file mode 100644 index 00000000..f71e41b2 --- /dev/null +++ b/java/src/main/java/com/google/protobuf/NioByteString.java @@ -0,0 +1,309 @@ +// 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 java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InvalidObjectException; +import java.io.ObjectInputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.InvalidMarkException; +import java.nio.channels.Channels; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.List; + +/** + * A {@link ByteString} that wraps around a {@link ByteBuffer}. + */ +final class NioByteString extends ByteString.LeafByteString { + private final ByteBuffer buffer; + + NioByteString(ByteBuffer buffer) { + if (buffer == null) { + throw new NullPointerException("buffer"); + } + + this.buffer = buffer.slice(); + } + + // ================================================================= + // Serializable + + /** + * Magic method that lets us override serialization behavior. + */ + private Object writeReplace() { + return ByteString.copyFrom(buffer.slice()); + } + + /** + * Magic method that lets us override deserialization behavior. + */ + private void readObject(@SuppressWarnings("unused") ObjectInputStream in) throws IOException { + throw new InvalidObjectException("NioByteString instances are not to be serialized directly"); + } + + // ================================================================= + + @Override + public byte byteAt(int index) { + try { + return buffer.get(index); + } catch (ArrayIndexOutOfBoundsException e) { + throw e; + } catch (IndexOutOfBoundsException e) { + throw new ArrayIndexOutOfBoundsException(e.getMessage()); + } + } + + @Override + public int size() { + return buffer.remaining(); + } + + @Override + public ByteString substring(int beginIndex, int endIndex) { + try { + ByteBuffer slice = slice(beginIndex, endIndex); + return new NioByteString(slice); + } catch (ArrayIndexOutOfBoundsException e) { + throw e; + } catch (IndexOutOfBoundsException e) { + throw new ArrayIndexOutOfBoundsException(e.getMessage()); + } + } + + @Override + protected void copyToInternal( + byte[] target, int sourceOffset, int targetOffset, int numberToCopy) { + ByteBuffer slice = buffer.slice(); + slice.position(sourceOffset); + slice.get(target, targetOffset, numberToCopy); + } + + @Override + public void copyTo(ByteBuffer target) { + target.put(buffer.slice()); + } + + @Override + public void writeTo(OutputStream out) throws IOException { + writeToInternal(out, buffer.position(), buffer.remaining()); + } + + @Override + boolean equalsRange(ByteString other, int offset, int length) { + return substring(0, length).equals(other.substring(offset, offset + length)); + } + + @Override + void writeToInternal(OutputStream out, int sourceOffset, int numberToWrite) throws IOException { + if (buffer.hasArray()) { + // Optimized write for array-backed buffers. + // Note that we're taking the risk that a malicious OutputStream could modify the array. + int bufferOffset = buffer.arrayOffset() + buffer.position() + sourceOffset; + out.write(buffer.array(), bufferOffset, numberToWrite); + return; + } + + // Slow path + if (out instanceof FileOutputStream || numberToWrite >= 8192) { + // Use a channel to write out the ByteBuffer. + Channels.newChannel(out).write(slice(sourceOffset, sourceOffset + numberToWrite)); + } else { + // Just copy the data to an array and write it. + out.write(toByteArray()); + } + } + + @Override + public ByteBuffer asReadOnlyByteBuffer() { + return buffer.asReadOnlyBuffer(); + } + + @Override + public List<ByteBuffer> asReadOnlyByteBufferList() { + return Collections.singletonList(asReadOnlyByteBuffer()); + } + + @Override + protected String toStringInternal(Charset charset) { + byte[] bytes; + int offset; + if (buffer.hasArray()) { + bytes = buffer.array(); + offset = buffer.arrayOffset() + buffer.position(); + } else { + bytes = toByteArray(); + offset = 0; + } + return new String(bytes, offset, size(), charset); + } + + @Override + public boolean isValidUtf8() { + // TODO(nathanmittler): add a ByteBuffer fork for Utf8.isValidUtf8 to avoid the copy + byte[] bytes; + int startIndex; + if (buffer.hasArray()) { + bytes = buffer.array(); + startIndex = buffer.arrayOffset() + buffer.position(); + } else { + bytes = toByteArray(); + startIndex = 0; + } + return Utf8.isValidUtf8(bytes, startIndex, startIndex + size()); + } + + @Override + protected int partialIsValidUtf8(int state, int offset, int length) { + // TODO(nathanmittler): TODO add a ByteBuffer fork for Utf8.partialIsValidUtf8 to avoid the copy + byte[] bytes; + int startIndex; + if (buffer.hasArray()) { + bytes = buffer.array(); + startIndex = buffer.arrayOffset() + buffer.position(); + } else { + bytes = toByteArray(); + startIndex = 0; + } + return Utf8.partialIsValidUtf8(state, bytes, startIndex, startIndex + size()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof ByteString)) { + return false; + } + ByteString otherString = ((ByteString) other); + if (size() != otherString.size()) { + return false; + } + if (size() == 0) { + return true; + } + if (other instanceof NioByteString) { + return buffer.equals(((NioByteString) other).buffer); + } + if (other instanceof RopeByteString) { + return other.equals(this); + } + return buffer.equals(otherString.asReadOnlyByteBuffer()); + } + + @Override + protected int partialHash(int h, int offset, int length) { + for (int i = offset; i < offset + length; i++) { + h = h * 31 + buffer.get(i); + } + return h; + } + + @Override + public InputStream newInput() { + return new InputStream() { + private final ByteBuffer buf = buffer.slice(); + + @Override + public void mark(int readlimit) { + buf.mark(); + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public void reset() throws IOException { + try { + buf.reset(); + } catch (InvalidMarkException e) { + throw new IOException(e); + } + } + + @Override + public int available() throws IOException { + return buf.remaining(); + } + + @Override + public int read() throws IOException { + if (!buf.hasRemaining()) { + return -1; + } + return buf.get() & 0xFF; + } + + @Override + public int read(byte[] bytes, int off, int len) throws IOException { + if (!buf.hasRemaining()) { + return -1; + } + + len = Math.min(len, buf.remaining()); + buf.get(bytes, off, len); + return len; + } + }; + } + + @Override + public CodedInputStream newCodedInput() { + return CodedInputStream.newInstance(buffer); + } + + /** + * Creates a slice of a range of this buffer. + * + * @param beginIndex the beginning index of the slice (inclusive). + * @param endIndex the end index of the slice (exclusive). + * @return the requested slice. + */ + private ByteBuffer slice(int beginIndex, int endIndex) { + if (beginIndex < buffer.position() || endIndex > buffer.limit() || beginIndex > endIndex) { + throw new IllegalArgumentException( + String.format("Invalid indices [%d, %d]", beginIndex, endIndex)); + } + + ByteBuffer slice = buffer.slice(); + slice.position(beginIndex - buffer.position()); + slice.limit(endIndex - buffer.position()); + return slice; + } +} diff --git a/java/src/main/java/com/google/protobuf/ProtobufArrayList.java b/java/src/main/java/com/google/protobuf/ProtobufArrayList.java index 759368c9..d2f82ac5 100644 --- a/java/src/main/java/com/google/protobuf/ProtobufArrayList.java +++ b/java/src/main/java/com/google/protobuf/ProtobufArrayList.java @@ -60,6 +60,10 @@ class ProtobufArrayList<E> extends AbstractProtobufList<E> { list = new ArrayList<E>(toCopy); } + ProtobufArrayList(int capacity) { + list = new ArrayList<E>(capacity); + } + @Override public void add(int index, E element) { ensureIsMutable(); diff --git a/java/src/main/java/com/google/protobuf/RopeByteString.java b/java/src/main/java/com/google/protobuf/RopeByteString.java index 2c332624..6e8eb724 100644 --- a/java/src/main/java/com/google/protobuf/RopeByteString.java +++ b/java/src/main/java/com/google/protobuf/RopeByteString.java @@ -69,7 +69,7 @@ import java.util.Stack; * * @author carlanton@google.com (Carl Haverl) */ -class RopeByteString extends ByteString { +final class RopeByteString extends ByteString { /** * BAP95. Let Fn be the nth Fibonacci number. A {@link RopeByteString} of @@ -151,21 +151,24 @@ class RopeByteString extends ByteString { * @return concatenation representing the same sequence as the given strings */ static ByteString concatenate(ByteString left, ByteString right) { - ByteString result; - RopeByteString leftRope = - (left instanceof RopeByteString) ? (RopeByteString) left : null; if (right.size() == 0) { - result = left; - } else if (left.size() == 0) { - result = right; - } else { - int newLength = left.size() + right.size(); - if (newLength < ByteString.CONCATENATE_BY_COPY_SIZE) { - // Optimization from BAP95: For short (leaves in paper, but just short - // here) total length, do a copy of data to a new leaf. - result = concatenateBytes(left, right); - } else if (leftRope != null - && leftRope.right.size() + right.size() < CONCATENATE_BY_COPY_SIZE) { + return left; + } + + if (left.size() == 0) { + return right; + } + + final int newLength = left.size() + right.size(); + if (newLength < ByteString.CONCATENATE_BY_COPY_SIZE) { + // Optimization from BAP95: For short (leaves in paper, but just short + // here) total length, do a copy of data to a new leaf. + return concatenateBytes(left, right); + } + + if (left instanceof RopeByteString) { + final RopeByteString leftRope = (RopeByteString) left; + if (leftRope.right.size() + right.size() < CONCATENATE_BY_COPY_SIZE) { // Optimization from BAP95: As an optimization of the case where the // ByteString is constructed by repeated concatenate, recognize the case // where a short string is concatenated to a left-hand node whose @@ -177,9 +180,10 @@ class RopeByteString extends ByteString { // new parent node so that the depth of the result is the same as the // given left tree. ByteString newRight = concatenateBytes(leftRope.right, right); - result = new RopeByteString(leftRope.left, newRight); - } else if (leftRope != null - && leftRope.left.getTreeDepth() > leftRope.right.getTreeDepth() + return new RopeByteString(leftRope.left, newRight); + } + + if (leftRope.left.getTreeDepth() > leftRope.right.getTreeDepth() && leftRope.getTreeDepth() > right.getTreeDepth()) { // Typically for concatenate-built strings the left-side is deeper than // the right. This is our final attempt to concatenate without @@ -187,20 +191,18 @@ class RopeByteString extends ByteString { // is yet another optimization for building the string by repeatedly // concatenating on the right. ByteString newRight = new RopeByteString(leftRope.right, right); - result = new RopeByteString(leftRope.left, newRight); - } else { - // Fine, we'll add a node and increase the tree depth--unless we - // rebalance ;^) - int newDepth = Math.max(left.getTreeDepth(), right.getTreeDepth()) + 1; - if (newLength >= minLengthByDepth[newDepth]) { - // The tree is shallow enough, so don't rebalance - result = new RopeByteString(left, right); - } else { - result = new Balancer().balance(left, right); - } + return new RopeByteString(leftRope.left, newRight); } } - return result; + + // Fine, we'll add a node and increase the tree depth--unless we rebalance ;^) + int newDepth = Math.max(left.getTreeDepth(), right.getTreeDepth()) + 1; + if (newLength >= minLengthByDepth[newDepth]) { + // The tree is shallow enough, so don't rebalance + return new RopeByteString(left, right); + } + + return new Balancer().balance(left, right); } /** @@ -248,22 +250,14 @@ class RopeByteString extends ByteString { */ @Override public byte byteAt(int index) { - if (index < 0) { - throw new ArrayIndexOutOfBoundsException("Index < 0: " + index); - } - if (index > totalLength) { - throw new ArrayIndexOutOfBoundsException( - "Index > length: " + index + ", " + totalLength); - } + checkIndex(index, totalLength); - byte result; // Find the relevant piece by recursive descent if (index < leftLength) { - result = left.byteAt(index); - } else { - result = right.byteAt(index - leftLength); + return left.byteAt(index); } - return result; + + return right.byteAt(index - leftLength); } @Override @@ -309,48 +303,36 @@ class RopeByteString extends ByteString { */ @Override public ByteString substring(int beginIndex, int endIndex) { - if (beginIndex < 0) { - throw new IndexOutOfBoundsException( - "Beginning index: " + beginIndex + " < 0"); + final int length = checkRange(beginIndex, endIndex, totalLength); + + if (length == 0) { + // Empty substring + return ByteString.EMPTY; } - if (endIndex > totalLength) { - throw new IndexOutOfBoundsException( - "End index: " + endIndex + " > " + totalLength); + + if (length == totalLength) { + // The whole string + return this; } - int substringLength = endIndex - beginIndex; - if (substringLength < 0) { - throw new IndexOutOfBoundsException( - "Beginning index larger than ending index: " + beginIndex + ", " - + endIndex); + + // Proper substring + if (endIndex <= leftLength) { + // Substring on the left + return left.substring(beginIndex, endIndex); } - ByteString result; - if (substringLength == 0) { - // Empty substring - result = ByteString.EMPTY; - } else if (substringLength == totalLength) { - // The whole string - result = this; - } else { - // Proper substring - if (endIndex <= leftLength) { - // Substring on the left - result = left.substring(beginIndex, endIndex); - } else if (beginIndex >= leftLength) { - // Substring on the right - result = right - .substring(beginIndex - leftLength, endIndex - leftLength); - } else { - // Split substring - ByteString leftSub = left.substring(beginIndex); - ByteString rightSub = right.substring(0, endIndex - leftLength); - // Intentionally not rebalancing, since in many cases these two - // substrings will already be less deep than the top-level - // RopeByteString we're taking a substring of. - result = new RopeByteString(leftSub, rightSub); - } + if (beginIndex >= leftLength) { + // Substring on the right + return right.substring(beginIndex - leftLength, endIndex - leftLength); } - return result; + + // Split substring + ByteString leftSub = left.substring(beginIndex); + ByteString rightSub = right.substring(0, endIndex - leftLength); + // Intentionally not rebalancing, since in many cases these two + // substrings will already be less deep than the top-level + // RopeByteString we're taking a substring of. + return new RopeByteString(leftSub, rightSub); } // ================================================================= @@ -391,7 +373,7 @@ class RopeByteString extends ByteString { List<ByteBuffer> result = new ArrayList<ByteBuffer>(); PieceIterator pieces = new PieceIterator(this); while (pieces.hasNext()) { - LiteralByteString byteString = pieces.next(); + LeafByteString byteString = pieces.next(); result.add(byteString.asReadOnlyByteBuffer()); } return result; @@ -471,11 +453,10 @@ class RopeByteString extends ByteString { // hashCode if it's already computed. It's arguable we should compute the // hashCode here, and if we're going to be testing a bunch of byteStrings, // it might even make sense. - if (hash != 0) { - int cachedOtherHash = otherByteString.peekCachedHashCode(); - if (cachedOtherHash != 0 && hash != cachedOtherHash) { - return false; - } + int thisHash = peekCachedHashCode(); + int thatHash = otherByteString.peekCachedHashCode(); + if (thisHash != 0 && thatHash != 0 && thisHash != thatHash) { + return false; } return equalsFragments(otherByteString); @@ -492,12 +473,12 @@ class RopeByteString extends ByteString { */ private boolean equalsFragments(ByteString other) { int thisOffset = 0; - Iterator<LiteralByteString> thisIter = new PieceIterator(this); - LiteralByteString thisString = thisIter.next(); + Iterator<LeafByteString> thisIter = new PieceIterator(this); + LeafByteString thisString = thisIter.next(); int thatOffset = 0; - Iterator<LiteralByteString> thatIter = new PieceIterator(other); - LiteralByteString thatString = thatIter.next(); + Iterator<LeafByteString> thatIter = new PieceIterator(other); + LeafByteString thatString = thatIter.next(); int pos = 0; while (true) { @@ -536,33 +517,6 @@ class RopeByteString extends ByteString { } } - /** - * Cached hash value. Intentionally accessed via a data race, which is safe - * because of the Java Memory Model's "no out-of-thin-air values" guarantees - * for ints. - */ - private int hash = 0; - - @Override - public int hashCode() { - int h = hash; - - if (h == 0) { - h = totalLength; - h = partialHash(h, 0, totalLength); - if (h == 0) { - h = 1; - } - hash = h; - } - return h; - } - - @Override - protected int peekCachedHashCode() { - return hash; - } - @Override protected int partialHash(int h, int offset, int length) { int toIndex = offset + length; @@ -714,34 +668,34 @@ class RopeByteString extends ByteString { * <p>This iterator is used to implement * {@link RopeByteString#equalsFragments(ByteString)}. */ - private static class PieceIterator implements Iterator<LiteralByteString> { + private static class PieceIterator implements Iterator<LeafByteString> { private final Stack<RopeByteString> breadCrumbs = new Stack<RopeByteString>(); - private LiteralByteString next; + private LeafByteString next; private PieceIterator(ByteString root) { next = getLeafByLeft(root); } - private LiteralByteString getLeafByLeft(ByteString root) { + private LeafByteString getLeafByLeft(ByteString root) { ByteString pos = root; while (pos instanceof RopeByteString) { RopeByteString rbs = (RopeByteString) pos; breadCrumbs.push(rbs); pos = rbs.left; } - return (LiteralByteString) pos; + return (LeafByteString) pos; } - private LiteralByteString getNextNonEmptyLeaf() { + private LeafByteString getNextNonEmptyLeaf() { while (true) { // Almost always, we go through this loop exactly once. However, if // we discover an empty string in the rope, we toss it and try again. if (breadCrumbs.isEmpty()) { return null; } else { - LiteralByteString result = getLeafByLeft(breadCrumbs.pop().right); + LeafByteString result = getLeafByLeft(breadCrumbs.pop().right); if (!result.isEmpty()) { return result; } @@ -749,6 +703,7 @@ class RopeByteString extends ByteString { } } + @Override public boolean hasNext() { return next != null; } @@ -758,15 +713,17 @@ class RopeByteString extends ByteString { * * @return next non-empty LiteralByteString or {@code null} */ - public LiteralByteString next() { + @Override + public LeafByteString next() { if (next == null) { throw new NoSuchElementException(); } - LiteralByteString result = next; + LeafByteString result = next; next = getNextNonEmptyLeaf(); return result; } + @Override public void remove() { throw new UnsupportedOperationException(); } @@ -781,52 +738,11 @@ class RopeByteString extends ByteString { return new LiteralByteString(toByteArray()); } - private void readObject(ObjectInputStream in) throws IOException { + private void readObject(@SuppressWarnings("unused") ObjectInputStream in) throws IOException { throw new InvalidObjectException( "RopeByteStream instances are not to be serialized directly"); } - // ================================================================= - // ByteIterator - - @Override - public ByteIterator iterator() { - return new RopeByteIterator(); - } - - private class RopeByteIterator implements ByteString.ByteIterator { - - private final PieceIterator pieces; - private ByteIterator bytes; - int bytesRemaining; - - private RopeByteIterator() { - pieces = new PieceIterator(RopeByteString.this); - bytes = pieces.next().iterator(); - bytesRemaining = size(); - } - - public boolean hasNext() { - return (bytesRemaining > 0); - } - - public Byte next() { - return nextByte(); // Does not instantiate a Byte - } - - public byte nextByte() { - if (!bytes.hasNext()) { - bytes = pieces.next().iterator(); - } - --bytesRemaining; - return bytes.nextByte(); - } - - public void remove() { - throw new UnsupportedOperationException(); - } - } - /** * This class is the {@link RopeByteString} equivalent for * {@link ByteArrayInputStream}. @@ -835,7 +751,7 @@ class RopeByteString extends ByteString { // Iterates through the pieces of the rope private PieceIterator pieceIterator; // The current piece - private LiteralByteString currentPiece; + private LeafByteString currentPiece; // The size of the current piece private int currentPieceSize; // The index of the next byte to read in the current piece @@ -872,7 +788,7 @@ class RopeByteString extends ByteString { /** * Internal implementation of read and skip. If b != null, then read the * next {@code length} bytes into the buffer {@code b} at - * offset {@code offset}. If b == null, then skip the next {@code length) + * offset {@code offset}. If b == null, then skip the next {@code length} * bytes. * <p> * This method assumes that all error checking has already happened. diff --git a/java/src/main/java/com/google/protobuf/TextFormat.java b/java/src/main/java/com/google/protobuf/TextFormat.java index 44d036c1..c99b5285 100644 --- a/java/src/main/java/com/google/protobuf/TextFormat.java +++ b/java/src/main/java/com/google/protobuf/TextFormat.java @@ -1074,6 +1074,18 @@ public final class TextFormat { private ParseException floatParseException(final NumberFormatException e) { return parseException("Couldn't parse number: " + e.getMessage()); } + + /** + * Returns a {@link UnknownFieldParseException} with the line and column + * numbers of the previous token in the description, and the unknown field + * name, suitable for throwing. + */ + public UnknownFieldParseException unknownFieldParseExceptionPreviousToken( + final String unknownField, final String description) { + // Note: People generally prefer one-based line and column numbers. + return new UnknownFieldParseException( + previousLine + 1, previousColumn + 1, unknownField, description); + } } /** Thrown when parsing an invalid text format message. */ @@ -1121,6 +1133,45 @@ public final class TextFormat { return column; } } + + /** + * Thrown when encountering an unknown field while parsing + * a text format message. + */ + public static class UnknownFieldParseException extends ParseException { + private final String unknownField; + + /** + * Create a new instance, with -1 as the line and column numbers, and an + * empty unknown field name. + */ + public UnknownFieldParseException(final String message) { + this(-1, -1, "", message); + } + + /** + * Create a new instance + * + * @param line the line number where the parse error occurred, + * using 1-offset. + * @param column the column number where the parser error occurred, + * using 1-offset. + * @param unknownField the name of the unknown field found while parsing. + */ + public UnknownFieldParseException(final int line, final int column, + final String unknownField, final String message) { + super(line, column, message); + this.unknownField = unknownField; + } + + /** + * Return the name of the unknown field encountered while parsing the + * protocol buffer string. + */ + public String getUnknownField() { + return unknownField; + } + } private static final Parser PARSER = Parser.newBuilder().build(); @@ -1388,7 +1439,8 @@ public final class TextFormat { if (field == null) { if (!allowUnknownFields) { - throw tokenizer.parseExceptionPreviousToken( + throw tokenizer.unknownFieldParseExceptionPreviousToken( + name, "Message type \"" + type.getFullName() + "\" has no field named \"" + name + "\"."); } else { diff --git a/java/src/main/java/com/google/protobuf/TextFormatEscaper.java b/java/src/main/java/com/google/protobuf/TextFormatEscaper.java new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/java/src/main/java/com/google/protobuf/TextFormatEscaper.java diff --git a/java/src/main/java/com/google/protobuf/UnsafeByteStrings.java b/java/src/main/java/com/google/protobuf/UnsafeByteStrings.java new file mode 100644 index 00000000..c1997515 --- /dev/null +++ b/java/src/main/java/com/google/protobuf/UnsafeByteStrings.java @@ -0,0 +1,55 @@ +// 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 java.nio.ByteBuffer; + +/** + * Provides unsafe factory methods for {@link ByteString} instances. + * + * <p><strong>DISCLAIMER:</strong> The methods in this class should only be called if it is + * guaranteed that the the buffer backing the {@link ByteString} will never change! Mutation of a + * {@link ByteString} can lead to unexpected and undesirable consequences in your application, + * and will likely be difficult to debug. Proceed with caution! + */ +public final class UnsafeByteStrings { + private UnsafeByteStrings() {} + + /** + * An unsafe operation that returns a {@link ByteString} that is backed by the provided buffer. + * + * @param buffer the Java NIO buffer to be wrapped. + * @return a {@link ByteString} backed by the provided buffer. + */ + public static ByteString unsafeWrap(ByteBuffer buffer) { + return new NioByteString(buffer); + } +} diff --git a/java/src/test/java/com/google/protobuf/BooleanArrayListTest.java b/java/src/test/java/com/google/protobuf/BooleanArrayListTest.java index df89c263..b8ad1fe4 100644 --- a/java/src/test/java/com/google/protobuf/BooleanArrayListTest.java +++ b/java/src/test/java/com/google/protobuf/BooleanArrayListTest.java @@ -310,10 +310,6 @@ public class BooleanArrayListTest extends TestCase { } private void assertImmutable(BooleanArrayList list) { - if (list.contains(1)) { - throw new RuntimeException("Cannot test the immutability of lists that contain 1."); - } - try { list.add(false); fail(); @@ -413,7 +409,7 @@ public class BooleanArrayListTest extends TestCase { } try { - list.removeAll(Collections.singleton(1)); + list.removeAll(Collections.singleton(Boolean.TRUE)); fail(); } catch (UnsupportedOperationException e) { // expected @@ -434,7 +430,7 @@ public class BooleanArrayListTest extends TestCase { } try { - list.retainAll(Collections.singleton(1)); + list.retainAll(Collections.singleton(Boolean.TRUE)); fail(); } catch (UnsupportedOperationException e) { // expected diff --git a/java/src/test/java/com/google/protobuf/BoundedByteStringTest.java b/java/src/test/java/com/google/protobuf/BoundedByteStringTest.java index 447e6ef3..2dfae2e6 100644 --- a/java/src/test/java/com/google/protobuf/BoundedByteStringTest.java +++ b/java/src/test/java/com/google/protobuf/BoundedByteStringTest.java @@ -73,7 +73,7 @@ public class BoundedByteStringTest extends LiteralByteStringTest { } @Override - public void testCharsetToString() throws UnsupportedEncodingException { + public void testCharsetToString() { String testString = "I love unicode \u1234\u5678 characters"; LiteralByteString unicode = new LiteralByteString(testString.getBytes(Internal.UTF_8)); ByteString chopped = unicode.substring(2, unicode.size() - 6); diff --git a/java/src/test/java/com/google/protobuf/ByteStringTest.java b/java/src/test/java/com/google/protobuf/ByteStringTest.java index 8af5dcde..36f64251 100644 --- a/java/src/test/java/com/google/protobuf/ByteStringTest.java +++ b/java/src/test/java/com/google/protobuf/ByteStringTest.java @@ -39,7 +39,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; @@ -129,7 +128,7 @@ public class ByteStringTest extends TestCase { isArrayRange(byteString.toByteArray(), bytes, 500, bytes.length - 500)); } - public void testCopyFrom_StringEncoding() throws UnsupportedEncodingException { + public void testCopyFrom_StringEncoding() { String testString = "I love unicode \u1234\u5678 characters"; ByteString byteString = ByteString.copyFrom(testString, UTF_16); byte[] testBytes = testString.getBytes(UTF_16); @@ -137,7 +136,7 @@ public class ByteStringTest extends TestCase { isArrayRange(byteString.toByteArray(), testBytes, 0, testBytes.length)); } - public void testCopyFrom_Utf8() throws UnsupportedEncodingException { + public void testCopyFrom_Utf8() { String testString = "I love unicode \u1234\u5678 characters"; ByteString byteString = ByteString.copyFromUtf8(testString); byte[] testBytes = testString.getBytes(Internal.UTF_8); @@ -154,6 +153,7 @@ public class ByteStringTest extends TestCase { isArrayRange(byteString.toByteArray(), testBytes, 0, testBytes.length)); // Call copyFrom on an iteration that's not a collection ByteString byteStringAlt = ByteString.copyFrom(new Iterable<ByteString>() { + @Override public Iterator<ByteString> iterator() { return pieces.iterator(); } @@ -399,7 +399,7 @@ public class ByteStringTest extends TestCase { } } - public void testToStringUtf8() throws UnsupportedEncodingException { + public void testToStringUtf8() { String testString = "I love unicode \u1234\u5678 characters"; byte[] testBytes = testString.getBytes(Internal.UTF_8); ByteString byteString = ByteString.copyFrom(testBytes); @@ -419,7 +419,7 @@ public class ByteStringTest extends TestCase { // Test newOutput() using a variety of buffer sizes and a variety of (fixed) // write sizes - public void testNewOutput_ArrayWrite() throws IOException { + public void testNewOutput_ArrayWrite() { byte[] bytes = getTestBytes(); int length = bytes.length; int[] bufferSizes = {128, 256, length / 2, length - 1, length, length + 1, @@ -442,7 +442,7 @@ public class ByteStringTest extends TestCase { // Test newOutput() using a variety of buffer sizes, but writing all the // characters using write(byte); - public void testNewOutput_WriteChar() throws IOException { + public void testNewOutput_WriteChar() { byte[] bytes = getTestBytes(); int length = bytes.length; int[] bufferSizes = {0, 1, 128, 256, length / 2, @@ -461,7 +461,7 @@ public class ByteStringTest extends TestCase { // 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() throws IOException { + public void testNewOutput_Mixed() { Random rng = new Random(1); byte[] bytes = getTestBytes(); int length = bytes.length; @@ -494,7 +494,7 @@ public class ByteStringTest extends TestCase { } } - public void testNewOutputEmpty() throws IOException { + public void testNewOutputEmpty() { // Make sure newOutput() correctly builds empty byte strings ByteString byteString = ByteString.newOutput().toByteString(); assertEquals(ByteString.EMPTY, byteString); diff --git a/java/src/test/java/com/google/protobuf/CodedOutputStreamTest.java b/java/src/test/java/com/google/protobuf/CodedOutputStreamTest.java index 360e759e..6018ea55 100644 --- a/java/src/test/java/com/google/protobuf/CodedOutputStreamTest.java +++ b/java/src/test/java/com/google/protobuf/CodedOutputStreamTest.java @@ -37,6 +37,7 @@ import protobuf_unittest.UnittestProto.TestSparseEnum; import junit.framework.TestCase; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -80,8 +81,8 @@ public class CodedOutputStreamTest extends TestCase { * checks that the result matches the given bytes. */ private void assertWriteVarint(byte[] data, long value) throws Exception { - // Only do 32-bit write if the value fits in 32 bits. - if ((value >>> 32) == 0) { + // Only test 32-bit write if the value fits into an int. + if (value == (int) value) { ByteArrayOutputStream rawOutput = new ByteArrayOutputStream(); CodedOutputStream output = CodedOutputStream.newInstance(rawOutput); output.writeRawVarint32((int) value); @@ -107,8 +108,8 @@ public class CodedOutputStreamTest extends TestCase { // Try different block sizes. for (int blockSize = 1; blockSize <= 16; blockSize *= 2) { - // Only do 32-bit write if the value fits in 32 bits. - if ((value >>> 32) == 0) { + // Only test 32-bit write if the value fits into an int. + if (value == (int) value) { ByteArrayOutputStream rawOutput = new ByteArrayOutputStream(); CodedOutputStream output = CodedOutputStream.newInstance(rawOutput, blockSize); @@ -128,6 +129,42 @@ public class CodedOutputStreamTest extends TestCase { } } + private void assertVarintRoundTrip(long value) throws Exception { + { + ByteArrayOutputStream rawOutput = new ByteArrayOutputStream(); + CodedOutputStream output = CodedOutputStream.newInstance(rawOutput); + output.writeRawVarint64(value); + output.flush(); + byte[] bytes = rawOutput.toByteArray(); + assertEquals(bytes.length, CodedOutputStream.computeRawVarint64Size(value)); + CodedInputStream input = CodedInputStream.newInstance(new ByteArrayInputStream(bytes)); + assertEquals(value, input.readRawVarint64()); + } + + if (value == (int) value) { + ByteArrayOutputStream rawOutput = new ByteArrayOutputStream(); + CodedOutputStream output = CodedOutputStream.newInstance(rawOutput); + output.writeRawVarint32((int) value); + output.flush(); + byte[] bytes = rawOutput.toByteArray(); + assertEquals(bytes.length, CodedOutputStream.computeRawVarint32Size((int) value)); + CodedInputStream input = CodedInputStream.newInstance(new ByteArrayInputStream(bytes)); + assertEquals(value, input.readRawVarint32()); + } + } + + /** Checks that invariants are maintained for varint round trip input and output. */ + public void testVarintRoundTrips() throws Exception { + assertVarintRoundTrip(0L); + for (int bits = 0; bits < 64; bits++) { + long value = 1L << bits; + assertVarintRoundTrip(value); + assertVarintRoundTrip(value + 1); + assertVarintRoundTrip(value - 1); + assertVarintRoundTrip(-value); + } + } + /** Tests writeRawVarint32() and writeRawVarint64(). */ public void testWriteVarint() throws Exception { assertWriteVarint(bytes(0x00), 0); diff --git a/java/src/test/java/com/google/protobuf/DescriptorsTest.java b/java/src/test/java/com/google/protobuf/DescriptorsTest.java index 30da2487..82ff34af 100644 --- a/java/src/test/java/com/google/protobuf/DescriptorsTest.java +++ b/java/src/test/java/com/google/protobuf/DescriptorsTest.java @@ -274,6 +274,15 @@ public class DescriptorsTest extends TestCase { assertFalse(repeatedField.isRequired()); assertTrue(repeatedField.isRepeated()); } + + public void testFieldDescriptorJsonName() throws Exception { + FieldDescriptor requiredField = TestRequired.getDescriptor().findFieldByName("a"); + FieldDescriptor optionalField = TestAllTypes.getDescriptor().findFieldByName("optional_int32"); + FieldDescriptor repeatedField = TestAllTypes.getDescriptor().findFieldByName("repeated_int32"); + assertEquals("a", requiredField.getJsonName()); + assertEquals("optionalInt32", optionalField.getJsonName()); + assertEquals("repeatedInt32", repeatedField.getJsonName()); + } public void testFieldDescriptorDefault() throws Exception { Descriptor d = TestAllTypes.getDescriptor(); diff --git a/java/src/test/java/com/google/protobuf/DoubleArrayListTest.java b/java/src/test/java/com/google/protobuf/DoubleArrayListTest.java index e7a73d70..d3deaa07 100644 --- a/java/src/test/java/com/google/protobuf/DoubleArrayListTest.java +++ b/java/src/test/java/com/google/protobuf/DoubleArrayListTest.java @@ -310,7 +310,7 @@ public class DoubleArrayListTest extends TestCase { } private void assertImmutable(DoubleArrayList list) { - if (list.contains(1)) { + if (list.contains(1D)) { throw new RuntimeException("Cannot test the immutability of lists that contain 1."); } @@ -413,7 +413,7 @@ public class DoubleArrayListTest extends TestCase { } try { - list.removeAll(Collections.singleton(1)); + list.removeAll(Collections.singleton(1D)); fail(); } catch (UnsupportedOperationException e) { // expected @@ -434,7 +434,7 @@ public class DoubleArrayListTest extends TestCase { } try { - list.retainAll(Collections.singleton(1)); + list.retainAll(Collections.singleton(1D)); fail(); } catch (UnsupportedOperationException e) { // expected diff --git a/java/src/test/java/com/google/protobuf/FloatArrayListTest.java b/java/src/test/java/com/google/protobuf/FloatArrayListTest.java index 8f3e93dc..a5e65424 100644 --- a/java/src/test/java/com/google/protobuf/FloatArrayListTest.java +++ b/java/src/test/java/com/google/protobuf/FloatArrayListTest.java @@ -310,7 +310,7 @@ public class FloatArrayListTest extends TestCase { } private void assertImmutable(FloatArrayList list) { - if (list.contains(1)) { + if (list.contains(1F)) { throw new RuntimeException("Cannot test the immutability of lists that contain 1."); } @@ -413,7 +413,7 @@ public class FloatArrayListTest extends TestCase { } try { - list.removeAll(Collections.singleton(1)); + list.removeAll(Collections.singleton(1F)); fail(); } catch (UnsupportedOperationException e) { // expected @@ -434,7 +434,7 @@ public class FloatArrayListTest extends TestCase { } try { - list.retainAll(Collections.singleton(1)); + list.retainAll(Collections.singleton(1F)); fail(); } catch (UnsupportedOperationException e) { // expected diff --git a/java/src/test/java/com/google/protobuf/LiteralByteStringTest.java b/java/src/test/java/com/google/protobuf/LiteralByteStringTest.java index 7dfda2ae..c8495633 100644 --- a/java/src/test/java/com/google/protobuf/LiteralByteStringTest.java +++ b/java/src/test/java/com/google/protobuf/LiteralByteStringTest.java @@ -355,7 +355,7 @@ public class LiteralByteStringTest extends TestCase { assertEquals(classUnderTest + " unicode must match", testString, roundTripString); } - public void testCharsetToString() throws UnsupportedEncodingException { + public void testCharsetToString() { String testString = "I love unicode \u1234\u5678 characters"; LiteralByteString unicode = new LiteralByteString(testString.getBytes(Internal.UTF_8)); String roundTripString = unicode.toString(Internal.UTF_8); diff --git a/java/src/test/java/com/google/protobuf/LongArrayListTest.java b/java/src/test/java/com/google/protobuf/LongArrayListTest.java index 3a52ec7f..1bd094f7 100644 --- a/java/src/test/java/com/google/protobuf/LongArrayListTest.java +++ b/java/src/test/java/com/google/protobuf/LongArrayListTest.java @@ -310,7 +310,7 @@ public class LongArrayListTest extends TestCase { } private void assertImmutable(LongArrayList list) { - if (list.contains(1)) { + if (list.contains(1L)) { throw new RuntimeException("Cannot test the immutability of lists that contain 1."); } @@ -413,7 +413,7 @@ public class LongArrayListTest extends TestCase { } try { - list.removeAll(Collections.singleton(1)); + list.removeAll(Collections.singleton(1L)); fail(); } catch (UnsupportedOperationException e) { // expected @@ -434,7 +434,7 @@ public class LongArrayListTest extends TestCase { } try { - list.retainAll(Collections.singleton(1)); + list.retainAll(Collections.singleton(1L)); fail(); } catch (UnsupportedOperationException e) { // expected diff --git a/java/src/test/java/com/google/protobuf/NioByteStringTest.java b/java/src/test/java/com/google/protobuf/NioByteStringTest.java new file mode 100644 index 00000000..0679937f --- /dev/null +++ b/java/src/test/java/com/google/protobuf/NioByteStringTest.java @@ -0,0 +1,546 @@ +// 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 junit.framework.TestCase; + +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.UnsupportedEncodingException; +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; + +/** + * Tests for {@link NioByteString}. + */ +public class NioByteStringTest extends TestCase { + private static final ByteString EMPTY = UnsafeByteStrings.unsafeWrap( + 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 = new LiteralByteString(BYTES).hashCode(); + private static final ByteBuffer BUFFER = ByteBuffer.wrap(BYTES.clone()); + private static final ByteString TEST_STRING = UnsafeByteStrings.unsafeWrap(BUFFER); + + public void testExpectedType() { + String actualClassName = getActualClassName(TEST_STRING); + 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] == TEST_STRING.byteAt(i)); + } + assertTrue(CLASSNAME + " must capture the right bytes", stillEqual); + } + + public void testByteIterator() { + boolean stillEqual = true; + ByteString.ByteIterator iter = TEST_STRING.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 : TEST_STRING) { + 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, + TEST_STRING.size()); + } + + public void testGetTreeDepth() { + assertEquals(CLASSNAME + " must have depth 0", 0, TEST_STRING.getTreeDepth()); + } + + public void testIsBalanced() { + assertTrue(CLASSNAME + " is technically balanced", TEST_STRING.isBalanced()); + } + + public void testCopyTo_ByteArrayOffsetLength() { + int destinationOffset = 50; + int length = 100; + byte[] destination = new byte[destinationOffset + length]; + int sourceOffset = 213; + TEST_STRING.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 + TEST_STRING.copyTo(destination, TEST_STRING.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 + TEST_STRING.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 + TEST_STRING.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 + TEST_STRING.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 + TEST_STRING.copyTo(destination, 2 * TEST_STRING.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 + TEST_STRING.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); + TEST_STRING.copyTo(myBuffer); + myBuffer.flip(); + assertEquals(CLASSNAME + ".copyTo(ByteBuffer) must give back the same bytes", + BUFFER, myBuffer); + + // Target buffer bigger than required. + myBuffer = ByteBuffer.allocate(TEST_STRING.size() + 1); + TEST_STRING.copyTo(myBuffer); + myBuffer.flip(); + assertEquals(BUFFER, myBuffer); + + // Target buffer has no space. + myBuffer = ByteBuffer.allocate(0); + try { + TEST_STRING.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 { + TEST_STRING.copyTo(myBuffer); + fail("Should have thrown an exception when target ByteBuffer has insufficient capacity"); + } catch (BufferOverflowException e) { + // Expected. + } + } + + public void testMarkSupported() { + InputStream stream = TEST_STRING.newInput(); + assertTrue(CLASSNAME + ".newInput() must support marking", stream.markSupported()); + } + + public void testMarkAndReset() throws IOException { + int fraction = TEST_STRING.size() / 3; + + InputStream stream = TEST_STRING.newInput(); + stream.mark(TEST_STRING.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", + (TEST_STRING.size() - fraction), stream.available()); + stream.reset(); + assertEquals( + CLASSNAME + ": after resetting, all bytes are available", + TEST_STRING.size(), stream.available()); + + skipFully(stream, TEST_STRING.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. + * <p>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 = TEST_STRING.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<ByteBuffer> byteBuffers = TEST_STRING.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 = TEST_STRING.toByteArray(); + assertTrue(CLASSNAME + ".toByteArray() must give back the same bytes", + Arrays.equals(BYTES, roundTripBytes)); + } + + public void testWriteTo() throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + TEST_STRING.writeTo(bos); + byte[] roundTripBytes = bos.toByteArray(); + assertTrue(CLASSNAME + ".writeTo() must give back the same bytes", + Arrays.equals(BYTES, roundTripBytes)); + } + + public void testNewOutput() throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ByteString.Output output = ByteString.newOutput(); + TEST_STRING.writeTo(output); + assertEquals("Output Size returns correct result", + output.size(), TEST_STRING.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", + TEST_STRING.concat(TEST_STRING), 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), + UnsafeByteStrings.unsafeWrap(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 { + TEST_STRING.toString("invalid"); + fail("Should have thrown an exception."); + } catch (UnsupportedEncodingException expected) { + // This is success + } + } + + public void testEquals() { + assertEquals(CLASSNAME + " must not equal null", false, TEST_STRING.equals(null)); + assertEquals(CLASSNAME + " must equal self", TEST_STRING, TEST_STRING); + assertFalse(CLASSNAME + " must not equal the empty string", + TEST_STRING.equals(EMPTY)); + assertEquals(CLASSNAME + " empty strings must be equal", + EMPTY, TEST_STRING.substring(55, 55)); + assertEquals(CLASSNAME + " must equal another string with the same value", + TEST_STRING, UnsafeByteStrings.unsafeWrap(BUFFER)); + + byte[] mungedBytes = mungedBytes(); + assertFalse(CLASSNAME + " must not equal every string with the same length", + TEST_STRING.equals(UnsafeByteStrings.unsafeWrap(ByteBuffer.wrap(mungedBytes)))); + } + + public void testEqualsLiteralByteString() { + ByteString literal = ByteString.copyFrom(BYTES); + assertEquals(CLASSNAME + " must equal LiteralByteString with same value", literal, + TEST_STRING); + assertEquals(CLASSNAME + " must equal LiteralByteString with same value", TEST_STRING, + literal); + assertFalse(CLASSNAME + " must not equal the empty string", + TEST_STRING.equals(ByteString.EMPTY)); + assertEquals(CLASSNAME + " empty strings must be equal", + ByteString.EMPTY, TEST_STRING.substring(55, 55)); + + literal = ByteString.copyFrom(mungedBytes()); + assertFalse(CLASSNAME + " must not equal every LiteralByteString with the same length", + TEST_STRING.equals(literal)); + assertFalse(CLASSNAME + " must not equal every LiteralByteString with the same length", + literal.equals(TEST_STRING)); + } + + 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, + TEST_STRING); + assertEquals(CLASSNAME + " must equal RopeByteString with same value", TEST_STRING, + rope); + assertFalse(CLASSNAME + " must not equal the empty string", + TEST_STRING.equals(ByteString.EMPTY.concat(ByteString.EMPTY))); + assertEquals(CLASSNAME + " empty strings must be equal", + ByteString.EMPTY.concat(ByteString.EMPTY), TEST_STRING.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", + TEST_STRING.equals(rope)); + assertFalse(CLASSNAME + " must not equal every RopeByteString with the same length", + rope.equals(TEST_STRING)); + } + + 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 = TEST_STRING.hashCode(); + assertEquals(CLASSNAME + " must have expected hashCode", EXPECTED_HASH, hash); + } + + public void testPeekCachedHashCode() { + ByteString newString = UnsafeByteStrings.unsafeWrap(BUFFER); + 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 = TEST_STRING.partialHash(TEST_STRING.size(), 0, TEST_STRING.size()); + assertEquals(CLASSNAME + ".partialHash() must yield expected hashCode", + EXPECTED_HASH, hash); + } + + public void testNewInput() throws IOException { + InputStream input = TEST_STRING.newInput(); + assertEquals("InputStream.available() returns correct value", + TEST_STRING.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 = TEST_STRING.newInput(); + int stringSize = TEST_STRING.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()", + TEST_STRING.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()", + TEST_STRING.byteAt(nearEndIndex) & 0xFF, input.read()); + } + + public void testNewCodedInput() throws IOException { + CodedInputStream cis = TEST_STRING.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, + TEST_STRING.concat(EMPTY), TEST_STRING); + assertSame("empty concatenated with " + CLASSNAME + " must give " + CLASSNAME, + EMPTY.concat(TEST_STRING), TEST_STRING); + } + + public void testJavaSerialization() throws Exception { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(out); + oos.writeObject(TEST_STRING); + 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", TEST_STRING, o); + } + + private static ByteString forString(String str) { + return UnsafeByteStrings.unsafeWrap(ByteBuffer.wrap(str.getBytes(UTF_8))); + } +} diff --git a/java/src/test/java/com/google/protobuf/ProtobufArrayListTest.java b/java/src/test/java/com/google/protobuf/ProtobufArrayListTest.java index f9f5e9c7..245c3dee 100644 --- a/java/src/test/java/com/google/protobuf/ProtobufArrayListTest.java +++ b/java/src/test/java/com/google/protobuf/ProtobufArrayListTest.java @@ -243,7 +243,7 @@ public class ProtobufArrayListTest extends TestCase { } try { - list.removeAll(Collections.<Double>emptyList()); + list.removeAll(Collections.emptyList()); fail(); } catch (UnsupportedOperationException e) { // expected @@ -264,7 +264,7 @@ public class ProtobufArrayListTest extends TestCase { } try { - list.retainAll(Collections.<Double>emptyList()); + list.retainAll(Collections.emptyList()); fail(); } catch (UnsupportedOperationException e) { // expected diff --git a/java/src/test/java/com/google/protobuf/RopeByteStringSubstringTest.java b/java/src/test/java/com/google/protobuf/RopeByteStringSubstringTest.java index cc385599..dc56f2e9 100644 --- a/java/src/test/java/com/google/protobuf/RopeByteStringSubstringTest.java +++ b/java/src/test/java/com/google/protobuf/RopeByteStringSubstringTest.java @@ -96,7 +96,7 @@ public class RopeByteStringSubstringTest extends LiteralByteStringTest { } @Override - public void testCharsetToString() throws UnsupportedEncodingException { + public void testCharsetToString() { String sourceString = "I love unicode \u1234\u5678 characters"; ByteString sourceByteString = ByteString.copyFromUtf8(sourceString); int copies = 250; diff --git a/java/src/test/java/com/google/protobuf/TestUtil.java b/java/src/test/java/com/google/protobuf/TestUtil.java index 792e8665..01acb884 100644 --- a/java/src/test/java/com/google/protobuf/TestUtil.java +++ b/java/src/test/java/com/google/protobuf/TestUtil.java @@ -300,6 +300,16 @@ public final class TestUtil { } /** + * Get a {@code TestAllTypesLite.Builder} with all fields set as they would be by + * {@link #setAllFields(TestAllTypesLite.Builder)}. + */ + public static TestAllTypesLite.Builder getAllLiteSetBuilder() { + TestAllTypesLite.Builder builder = TestAllTypesLite.newBuilder(); + setAllFields(builder); + return builder; + } + + /** * Get a {@code TestAllExtensions} with all fields set as they would be by * {@link #setAllExtensions(TestAllExtensions.Builder)}. */ @@ -339,6 +349,150 @@ public final class TestUtil { setPackedExtensions(builder); return builder.build(); } + + /** + * Set every field of {@code builder} to the values expected by + * {@code assertAllFieldsSet()}. + */ + public static void setAllFields(TestAllTypesLite.Builder builder) { + builder.setOptionalInt32 (101); + builder.setOptionalInt64 (102); + builder.setOptionalUint32 (103); + builder.setOptionalUint64 (104); + builder.setOptionalSint32 (105); + builder.setOptionalSint64 (106); + builder.setOptionalFixed32 (107); + builder.setOptionalFixed64 (108); + builder.setOptionalSfixed32(109); + builder.setOptionalSfixed64(110); + builder.setOptionalFloat (111); + builder.setOptionalDouble (112); + builder.setOptionalBool (true); + builder.setOptionalString ("115"); + builder.setOptionalBytes (toBytes("116")); + + builder.setOptionalGroup( + TestAllTypesLite.OptionalGroup.newBuilder().setA(117).build()); + builder.setOptionalNestedMessage( + TestAllTypesLite.NestedMessage.newBuilder().setBb(118).build()); + builder.setOptionalForeignMessage( + ForeignMessageLite.newBuilder().setC(119).build()); + builder.setOptionalImportMessage( + ImportMessageLite.newBuilder().setD(120).build()); + builder.setOptionalPublicImportMessage( + PublicImportMessageLite.newBuilder().setE(126).build()); + builder.setOptionalLazyMessage( + TestAllTypesLite.NestedMessage.newBuilder().setBb(127).build()); + + builder.setOptionalNestedEnum (TestAllTypesLite.NestedEnum.BAZ); + builder.setOptionalForeignEnum(ForeignEnumLite.FOREIGN_LITE_BAZ); + builder.setOptionalImportEnum (ImportEnumLite.IMPORT_LITE_BAZ); + + builder.setOptionalStringPiece("124"); + builder.setOptionalCord("125"); + + // ----------------------------------------------------------------- + + builder.addRepeatedInt32 (201); + builder.addRepeatedInt64 (202); + builder.addRepeatedUint32 (203); + builder.addRepeatedUint64 (204); + builder.addRepeatedSint32 (205); + builder.addRepeatedSint64 (206); + builder.addRepeatedFixed32 (207); + builder.addRepeatedFixed64 (208); + builder.addRepeatedSfixed32(209); + builder.addRepeatedSfixed64(210); + builder.addRepeatedFloat (211); + builder.addRepeatedDouble (212); + builder.addRepeatedBool (true); + builder.addRepeatedString ("215"); + builder.addRepeatedBytes (toBytes("216")); + + builder.addRepeatedGroup( + TestAllTypesLite.RepeatedGroup.newBuilder().setA(217).build()); + builder.addRepeatedNestedMessage( + TestAllTypesLite.NestedMessage.newBuilder().setBb(218).build()); + builder.addRepeatedForeignMessage( + ForeignMessageLite.newBuilder().setC(219).build()); + builder.addRepeatedImportMessage( + ImportMessageLite.newBuilder().setD(220).build()); + builder.addRepeatedLazyMessage( + TestAllTypesLite.NestedMessage.newBuilder().setBb(227).build()); + + builder.addRepeatedNestedEnum (TestAllTypesLite.NestedEnum.BAR); + builder.addRepeatedForeignEnum(ForeignEnumLite.FOREIGN_LITE_BAR); + builder.addRepeatedImportEnum (ImportEnumLite.IMPORT_LITE_BAR); + + builder.addRepeatedStringPiece("224"); + builder.addRepeatedCord("225"); + + // Add a second one of each field. + builder.addRepeatedInt32 (301); + builder.addRepeatedInt64 (302); + builder.addRepeatedUint32 (303); + builder.addRepeatedUint64 (304); + builder.addRepeatedSint32 (305); + builder.addRepeatedSint64 (306); + builder.addRepeatedFixed32 (307); + builder.addRepeatedFixed64 (308); + builder.addRepeatedSfixed32(309); + builder.addRepeatedSfixed64(310); + builder.addRepeatedFloat (311); + builder.addRepeatedDouble (312); + builder.addRepeatedBool (false); + builder.addRepeatedString ("315"); + builder.addRepeatedBytes (toBytes("316")); + + builder.addRepeatedGroup( + TestAllTypesLite.RepeatedGroup.newBuilder().setA(317).build()); + builder.addRepeatedNestedMessage( + TestAllTypesLite.NestedMessage.newBuilder().setBb(318).build()); + builder.addRepeatedForeignMessage( + ForeignMessageLite.newBuilder().setC(319).build()); + builder.addRepeatedImportMessage( + ImportMessageLite.newBuilder().setD(320).build()); + builder.addRepeatedLazyMessage( + TestAllTypesLite.NestedMessage.newBuilder().setBb(327).build()); + + builder.addRepeatedNestedEnum (TestAllTypesLite.NestedEnum.BAZ); + builder.addRepeatedForeignEnum(ForeignEnumLite.FOREIGN_LITE_BAZ); + builder.addRepeatedImportEnum (ImportEnumLite.IMPORT_LITE_BAZ); + + builder.addRepeatedStringPiece("324"); + builder.addRepeatedCord("325"); + + // ----------------------------------------------------------------- + + builder.setDefaultInt32 (401); + builder.setDefaultInt64 (402); + builder.setDefaultUint32 (403); + builder.setDefaultUint64 (404); + builder.setDefaultSint32 (405); + builder.setDefaultSint64 (406); + builder.setDefaultFixed32 (407); + builder.setDefaultFixed64 (408); + builder.setDefaultSfixed32(409); + builder.setDefaultSfixed64(410); + builder.setDefaultFloat (411); + builder.setDefaultDouble (412); + builder.setDefaultBool (false); + builder.setDefaultString ("415"); + builder.setDefaultBytes (toBytes("416")); + + builder.setDefaultNestedEnum (TestAllTypesLite.NestedEnum.FOO); + builder.setDefaultForeignEnum(ForeignEnumLite.FOREIGN_LITE_FOO); + builder.setDefaultImportEnum (ImportEnumLite.IMPORT_LITE_FOO); + + builder.setDefaultStringPiece("424"); + builder.setDefaultCord("425"); + + builder.setOneofUint32(601); + builder.setOneofNestedMessage( + TestAllTypesLite.NestedMessage.newBuilder().setBb(602).build()); + builder.setOneofString("603"); + builder.setOneofBytes(toBytes("604")); + } /** * Set every field of {@code message} to the values expected by diff --git a/java/src/test/java/com/google/protobuf/map_test.proto b/java/src/test/java/com/google/protobuf/map_test.proto index 2f7709be..2280ac03 100644 --- a/java/src/test/java/com/google/protobuf/map_test.proto +++ b/java/src/test/java/com/google/protobuf/map_test.proto @@ -36,7 +36,6 @@ option java_package = "map_test"; option java_outer_classname = "MapTestProto"; option java_generate_equals_and_hash = true; - message TestMap { message MessageValue { int32 value = 1; diff --git a/java/src/test/java/com/google/protobuf/test_bad_identifiers.proto b/java/src/test/java/com/google/protobuf/test_bad_identifiers.proto index 8c37c03c..2b1f65e4 100644 --- a/java/src/test/java/com/google/protobuf/test_bad_identifiers.proto +++ b/java/src/test/java/com/google/protobuf/test_bad_identifiers.proto @@ -45,7 +45,6 @@ option java_package = "com.google.protobuf"; option java_outer_classname = "TestBadIdentifiersProto"; option java_generate_equals_and_hash = true; - message TestMessage { optional string cached_size = 1; optional string serialized_size = 2; diff --git a/java/util/src/main/java/com/google/protobuf/util/FieldMaskUtil.java b/java/util/src/main/java/com/google/protobuf/util/FieldMaskUtil.java index 7bf87858..535be0fa 100644 --- a/java/util/src/main/java/com/google/protobuf/util/FieldMaskUtil.java +++ b/java/util/src/main/java/com/google/protobuf/util/FieldMaskUtil.java @@ -30,6 +30,9 @@ package com.google.protobuf.util; +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.primitives.Ints; import com.google.protobuf.Descriptors.Descriptor; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.FieldMask; @@ -37,7 +40,6 @@ import com.google.protobuf.Internal; import com.google.protobuf.Message; import java.util.Arrays; -import java.util.List; /** * Utility helper functions to work with {@link com.google.protobuf.FieldMask}. @@ -53,6 +55,7 @@ public class FieldMaskUtil { * Converts a FieldMask to a string. */ public static String toString(FieldMask fieldMask) { + // TODO(xiaofeng): Consider using com.google.common.base.Joiner here instead. StringBuilder result = new StringBuilder(); boolean first = true; for (String value : fieldMask.getPathsList()) { @@ -74,6 +77,7 @@ public class FieldMaskUtil { * Parses from a string to a FieldMask. */ public static FieldMask fromString(String value) { + // TODO(xiaofeng): Consider using com.google.common.base.Splitter here instead. return fromStringList( null, Arrays.asList(value.split(FIELD_PATH_SEPARATOR_REGEX))); } @@ -83,8 +87,8 @@ public class FieldMaskUtil { * * @throws IllegalArgumentException if any of the field path is invalid. */ - public static FieldMask fromString(Class<? extends Message> type, String value) - throws IllegalArgumentException { + public static FieldMask fromString(Class<? extends Message> type, String value) { + // TODO(xiaofeng): Consider using com.google.common.base.Splitter here instead. return fromStringList( type, Arrays.asList(value.split(FIELD_PATH_SEPARATOR_REGEX))); } @@ -94,9 +98,9 @@ public class FieldMaskUtil { * * @throws IllegalArgumentException if any of the field path is not valid. */ + // TODO(xiaofeng): Consider renaming fromStrings() public static FieldMask fromStringList( - Class<? extends Message> type, List<String> paths) - throws IllegalArgumentException { + Class<? extends Message> type, Iterable<String> paths) { FieldMask.Builder builder = FieldMask.newBuilder(); for (String path : paths) { if (path.isEmpty()) { @@ -113,15 +117,74 @@ public class FieldMaskUtil { } /** + * Constructs a FieldMask from the passed field numbers. + * + * @throws IllegalArugmentException if any of the fields are invalid for the message. + */ + public static FieldMask fromFieldNumbers(Class<? extends Message> type, int... fieldNumbers) { + return fromFieldNumbers(type, Ints.asList(fieldNumbers)); + } + + /** + * Constructs a FieldMask from the passed field numbers. + * + * @throws IllegalArugmentException if any of the fields are invalid for the message. + */ + public static FieldMask fromFieldNumbers( + Class<? extends Message> type, Iterable<Integer> fieldNumbers) { + Descriptor descriptor = Internal.getDefaultInstance(type).getDescriptorForType(); + + FieldMask.Builder builder = FieldMask.newBuilder(); + for (Integer fieldNumber : fieldNumbers) { + FieldDescriptor field = descriptor.findFieldByNumber(fieldNumber); + checkArgument( + field != null, + String.format("%s is not a valid field number for %s.", fieldNumber, type)); + builder.addPaths(field.getName()); + } + return builder.build(); + } + + /** + * Checks whether paths in a given fields mask are valid. + */ + public static boolean isValid(Class<? extends Message> type, FieldMask fieldMask) { + Descriptor descriptor = + Internal.getDefaultInstance(type).getDescriptorForType(); + + return isValid(descriptor, fieldMask); + } + + /** + * Checks whether paths in a given fields mask are valid. + */ + public static boolean isValid(Descriptor descriptor, FieldMask fieldMask) { + for (String path : fieldMask.getPathsList()) { + if (!isValid(descriptor, path)) { + return false; + } + } + return true; + } + + /** * Checks whether a given field path is valid. */ public static boolean isValid(Class<? extends Message> type, String path) { + Descriptor descriptor = + Internal.getDefaultInstance(type).getDescriptorForType(); + + return isValid(descriptor, path); + } + + /** + * Checks whether paths in a given fields mask are valid. + */ + public static boolean isValid(Descriptor descriptor, String path) { String[] parts = path.split(FIELD_SEPARATOR_REGEX); if (parts.length == 0) { return false; } - Descriptor descriptor = - Internal.getDefaultInstance(type).getDescriptorForType(); for (String name : parts) { if (descriptor == null) { return false; @@ -171,7 +234,7 @@ public class FieldMaskUtil { /** * Options to customize merging behavior. */ - public static class MergeOptions { + public static final class MergeOptions { private boolean replaceMessageFields = false; private boolean replaceRepeatedFields = false; diff --git a/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java b/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java index c9a39153..d13ff0ed 100644 --- a/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java +++ b/java/util/src/main/java/com/google/protobuf/util/JsonFormat.java @@ -78,6 +78,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeMap; import java.util.logging.Logger; /** @@ -99,7 +100,7 @@ public class JsonFormat { * Creates a {@link Printer} with default configurations. */ public static Printer printer() { - return new Printer(TypeRegistry.getEmptyTypeRegistry()); + return new Printer(TypeRegistry.getEmptyTypeRegistry(), false, false); } /** @@ -107,9 +108,16 @@ public class JsonFormat { */ public static class Printer { private final TypeRegistry registry; - - private Printer(TypeRegistry registry) { + private final boolean includingDefaultValueFields; + private final boolean preservingProtoFieldNames; + + private Printer( + TypeRegistry registry, + boolean includingDefaultValueFields, + boolean preservingProtoFieldNames) { this.registry = registry; + this.includingDefaultValueFields = includingDefaultValueFields; + this.preservingProtoFieldNames = preservingProtoFieldNames; } /** @@ -122,7 +130,27 @@ public class JsonFormat { if (this.registry != TypeRegistry.getEmptyTypeRegistry()) { throw new IllegalArgumentException("Only one registry is allowed."); } - return new Printer(registry); + return new Printer(registry, includingDefaultValueFields, preservingProtoFieldNames); + } + + /** + * Creates a new {@link Printer} that will also print fields set to their + * defaults. Empty repeated fields and map fields will be printed as well. + * The new Printer clones all other configurations from the current + * {@link Printer}. + */ + public Printer includingDefaultValueFields() { + return new Printer(registry, true, preservingProtoFieldNames); + } + + /** + * Creates a new {@link Printer} that is configured to use the original proto + * field names as defined in the .proto file rather than converting them to + * lowerCamelCase. The new Printer clones all other configurations from the + * current {@link Printer}. + */ + public Printer preservingProtoFieldNames() { + return new Printer(registry, includingDefaultValueFields, true); } /** @@ -136,7 +164,8 @@ public class JsonFormat { throws IOException { // TODO(xiaofeng): Investigate the allocation overhead and optimize for // mobile. - new PrinterImpl(registry, output).print(message); + new PrinterImpl(registry, includingDefaultValueFields, preservingProtoFieldNames, output) + .print(message); } /** @@ -298,7 +327,7 @@ public class JsonFormat { private void addFile(FileDescriptor file) { // Skip the file if it's already added. - if (files.contains(file.getName())) { + if (!files.add(file.getFullName())) { return; } for (FileDescriptor dependency : file.getDependencies()) { @@ -397,6 +426,8 @@ public class JsonFormat { */ private static final class PrinterImpl { private final TypeRegistry registry; + private final boolean includingDefaultValueFields; + private final boolean preservingProtoFieldNames; private final TextGenerator generator; // We use Gson to help handle string escapes. private final Gson gson; @@ -405,8 +436,14 @@ public class JsonFormat { private static final Gson DEFAULT_GSON = new Gson(); } - PrinterImpl(TypeRegistry registry, Appendable jsonOutput) { + PrinterImpl( + TypeRegistry registry, + boolean includingDefaultValueFields, + boolean preservingProtoFieldNames, + Appendable jsonOutput) { this.registry = registry; + this.includingDefaultValueFields = includingDefaultValueFields; + this.preservingProtoFieldNames = preservingProtoFieldNames; this.generator = new TextGenerator(jsonOutput); this.gson = GsonHolder.DEFAULT_GSON; } @@ -647,13 +684,23 @@ public class JsonFormat { generator.print("\"@type\": " + gson.toJson(typeUrl)); printedField = true; } - for (Map.Entry<FieldDescriptor, Object> field - : message.getAllFields().entrySet()) { - // Skip unknown enum fields. - if (field.getValue() instanceof EnumValueDescriptor - && ((EnumValueDescriptor) field.getValue()).getIndex() == -1) { - continue; + Map<FieldDescriptor, Object> fieldsToPrint = null; + if (includingDefaultValueFields) { + fieldsToPrint = new TreeMap<FieldDescriptor, Object>(); + for (FieldDescriptor field : message.getDescriptorForType().getFields()) { + if (field.isOptional() + && field.getJavaType() == FieldDescriptor.JavaType.MESSAGE + && !message.hasField(field)) { + // Always skip empty optional message fields. If not we will recurse indefinitely if + // a message has itself as a sub-field. + continue; + } + fieldsToPrint.put(field, message.getField(field)); } + } else { + fieldsToPrint = message.getAllFields(); + } + for (Map.Entry<FieldDescriptor, Object> field : fieldsToPrint.entrySet()) { if (printedField) { // Add line-endings for the previous field. generator.print(",\n"); @@ -673,7 +720,11 @@ public class JsonFormat { private void printField(FieldDescriptor field, Object value) throws IOException { - generator.print("\"" + fieldNameToCamelName(field.getName()) + "\": "); + if (preservingProtoFieldNames) { + generator.print("\"" + field.getName() + "\": "); + } else { + generator.print("\"" + field.getJsonName() + "\": "); + } if (field.isMapField()) { printMapFieldValue(field, value); } else if (field.isRepeated()) { @@ -689,11 +740,6 @@ public class JsonFormat { generator.print("["); boolean printedElement = false; for (Object element : (List) value) { - // Skip unknown enum entries. - if (element instanceof EnumValueDescriptor - && ((EnumValueDescriptor) element).getIndex() == -1) { - continue; - } if (printedElement) { generator.print(", "); } else { @@ -720,11 +766,6 @@ public class JsonFormat { Message entry = (Message) element; Object entryKey = entry.getField(keyField); Object entryValue = entry.getField(valueField); - // Skip unknown enum entries. - if (entryValue instanceof EnumValueDescriptor - && ((EnumValueDescriptor) entryValue).getIndex() == -1) { - continue; - } if (printedElement) { generator.print(",\n"); } else { @@ -871,8 +912,13 @@ public class JsonFormat { generator.print("\""); } } else { - generator.print( - "\"" + ((EnumValueDescriptor) value).getName() + "\""); + if (((EnumValueDescriptor) value).getIndex() == -1) { + generator.print( + String.valueOf(((EnumValueDescriptor) value).getNumber())); + } else { + generator.print( + "\"" + ((EnumValueDescriptor) value).getName() + "\""); + } } break; @@ -916,38 +962,6 @@ public class JsonFormat { } return parts[1]; } - - private static String fieldNameToCamelName(String name) { - StringBuilder result = new StringBuilder(name.length()); - boolean isNextUpperCase = false; - for (int i = 0; i < name.length(); i++) { - Character ch = name.charAt(i); - if (Character.isLowerCase(ch)) { - if (isNextUpperCase) { - result.append(Character.toUpperCase(ch)); - } else { - result.append(ch); - } - isNextUpperCase = false; - } else if (Character.isUpperCase(ch)) { - if (i == 0 && !isNextUpperCase) { - // Force first letter to lower-case unless explicitly told to - // capitalize it. - result.append(Character.toLowerCase(ch)); - } else { - // Capital letters after the first are left as-is. - result.append(ch); - } - isNextUpperCase = false; - } else if (Character.isDigit(ch)) { - result.append(ch); - isNextUpperCase = true; - } else { - isNextUpperCase = true; - } - } - return result.toString(); - } private static class ParserImpl { private final TypeRegistry registry; @@ -1085,7 +1099,8 @@ public class JsonFormat { Map<String, FieldDescriptor> fieldNameMap = new HashMap<String, FieldDescriptor>(); for (FieldDescriptor field : descriptor.getFields()) { - fieldNameMap.put(fieldNameToCamelName(field.getName()), field); + fieldNameMap.put(field.getName(), field); + fieldNameMap.put(field.getJsonName(), field); } fieldNameMaps.put(descriptor, fieldNameMap); return fieldNameMap; @@ -1244,7 +1259,25 @@ public class JsonFormat { private void mergeField(FieldDescriptor field, JsonElement json, Message.Builder builder) throws InvalidProtocolBufferException { - if (json instanceof JsonNull) { + if (field.isRepeated()) { + if (builder.getRepeatedFieldCount(field) > 0) { + throw new InvalidProtocolBufferException( + "Field " + field.getFullName() + " has already been set."); + } + } else { + if (builder.hasField(field)) { + throw new InvalidProtocolBufferException( + "Field " + field.getFullName() + " has already been set."); + } + if (field.getContainingOneof() != null + && builder.getOneofFieldDescriptor(field.getContainingOneof()) != null) { + FieldDescriptor other = builder.getOneofFieldDescriptor(field.getContainingOneof()); + throw new InvalidProtocolBufferException( + "Cannot set field " + field.getFullName() + " because another field " + + other.getFullName() + " belonging to the same oneof has already been set "); + } + } + if (field.isRepeated() && json instanceof JsonNull) { // We allow "null" as value for all field types and treat it as if the // field is not present. return; @@ -1282,7 +1315,8 @@ public class JsonFormat { Object value = parseFieldValue( valueField, entry.getValue(), entryBuilder); if (value == null) { - value = getDefaultValue(valueField, entryBuilder); + throw new InvalidProtocolBufferException( + "Map value cannot be null."); } entryBuilder.setField(keyField, key); entryBuilder.setField(valueField, value); @@ -1341,7 +1375,8 @@ public class JsonFormat { for (int i = 0; i < array.size(); ++i) { Object value = parseFieldValue(field, array.get(i), builder); if (value == null) { - value = getDefaultValue(field, builder); + throw new InvalidProtocolBufferException( + "Repeated field elements cannot be null"); } builder.addRepeatedField(field, value); } @@ -1352,6 +1387,15 @@ public class JsonFormat { try { return Integer.parseInt(json.getAsString()); } catch (Exception e) { + // Fall through. + } + // JSON doesn't distinguish between integer values and floating point values so "1" and + // "1.000" are treated as equal in JSON. For this reason we accept floating point values for + // integer fields as well as long as it actually is an integer (i.e., round(value) == value). + try { + BigDecimal value = new BigDecimal(json.getAsString()); + return value.intValueExact(); + } catch (Exception e) { throw new InvalidProtocolBufferException("Not an int32 value: " + json); } } @@ -1361,7 +1405,16 @@ public class JsonFormat { try { return Long.parseLong(json.getAsString()); } catch (Exception e) { - throw new InvalidProtocolBufferException("Not an int64 value: " + json); + // Fall through. + } + // JSON doesn't distinguish between integer values and floating point values so "1" and + // "1.000" are treated as equal in JSON. For this reason we accept floating point values for + // integer fields as well as long as it actually is an integer (i.e., round(value) == value). + try { + BigDecimal value = new BigDecimal(json.getAsString()); + return value.longValueExact(); + } catch (Exception e) { + throw new InvalidProtocolBufferException("Not an int32 value: " + json); } } @@ -1377,6 +1430,21 @@ public class JsonFormat { } catch (InvalidProtocolBufferException e) { throw e; } catch (Exception e) { + // Fall through. + } + // JSON doesn't distinguish between integer values and floating point values so "1" and + // "1.000" are treated as equal in JSON. For this reason we accept floating point values for + // integer fields as well as long as it actually is an integer (i.e., round(value) == value). + try { + BigDecimal decimalValue = new BigDecimal(json.getAsString()); + BigInteger value = decimalValue.toBigIntegerExact(); + if (value.signum() < 0 || value.compareTo(new BigInteger("FFFFFFFF", 16)) > 0) { + throw new InvalidProtocolBufferException("Out of range uint32 value: " + json); + } + return value.intValue(); + } catch (InvalidProtocolBufferException e) { + throw e; + } catch (Exception e) { throw new InvalidProtocolBufferException( "Not an uint32 value: " + json); } @@ -1388,7 +1456,8 @@ public class JsonFormat { private long parseUint64(JsonElement json) throws InvalidProtocolBufferException { try { - BigInteger value = new BigInteger(json.getAsString()); + BigDecimal decimalValue = new BigDecimal(json.getAsString()); + BigInteger value = decimalValue.toBigIntegerExact(); if (value.compareTo(BigInteger.ZERO) < 0 || value.compareTo(MAX_UINT64) > 0) { throw new InvalidProtocolBufferException( @@ -1488,7 +1557,12 @@ public class JsonFormat { return json.getAsString(); } - private ByteString parseBytes(JsonElement json) { + private ByteString parseBytes(JsonElement json) throws InvalidProtocolBufferException { + String encoded = json.getAsString(); + if (encoded.length() % 4 != 0) { + throw new InvalidProtocolBufferException( + "Bytes field is not encoded in standard BASE64 with paddings: " + encoded); + } return ByteString.copyFrom( BaseEncoding.base64().decode(json.getAsString())); } @@ -1498,9 +1572,25 @@ public class JsonFormat { String value = json.getAsString(); EnumValueDescriptor result = enumDescriptor.findValueByName(value); if (result == null) { - throw new InvalidProtocolBufferException( - "Invalid enum value: " + value + " for enum type: " - + enumDescriptor.getFullName()); + // Try to interpret the value as a number. + try { + int numericValue = parseInt32(json); + if (enumDescriptor.getFile().getSyntax() == FileDescriptor.Syntax.PROTO3) { + result = enumDescriptor.findValueByNumberCreatingIfUnknown(numericValue); + } else { + result = enumDescriptor.findValueByNumber(numericValue); + } + } catch (InvalidProtocolBufferException e) { + // Fall through. This exception is about invalid int32 value we get from parseInt32() but + // that's not the exception we want the user to see. Since result == null, we will throw + // an exception later. + } + + if (result == null) { + throw new InvalidProtocolBufferException( + "Invalid enum value: " + value + " for enum type: " + + enumDescriptor.getFullName()); + } } return result; } diff --git a/java/util/src/main/java/com/google/protobuf/util/TimeUtil.java b/java/util/src/main/java/com/google/protobuf/util/TimeUtil.java index 6e4b7c03..3033182a 100644 --- a/java/util/src/main/java/com/google/protobuf/util/TimeUtil.java +++ b/java/util/src/main/java/com/google/protobuf/util/TimeUtil.java @@ -58,8 +58,12 @@ public class TimeUtil { private static final long MILLIS_PER_SECOND = 1000; private static final long MICROS_PER_SECOND = 1000000; - private static final SimpleDateFormat timestampFormat = - createTimestampFormat(); + private static final ThreadLocal<SimpleDateFormat> timestampFormat = + new ThreadLocal<SimpleDateFormat>() { + protected SimpleDateFormat initialValue() { + return createTimestampFormat(); + } + }; private static SimpleDateFormat createTimestampFormat() { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); @@ -96,7 +100,7 @@ public class TimeUtil { throw new IllegalArgumentException("Timestamp is out of range."); } Date date = new Date(timestamp.getSeconds() * MILLIS_PER_SECOND); - result.append(timestampFormat.format(date)); + result.append(timestampFormat.get().format(date)); // Format the nanos part. if (timestamp.getNanos() < 0 || timestamp.getNanos() >= NANOS_PER_SECOND) { throw new IllegalArgumentException("Timestamp has invalid nanos value."); @@ -147,7 +151,7 @@ public class TimeUtil { secondValue = timeValue.substring(0, pointPosition); nanoValue = timeValue.substring(pointPosition + 1); } - Date date = timestampFormat.parse(secondValue); + Date date = timestampFormat.get().parse(secondValue); long seconds = date.getTime() / MILLIS_PER_SECOND; int nanos = nanoValue.isEmpty() ? 0 : parseNanos(nanoValue); // Parse timezone offsets. diff --git a/java/util/src/test/java/com/google/protobuf/util/FieldMaskUtilTest.java b/java/util/src/test/java/com/google/protobuf/util/FieldMaskUtilTest.java index 67fbe0b1..a312fc33 100644 --- a/java/util/src/test/java/com/google/protobuf/util/FieldMaskUtilTest.java +++ b/java/util/src/test/java/com/google/protobuf/util/FieldMaskUtilTest.java @@ -53,6 +53,21 @@ public class FieldMaskUtilTest extends TestCase { NestedTestAllTypes.class, "payload.nonexist")); assertTrue(FieldMaskUtil.isValid( + NestedTestAllTypes.class, FieldMaskUtil.fromString("payload"))); + assertFalse(FieldMaskUtil.isValid( + NestedTestAllTypes.class, FieldMaskUtil.fromString("nonexist"))); + assertFalse(FieldMaskUtil.isValid( + NestedTestAllTypes.class, FieldMaskUtil.fromString("payload,nonexist"))); + + assertTrue(FieldMaskUtil.isValid(NestedTestAllTypes.getDescriptor(), "payload")); + assertFalse(FieldMaskUtil.isValid(NestedTestAllTypes.getDescriptor(), "nonexist")); + + assertTrue(FieldMaskUtil.isValid( + NestedTestAllTypes.getDescriptor(), FieldMaskUtil.fromString("payload"))); + assertFalse(FieldMaskUtil.isValid( + NestedTestAllTypes.getDescriptor(), FieldMaskUtil.fromString("nonexist"))); + + assertTrue(FieldMaskUtil.isValid( NestedTestAllTypes.class, "payload.optional_nested_message.bb")); // Repeated fields cannot have sub-paths. assertFalse(FieldMaskUtil.isValid( @@ -74,7 +89,7 @@ public class FieldMaskUtilTest extends TestCase { addPaths("bar").addPaths("").build(); assertEquals("foo,bar", FieldMaskUtil.toString(mask)); } - + public void testFromString() throws Exception { FieldMask mask = FieldMaskUtil.fromString(""); assertEquals(0, mask.getPathsCount()); @@ -85,16 +100,16 @@ public class FieldMaskUtilTest extends TestCase { assertEquals(2, mask.getPathsCount()); assertEquals("foo", mask.getPaths(0)); assertEquals("bar.baz", mask.getPaths(1)); - + // Empty field paths are ignore. mask = FieldMaskUtil.fromString(",foo,,bar,"); assertEquals(2, mask.getPathsCount()); assertEquals("foo", mask.getPaths(0)); assertEquals("bar", mask.getPaths(1)); - + // Check whether the field paths are valid if a class parameter is provided. mask = FieldMaskUtil.fromString(NestedTestAllTypes.class, ",payload"); - + try { mask = FieldMaskUtil.fromString( NestedTestAllTypes.class, "payload,nonexist"); @@ -103,6 +118,31 @@ public class FieldMaskUtilTest extends TestCase { // Expected. } } + + public void testFromFieldNumbers() throws Exception { + FieldMask mask = FieldMaskUtil.fromFieldNumbers(TestAllTypes.class); + assertEquals(0, mask.getPathsCount()); + mask = + FieldMaskUtil.fromFieldNumbers( + TestAllTypes.class, TestAllTypes.OPTIONAL_INT32_FIELD_NUMBER); + assertEquals(1, mask.getPathsCount()); + assertEquals("optional_int32", mask.getPaths(0)); + mask = + FieldMaskUtil.fromFieldNumbers( + TestAllTypes.class, + TestAllTypes.OPTIONAL_INT32_FIELD_NUMBER, + TestAllTypes.OPTIONAL_INT64_FIELD_NUMBER); + assertEquals(2, mask.getPathsCount()); + assertEquals("optional_int32", mask.getPaths(0)); + assertEquals("optional_int64", mask.getPaths(1)); + + try { + int invalidFieldNumber = 1000; + mask = FieldMaskUtil.fromFieldNumbers(TestAllTypes.class, invalidFieldNumber); + fail("Exception is expected."); + } catch (IllegalArgumentException expected) { + } + } public void testUnion() throws Exception { // Only test a simple case here and expect diff --git a/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java b/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java index ddf5ad2a..c0eb0330 100644 --- a/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java +++ b/java/util/src/test/java/com/google/protobuf/util/JsonFormatTest.java @@ -51,9 +51,11 @@ import com.google.protobuf.util.JsonTestProto.TestAllTypes; import com.google.protobuf.util.JsonTestProto.TestAllTypes.NestedEnum; import com.google.protobuf.util.JsonTestProto.TestAllTypes.NestedMessage; import com.google.protobuf.util.JsonTestProto.TestAny; +import com.google.protobuf.util.JsonTestProto.TestCustomJsonName; import com.google.protobuf.util.JsonTestProto.TestDuration; import com.google.protobuf.util.JsonTestProto.TestFieldMask; import com.google.protobuf.util.JsonTestProto.TestMap; +import com.google.protobuf.util.JsonTestProto.TestOneof; import com.google.protobuf.util.JsonTestProto.TestStruct; import com.google.protobuf.util.JsonTestProto.TestTimestamp; import com.google.protobuf.util.JsonTestProto.TestWrappers; @@ -196,9 +198,6 @@ public class JsonFormatTest extends TestCase { } public void testUnknownEnumValues() throws Exception { - // Unknown enum values will be dropped. - // TODO(xiaofeng): We may want to revisit this (whether we should omit - // unknown enum values). TestAllTypes message = TestAllTypes.newBuilder() .setOptionalNestedEnumValue(12345) .addRepeatedNestedEnumValue(12345) @@ -206,8 +205,10 @@ public class JsonFormatTest extends TestCase { .build(); assertEquals( "{\n" - + " \"repeatedNestedEnum\": [\"FOO\"]\n" + + " \"optionalNestedEnum\": 12345,\n" + + " \"repeatedNestedEnum\": [12345, \"FOO\"]\n" + "}", toJsonString(message)); + assertRoundTripEquals(message); TestMap.Builder mapBuilder = TestMap.newBuilder(); mapBuilder.getMutableInt32ToEnumMapValue().put(1, 0); @@ -216,9 +217,11 @@ public class JsonFormatTest extends TestCase { assertEquals( "{\n" + " \"int32ToEnumMap\": {\n" - + " \"1\": \"FOO\"\n" + + " \"1\": \"FOO\",\n" + + " \"2\": 12345\n" + " }\n" + "}", toJsonString(mapMessage)); + assertRoundTripEquals(mapMessage); } public void testSpecialFloatValues() throws Exception { @@ -263,6 +266,35 @@ public class JsonFormatTest extends TestCase { assertEquals(true, message.getOptionalBool()); } + public void testParserAcceptFloatingPointValueForIntegerField() throws Exception { + // Test that numeric values like "1.000", "1e5" will also be accepted. + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + mergeFromJson( + "{\n" + + " \"repeatedInt32\": [1.000, 1e5, \"1.000\", \"1e5\"],\n" + + " \"repeatedUint32\": [1.000, 1e5, \"1.000\", \"1e5\"],\n" + + " \"repeatedInt64\": [1.000, 1e5, \"1.000\", \"1e5\"],\n" + + " \"repeatedUint64\": [1.000, 1e5, \"1.000\", \"1e5\"]\n" + + "}", builder); + int[] expectedValues = new int[]{1, 100000, 1, 100000}; + assertEquals(4, builder.getRepeatedInt32Count()); + assertEquals(4, builder.getRepeatedUint32Count()); + assertEquals(4, builder.getRepeatedInt64Count()); + assertEquals(4, builder.getRepeatedUint64Count()); + for (int i = 0; i < 4; ++i) { + assertEquals(expectedValues[i], builder.getRepeatedInt32(i)); + assertEquals(expectedValues[i], builder.getRepeatedUint32(i)); + assertEquals(expectedValues[i], builder.getRepeatedInt64(i)); + assertEquals(expectedValues[i], builder.getRepeatedUint64(i)); + } + + // Non-integers will still be rejected. + assertRejects("optionalInt32", "1.5"); + assertRejects("optionalUint32", "1.5"); + assertRejects("optionalInt64", "1.5"); + assertRejects("optionalUint64", "1.5"); + } + private void assertRejects(String name, String value) { TestAllTypes.Builder builder = TestAllTypes.newBuilder(); try { @@ -285,6 +317,7 @@ public class JsonFormatTest extends TestCase { TestAllTypes.Builder builder = TestAllTypes.newBuilder(); // Both numeric form and string form are accepted. mergeFromJson("{\"" + name + "\":" + value + "}", builder); + builder.clear(); mergeFromJson("{\"" + name + "\":\"" + value + "\"}", builder); } @@ -370,84 +403,74 @@ public class JsonFormatTest extends TestCase { TestAllTypes message = builder.build(); assertEquals(TestAllTypes.getDefaultInstance(), message); - // Repeated field elements can also be null. - builder = TestAllTypes.newBuilder(); - mergeFromJson( - "{\n" - + " \"repeatedInt32\": [null, null],\n" - + " \"repeatedInt64\": [null, null],\n" - + " \"repeatedUint32\": [null, null],\n" - + " \"repeatedUint64\": [null, null],\n" - + " \"repeatedSint32\": [null, null],\n" - + " \"repeatedSint64\": [null, null],\n" - + " \"repeatedFixed32\": [null, null],\n" - + " \"repeatedFixed64\": [null, null],\n" - + " \"repeatedSfixed32\": [null, null],\n" - + " \"repeatedSfixed64\": [null, null],\n" - + " \"repeatedFloat\": [null, null],\n" - + " \"repeatedDouble\": [null, null],\n" - + " \"repeatedBool\": [null, null],\n" - + " \"repeatedString\": [null, null],\n" - + " \"repeatedBytes\": [null, null],\n" - + " \"repeatedNestedMessage\": [null, null],\n" - + " \"repeatedNestedEnum\": [null, null]\n" - + "}", builder); - message = builder.build(); - // "null" elements will be parsed to default values. - assertEquals(2, message.getRepeatedInt32Count()); - assertEquals(0, message.getRepeatedInt32(0)); - assertEquals(0, message.getRepeatedInt32(1)); - assertEquals(2, message.getRepeatedInt32Count()); - assertEquals(0, message.getRepeatedInt32(0)); - assertEquals(0, message.getRepeatedInt32(1)); - assertEquals(2, message.getRepeatedInt64Count()); - assertEquals(0, message.getRepeatedInt64(0)); - assertEquals(0, message.getRepeatedInt64(1)); - assertEquals(2, message.getRepeatedUint32Count()); - assertEquals(0, message.getRepeatedUint32(0)); - assertEquals(0, message.getRepeatedUint32(1)); - assertEquals(2, message.getRepeatedUint64Count()); - assertEquals(0, message.getRepeatedUint64(0)); - assertEquals(0, message.getRepeatedUint64(1)); - assertEquals(2, message.getRepeatedSint32Count()); - assertEquals(0, message.getRepeatedSint32(0)); - assertEquals(0, message.getRepeatedSint32(1)); - assertEquals(2, message.getRepeatedSint64Count()); - assertEquals(0, message.getRepeatedSint64(0)); - assertEquals(0, message.getRepeatedSint64(1)); - assertEquals(2, message.getRepeatedFixed32Count()); - assertEquals(0, message.getRepeatedFixed32(0)); - assertEquals(0, message.getRepeatedFixed32(1)); - assertEquals(2, message.getRepeatedFixed64Count()); - assertEquals(0, message.getRepeatedFixed64(0)); - assertEquals(0, message.getRepeatedFixed64(1)); - assertEquals(2, message.getRepeatedSfixed32Count()); - assertEquals(0, message.getRepeatedSfixed32(0)); - assertEquals(0, message.getRepeatedSfixed32(1)); - assertEquals(2, message.getRepeatedSfixed64Count()); - assertEquals(0, message.getRepeatedSfixed64(0)); - assertEquals(0, message.getRepeatedSfixed64(1)); - assertEquals(2, message.getRepeatedFloatCount()); - assertEquals(0f, message.getRepeatedFloat(0)); - assertEquals(0f, message.getRepeatedFloat(1)); - assertEquals(2, message.getRepeatedDoubleCount()); - assertEquals(0.0, message.getRepeatedDouble(0)); - assertEquals(0.0, message.getRepeatedDouble(1)); - assertEquals(2, message.getRepeatedBoolCount()); - assertFalse(message.getRepeatedBool(0)); - assertFalse(message.getRepeatedBool(1)); - assertEquals(2, message.getRepeatedStringCount()); - assertTrue(message.getRepeatedString(0).isEmpty()); - assertTrue(message.getRepeatedString(1).isEmpty()); - assertEquals(2, message.getRepeatedBytesCount()); - assertTrue(message.getRepeatedBytes(0).isEmpty()); - assertTrue(message.getRepeatedBytes(1).isEmpty()); - assertEquals(2, message.getRepeatedNestedMessageCount()); - assertEquals(NestedMessage.getDefaultInstance(), message.getRepeatedNestedMessage(0)); - assertEquals(NestedMessage.getDefaultInstance(), message.getRepeatedNestedMessage(1)); - assertEquals(2, message.getRepeatedNestedEnumCount()); - assertEquals(0, message.getRepeatedNestedEnumValue(0)); - assertEquals(0, message.getRepeatedNestedEnumValue(1)); + // Repeated field elements cannot be null. + try { + builder = TestAllTypes.newBuilder(); + mergeFromJson( + "{\n" + + " \"repeatedInt32\": [null, null],\n" + + "}", builder); + fail(); + } catch (InvalidProtocolBufferException e) { + // Exception expected. + } + + try { + builder = TestAllTypes.newBuilder(); + mergeFromJson( + "{\n" + + " \"repeatedNestedMessage\": [null, null],\n" + + "}", builder); + fail(); + } catch (InvalidProtocolBufferException e) { + // Exception expected. + } + } + + public void testParserRejectDuplicatedFields() throws Exception { + // TODO(xiaofeng): The parser we are currently using (GSON) will accept and keep the last + // one if multiple entries have the same name. This is not the desired behavior but it can + // only be fixed by using our own parser. Here we only test the cases where the names are + // different but still referring to the same field. + + // Duplicated optional fields. + try { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + mergeFromJson( + "{\n" + + " \"optionalNestedMessage\": {},\n" + + " \"optional_nested_message\": {}\n" + + "}", builder); + fail(); + } catch (InvalidProtocolBufferException e) { + // Exception expected. + } + + // Duplicated repeated fields. + try { + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + mergeFromJson( + "{\n" + + " \"repeatedNestedMessage\": [null, null],\n" + + " \"repeated_nested_message\": [null, null]\n" + + "}", builder); + fail(); + } catch (InvalidProtocolBufferException e) { + // Exception expected. + } + + // Duplicated oneof fields. + try { + TestOneof.Builder builder = TestOneof.newBuilder(); + mergeFromJson( + "{\n" + + " \"oneofInt32\": 1,\n" + + " \"oneof_int32\": 2\n" + + "}", builder); + fail(); + } catch (InvalidProtocolBufferException e) { + // Exception expected. + } } public void testMapFields() throws Exception { @@ -592,18 +615,30 @@ public class JsonFormatTest extends TestCase { assertRoundTripEquals(message); } - public void testMapNullValueIsDefault() throws Exception { - TestMap.Builder builder = TestMap.newBuilder(); - mergeFromJson( - "{\n" - + " \"int32ToInt32Map\": {\"1\": null},\n" - + " \"int32ToMessageMap\": {\"2\": null}\n" - + "}", builder); - TestMap message = builder.build(); - assertTrue(message.getInt32ToInt32Map().containsKey(1)); - assertEquals(0, message.getInt32ToInt32Map().get(1).intValue()); - assertTrue(message.getInt32ToMessageMap().containsKey(2)); - assertEquals(0, message.getInt32ToMessageMap().get(2).getValue()); + public void testMapNullValueIsRejected() throws Exception { + try { + TestMap.Builder builder = TestMap.newBuilder(); + mergeFromJson( + "{\n" + + " \"int32ToInt32Map\": {null: 1},\n" + + " \"int32ToMessageMap\": {null: 2}\n" + + "}", builder); + fail(); + } catch (InvalidProtocolBufferException e) { + // Exception expected. + } + + try { + TestMap.Builder builder = TestMap.newBuilder(); + mergeFromJson( + "{\n" + + " \"int32ToInt32Map\": {\"1\": null},\n" + + " \"int32ToMessageMap\": {\"2\": null}\n" + + "}", builder); + fail(); + } catch (InvalidProtocolBufferException e) { + // Exception expected. + } } public void testParserAcceptNonQuotedObjectKey() throws Exception { @@ -743,6 +778,15 @@ public class JsonFormatTest extends TestCase { + " }\n" + "}", toJsonString(message)); assertRoundTripEquals(message); + + builder = TestStruct.newBuilder(); + builder.setValue(Value.newBuilder().setNullValueValue(0).build()); + message = builder.build(); + assertEquals( + "{\n" + + " \"value\": null\n" + + "}", toJsonString(message)); + assertRoundTripEquals(message); } public void testAnyFields() throws Exception { @@ -891,6 +935,15 @@ public class JsonFormatTest extends TestCase { + " }\n" + "}", printer.print(anyMessage)); assertRoundTripEquals(anyMessage, registry); + Value.Builder valueBuilder = Value.newBuilder(); + valueBuilder.setNumberValue(1); + anyMessage = Any.pack(valueBuilder.build()); + assertEquals( + "{\n" + + " \"@type\": \"type.googleapis.com/google.protobuf.Value\",\n" + + " \"value\": 1.0\n" + + "}", printer.print(anyMessage)); + assertRoundTripEquals(anyMessage, registry); } public void testParserMissingTypeUrl() throws Exception { @@ -949,16 +1002,9 @@ public class JsonFormatTest extends TestCase { } public void testParserRejectInvalidBase64() throws Exception { - try { - TestAllTypes.Builder builder = TestAllTypes.newBuilder(); - mergeFromJson( - "{\n" - + " \"optionalBytes\": \"!@#$\"\n" - + "}", builder); - fail("Exception is expected."); - } catch (InvalidProtocolBufferException e) { - // Expected. - } + assertRejects("optionalBytes", "!@#$"); + // We use standard BASE64 with paddings. + assertRejects("optionalBytes", "AQI"); } public void testParserRejectInvalidEnumValue() throws Exception { @@ -973,4 +1019,138 @@ public class JsonFormatTest extends TestCase { // Expected. } } + + public void testCustomJsonName() throws Exception { + TestCustomJsonName message = TestCustomJsonName.newBuilder().setValue(12345).build(); + assertEquals("{\n" + " \"@value\": 12345\n" + "}", JsonFormat.printer().print(message)); + assertRoundTripEquals(message); + } + + public void testIncludingDefaultValueFields() throws Exception { + TestAllTypes message = TestAllTypes.getDefaultInstance(); + assertEquals("{\n}", JsonFormat.printer().print(message)); + assertEquals( + "{\n" + + " \"optionalInt32\": 0,\n" + + " \"optionalInt64\": \"0\",\n" + + " \"optionalUint32\": 0,\n" + + " \"optionalUint64\": \"0\",\n" + + " \"optionalSint32\": 0,\n" + + " \"optionalSint64\": \"0\",\n" + + " \"optionalFixed32\": 0,\n" + + " \"optionalFixed64\": \"0\",\n" + + " \"optionalSfixed32\": 0,\n" + + " \"optionalSfixed64\": \"0\",\n" + + " \"optionalFloat\": 0.0,\n" + + " \"optionalDouble\": 0.0,\n" + + " \"optionalBool\": false,\n" + + " \"optionalString\": \"\",\n" + + " \"optionalBytes\": \"\",\n" + + " \"optionalNestedEnum\": \"FOO\",\n" + + " \"repeatedInt32\": [],\n" + + " \"repeatedInt64\": [],\n" + + " \"repeatedUint32\": [],\n" + + " \"repeatedUint64\": [],\n" + + " \"repeatedSint32\": [],\n" + + " \"repeatedSint64\": [],\n" + + " \"repeatedFixed32\": [],\n" + + " \"repeatedFixed64\": [],\n" + + " \"repeatedSfixed32\": [],\n" + + " \"repeatedSfixed64\": [],\n" + + " \"repeatedFloat\": [],\n" + + " \"repeatedDouble\": [],\n" + + " \"repeatedBool\": [],\n" + + " \"repeatedString\": [],\n" + + " \"repeatedBytes\": [],\n" + + " \"repeatedNestedMessage\": [],\n" + + " \"repeatedNestedEnum\": []\n" + + "}", + JsonFormat.printer().includingDefaultValueFields().print(message)); + + TestMap mapMessage = TestMap.getDefaultInstance(); + assertEquals("{\n}", JsonFormat.printer().print(mapMessage)); + assertEquals( + "{\n" + + " \"int32ToInt32Map\": {\n" + + " },\n" + + " \"int64ToInt32Map\": {\n" + + " },\n" + + " \"uint32ToInt32Map\": {\n" + + " },\n" + + " \"uint64ToInt32Map\": {\n" + + " },\n" + + " \"sint32ToInt32Map\": {\n" + + " },\n" + + " \"sint64ToInt32Map\": {\n" + + " },\n" + + " \"fixed32ToInt32Map\": {\n" + + " },\n" + + " \"fixed64ToInt32Map\": {\n" + + " },\n" + + " \"sfixed32ToInt32Map\": {\n" + + " },\n" + + " \"sfixed64ToInt32Map\": {\n" + + " },\n" + + " \"boolToInt32Map\": {\n" + + " },\n" + + " \"stringToInt32Map\": {\n" + + " },\n" + + " \"int32ToInt64Map\": {\n" + + " },\n" + + " \"int32ToUint32Map\": {\n" + + " },\n" + + " \"int32ToUint64Map\": {\n" + + " },\n" + + " \"int32ToSint32Map\": {\n" + + " },\n" + + " \"int32ToSint64Map\": {\n" + + " },\n" + + " \"int32ToFixed32Map\": {\n" + + " },\n" + + " \"int32ToFixed64Map\": {\n" + + " },\n" + + " \"int32ToSfixed32Map\": {\n" + + " },\n" + + " \"int32ToSfixed64Map\": {\n" + + " },\n" + + " \"int32ToFloatMap\": {\n" + + " },\n" + + " \"int32ToDoubleMap\": {\n" + + " },\n" + + " \"int32ToBoolMap\": {\n" + + " },\n" + + " \"int32ToStringMap\": {\n" + + " },\n" + + " \"int32ToBytesMap\": {\n" + + " },\n" + + " \"int32ToMessageMap\": {\n" + + " },\n" + + " \"int32ToEnumMap\": {\n" + + " }\n" + + "}", + JsonFormat.printer().includingDefaultValueFields().print(mapMessage)); + } + + public void testPreservingProtoFieldNames() throws Exception { + TestAllTypes message = TestAllTypes.newBuilder().setOptionalInt32(12345).build(); + assertEquals("{\n" + " \"optionalInt32\": 12345\n" + "}", JsonFormat.printer().print(message)); + assertEquals( + "{\n" + " \"optional_int32\": 12345\n" + "}", + JsonFormat.printer().preservingProtoFieldNames().print(message)); + + // The json_name field option is ignored when configured to use original proto field names. + TestCustomJsonName messageWithCustomJsonName = + TestCustomJsonName.newBuilder().setValue(12345).build(); + assertEquals( + "{\n" + " \"value\": 12345\n" + "}", + JsonFormat.printer().preservingProtoFieldNames().print(messageWithCustomJsonName)); + + // Parsers accept both original proto field names and lowerCamelCase names. + TestAllTypes.Builder builder = TestAllTypes.newBuilder(); + JsonFormat.parser().merge("{\"optionalInt32\": 12345}", builder); + assertEquals(12345, builder.getOptionalInt32()); + builder.clear(); + JsonFormat.parser().merge("{\"optional_int32\": 54321}", builder); + assertEquals(54321, builder.getOptionalInt32()); + } } diff --git a/java/util/src/test/java/com/google/protobuf/util/TimeUtilTest.java b/java/util/src/test/java/com/google/protobuf/util/TimeUtilTest.java index fe5617e1..4c31b2b3 100644 --- a/java/util/src/test/java/com/google/protobuf/util/TimeUtilTest.java +++ b/java/util/src/test/java/com/google/protobuf/util/TimeUtilTest.java @@ -38,6 +38,8 @@ import junit.framework.TestCase; import org.junit.Assert; import java.text.ParseException; +import java.util.ArrayList; +import java.util.List; /** Unit tests for {@link TimeUtil}. */ public class TimeUtilTest extends TestCase { @@ -76,6 +78,71 @@ public class TimeUtilTest extends TestCase { assertEquals("1970-01-01T08:00:00.010Z", TimeUtil.toString(value)); } + private volatile boolean stopParsingThreads = false; + private volatile String errorMessage = ""; + + private class ParseTimestampThread extends Thread { + private final String[] strings; + private final Timestamp[] values; + public ParseTimestampThread(String[] strings, Timestamp[] values) { + this.strings = strings; + this.values = values; + } + + @Override + public void run() { + int index = 0; + while (!stopParsingThreads) { + Timestamp result; + try { + result = TimeUtil.parseTimestamp(strings[index]); + } catch (ParseException e) { + errorMessage = "Failed to parse timestamp: " + strings[index]; + break; + } + if (result.getSeconds() != values[index].getSeconds() + || result.getNanos() != values[index].getNanos()) { + errorMessage = "Actual result: " + result.toString() + ", expected: " + + values[index].toString(); + break; + } + index = (index + 1) % strings.length; + } + } + } + + public void testTimestampConcurrentParsing() throws Exception { + String[] timestampStrings = new String[]{ + "0001-01-01T00:00:00Z", + "9999-12-31T23:59:59.999999999Z", + "1970-01-01T00:00:00Z", + "1969-12-31T23:59:59.999Z", + }; + Timestamp[] timestampValues = new Timestamp[timestampStrings.length]; + for (int i = 0; i < timestampStrings.length; i++) { + timestampValues[i] = TimeUtil.parseTimestamp(timestampStrings[i]); + } + + final int THREAD_COUNT = 16; + final int RUNNING_TIME = 5000; // in milliseconds. + final List<Thread> threads = new ArrayList<Thread>(); + + stopParsingThreads = false; + errorMessage = ""; + for (int i = 0; i < THREAD_COUNT; i++) { + Thread thread = new ParseTimestampThread( + timestampStrings, timestampValues); + thread.start(); + threads.add(thread); + } + Thread.sleep(RUNNING_TIME); + stopParsingThreads = true; + for (Thread thread : threads) { + thread.join(); + } + Assert.assertEquals("", errorMessage); + } + public void testTimetampInvalidFormat() throws Exception { try { // Value too small. diff --git a/java/util/src/test/java/com/google/protobuf/util/json_test.proto b/java/util/src/test/java/com/google/protobuf/util/json_test.proto index b2753af6..023ec2ca 100644 --- a/java/util/src/test/java/com/google/protobuf/util/json_test.proto +++ b/java/util/src/test/java/com/google/protobuf/util/json_test.proto @@ -65,7 +65,7 @@ message TestAllTypes { float optional_float = 11; double optional_double = 12; bool optional_bool = 13; - string optional_string = 14; + string optional_string = 14 [enforce_utf8 = false]; bytes optional_bytes = 15; NestedMessage optional_nested_message = 18; NestedEnum optional_nested_enum = 21; @@ -84,12 +84,19 @@ message TestAllTypes { repeated float repeated_float = 41; repeated double repeated_double = 42; repeated bool repeated_bool = 43; - repeated string repeated_string = 44; + repeated string repeated_string = 44 [enforce_utf8 = false]; repeated bytes repeated_bytes = 45; repeated NestedMessage repeated_nested_message = 48; repeated NestedEnum repeated_nested_enum = 51; } +message TestOneof { + oneof oneof_field { + int32 oneof_int32 = 1; + TestAllTypes.NestedMessage oneof_nested_message = 2; + } +} + message TestMap { // Instead of testing all combinations (too many), we only make sure all // valid types have been used at least in one field as key and in one @@ -105,7 +112,7 @@ message TestMap { map<sfixed32, int32> sfixed32_to_int32_map = 9; map<sfixed64, int32> sfixed64_to_int32_map = 10; map<bool, int32> bool_to_int32_map = 11; - map<string, int32> string_to_int32_map = 12; + map<string, int32> string_to_int32_map = 12 [enforce_utf8 = false]; map<int32, int64> int32_to_int64_map = 101; map<int32, uint32> int32_to_uint32_map = 102; @@ -119,7 +126,7 @@ message TestMap { map<int32, float> int32_to_float_map = 110; map<int32, double> int32_to_double_map = 111; map<int32, bool> int32_to_bool_map = 112; - map<int32, string> int32_to_string_map = 113; + map<int32, string> int32_to_string_map = 113 [enforce_utf8 = false]; map<int32, bytes> int32_to_bytes_map = 114; map<int32, TestAllTypes.NestedMessage> int32_to_message_map = 115; map<int32, TestAllTypes.NestedEnum> int32_to_enum_map = 116; @@ -151,8 +158,13 @@ message TestFieldMask { message TestStruct { google.protobuf.Struct struct_value = 1; + google.protobuf.Value value = 2; } message TestAny { google.protobuf.Any any_value = 1; } + +message TestCustomJsonName { + int32 value = 1 [json_name = "@value"]; +} |