summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOwen O'Malley <omalley@apache.org>2018-12-17 13:53:49 -0800
committerWouter van Oortmerssen <aardappel@gmail.com>2018-12-17 13:53:49 -0800
commitcb99116aca03722ffdd7c7ddf1741886db32a7fc (patch)
tree570c0c23999fd9e4d12073b1407690a5e38dd42b
parent9ad73bf5a7ebc5d52e4c553707e5d3bdc5207965 (diff)
downloadflatbuffers-cb99116aca03722ffdd7c7ddf1741886db32a7fc.tar.gz
flatbuffers-cb99116aca03722ffdd7c7ddf1741886db32a7fc.tar.bz2
flatbuffers-cb99116aca03722ffdd7c7ddf1741886db32a7fc.zip
Java: Pulling in protobuf's faster UTF-8 encoder. (#5035)
* Pulling in protobuf's faster UTF-8 encoder. * Remove Utf8 unsafe code.
-rw-r--r--java/com/google/flatbuffers/FlatBufferBuilder.java66
-rw-r--r--java/com/google/flatbuffers/Table.java38
-rw-r--r--java/com/google/flatbuffers/Utf8.java191
-rw-r--r--java/com/google/flatbuffers/Utf8Old.java99
-rw-r--r--java/com/google/flatbuffers/Utf8Safe.java451
-rw-r--r--pom.xml6
6 files changed, 781 insertions, 70 deletions
diff --git a/java/com/google/flatbuffers/FlatBufferBuilder.java b/java/com/google/flatbuffers/FlatBufferBuilder.java
index c34ae3f8..4a62f33c 100644
--- a/java/com/google/flatbuffers/FlatBufferBuilder.java
+++ b/java/com/google/flatbuffers/FlatBufferBuilder.java
@@ -21,11 +21,7 @@ import static com.google.flatbuffers.Constants.*;
import java.io.IOException;
import java.io.InputStream;
import java.nio.*;
-import java.nio.charset.CharacterCodingException;
-import java.nio.charset.CharsetEncoder;
-import java.nio.charset.CoderResult;
import java.util.Arrays;
-import java.nio.charset.Charset;
/// @file
/// @addtogroup flatbuffers_java_api
@@ -39,7 +35,6 @@ public class FlatBufferBuilder {
/// @cond FLATBUFFERS_INTERNAL
ByteBuffer bb; // Where we construct the FlatBuffer.
int space; // Remaining space in the ByteBuffer.
- static final Charset utf8charset = Charset.forName("UTF-8"); // The UTF-8 character set used by FlatBuffers.
int minalign = 1; // Minimum alignment encountered so far.
int[] vtable = null; // The vtable for the current table.
int vtable_in_use = 0; // The amount of fields we're actually using.
@@ -50,9 +45,8 @@ public class FlatBufferBuilder {
int num_vtables = 0; // Number of entries in `vtables` in use.
int vector_num_elems = 0; // For the current vector being built.
boolean force_defaults = false; // False omits default values from the serialized data.
- CharsetEncoder encoder = utf8charset.newEncoder();
- ByteBuffer dst;
ByteBufferFactory bb_factory; // Factory for allocating the internal buffer
+ final Utf8 utf8; // UTF-8 encoder to use
/// @endcond
/**
@@ -62,10 +56,31 @@ public class FlatBufferBuilder {
* @param bb_factory The factory to be used for allocating the internal buffer
*/
public FlatBufferBuilder(int initial_size, ByteBufferFactory bb_factory) {
- if (initial_size <= 0) initial_size = 1;
+ this(initial_size, bb_factory, null, Utf8.getDefault());
+ }
+
+ /**
+ * Start with a buffer of size `initial_size`, then grow as required.
+ *
+ * @param initial_size The initial size of the internal buffer to use.
+ * @param bb_factory The factory to be used for allocating the internal buffer
+ * @param existing_bb The byte buffer to reuse.
+ */
+ public FlatBufferBuilder(int initial_size, ByteBufferFactory bb_factory,
+ ByteBuffer existing_bb, Utf8 utf8) {
+ if (initial_size <= 0) {
+ initial_size = 1;
+ }
space = initial_size;
this.bb_factory = bb_factory;
- bb = bb_factory.newByteBuffer(initial_size);
+ if (existing_bb != null) {
+ bb = existing_bb;
+ bb.clear();
+ } else {
+ bb = bb_factory.newByteBuffer(initial_size);
+ }
+ bb.order(ByteOrder.LITTLE_ENDIAN);
+ this.utf8 = utf8;
}
/**
@@ -74,7 +89,7 @@ public class FlatBufferBuilder {
* @param initial_size The initial size of the internal buffer to use.
*/
public FlatBufferBuilder(int initial_size) {
- this(initial_size, new HeapByteBufferFactory());
+ this(initial_size, new HeapByteBufferFactory(), null, Utf8.getDefault());
}
/**
@@ -94,7 +109,7 @@ public class FlatBufferBuilder {
* the existing buffer needs to grow
*/
public FlatBufferBuilder(ByteBuffer existing_bb, ByteBufferFactory bb_factory) {
- init(existing_bb, bb_factory);
+ this(existing_bb.capacity(), bb_factory, existing_bb, Utf8.getDefault());
}
/**
@@ -105,7 +120,7 @@ public class FlatBufferBuilder {
* @param existing_bb The byte buffer to reuse.
*/
public FlatBufferBuilder(ByteBuffer existing_bb) {
- init(existing_bb, new HeapByteBufferFactory());
+ this(existing_bb, new HeapByteBufferFactory());
}
/**
@@ -503,27 +518,12 @@ public class FlatBufferBuilder {
* @return The offset in the buffer where the encoded string starts.
*/
public int createString(CharSequence s) {
- int length = s.length();
- int estimatedDstCapacity = (int) (length * encoder.maxBytesPerChar());
- if (dst == null || dst.capacity() < estimatedDstCapacity) {
- dst = ByteBuffer.allocate(Math.max(128, estimatedDstCapacity));
- }
-
- dst.clear();
-
- CharBuffer src = s instanceof CharBuffer ? (CharBuffer) s :
- CharBuffer.wrap(s);
- CoderResult result = encoder.encode(src, dst, true);
- if (result.isError()) {
- try {
- result.throwException();
- } catch (CharacterCodingException x) {
- throw new Error(x);
- }
- }
-
- dst.flip();
- return createString(dst);
+ int length = utf8.encodedLength(s);
+ addByte((byte)0);
+ startVector(1, length, 1);
+ bb.position(space -= length);
+ utf8.encodeUtf8(s, bb);
+ return endVector();
}
/**
diff --git a/java/com/google/flatbuffers/Table.java b/java/com/google/flatbuffers/Table.java
index 489893b6..703ce28a 100644
--- a/java/com/google/flatbuffers/Table.java
+++ b/java/com/google/flatbuffers/Table.java
@@ -19,11 +19,7 @@ package com.google.flatbuffers;
import static com.google.flatbuffers.Constants.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
-import java.nio.CharBuffer;
-import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
-import java.nio.charset.CharsetDecoder;
-import java.nio.charset.CoderResult;
/// @cond FLATBUFFERS_INTERNAL
@@ -31,23 +27,17 @@ import java.nio.charset.CoderResult;
* All tables in the generated code derive from this class, and add their own accessors.
*/
public class Table {
- private final static ThreadLocal<CharsetDecoder> UTF8_DECODER = new ThreadLocal<CharsetDecoder>() {
- @Override
- protected CharsetDecoder initialValue() {
- return Charset.forName("UTF-8").newDecoder();
- }
- };
public final static ThreadLocal<Charset> UTF8_CHARSET = new ThreadLocal<Charset>() {
@Override
protected Charset initialValue() {
return Charset.forName("UTF-8");
}
};
- private final static ThreadLocal<CharBuffer> CHAR_BUFFER = new ThreadLocal<CharBuffer>();
/** Used to hold the position of the `bb` buffer. */
protected int bb_pos;
/** The underlying ByteBuffer to hold the data of the Table. */
protected ByteBuffer bb;
+ Utf8 utf8 = Utf8.getDefault();
/**
* Get the underlying ByteBuffer.
@@ -98,34 +88,10 @@ public class Table {
* @return Returns a `String` from the data stored inside the FlatBuffer at `offset`.
*/
protected String __string(int offset) {
- CharsetDecoder decoder = UTF8_DECODER.get();
- decoder.reset();
-
offset += bb.getInt(offset);
ByteBuffer src = bb.duplicate().order(ByteOrder.LITTLE_ENDIAN);
int length = src.getInt(offset);
- src.position(offset + SIZEOF_INT);
- src.limit(offset + SIZEOF_INT + length);
-
- int required = (int)((float)length * decoder.maxCharsPerByte());
- CharBuffer dst = CHAR_BUFFER.get();
- if (dst == null || dst.capacity() < required) {
- dst = CharBuffer.allocate(required);
- CHAR_BUFFER.set(dst);
- }
-
- dst.clear();
-
- try {
- CoderResult cr = decoder.decode(src, dst, true);
- if (!cr.isUnderflow()) {
- cr.throwException();
- }
- } catch (CharacterCodingException x) {
- throw new RuntimeException(x);
- }
-
- return dst.flip().toString();
+ return utf8.decodeUtf8(bb, offset + SIZEOF_INT, length);
}
/**
diff --git a/java/com/google/flatbuffers/Utf8.java b/java/com/google/flatbuffers/Utf8.java
new file mode 100644
index 00000000..a2640637
--- /dev/null
+++ b/java/com/google/flatbuffers/Utf8.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.flatbuffers;
+
+import java.nio.ByteBuffer;
+
+import static java.lang.Character.MIN_HIGH_SURROGATE;
+import static java.lang.Character.MIN_LOW_SURROGATE;
+import static java.lang.Character.MIN_SUPPLEMENTARY_CODE_POINT;
+
+public abstract class Utf8 {
+
+ /**
+ * Returns the number of bytes in the UTF-8-encoded form of {@code sequence}. For a string,
+ * this method is equivalent to {@code string.getBytes(UTF_8).length}, but is more efficient in
+ * both time and space.
+ *
+ * @throws IllegalArgumentException if {@code sequence} contains ill-formed UTF-16 (unpaired
+ * surrogates)
+ */
+ public abstract int encodedLength(CharSequence sequence);
+
+ /**
+ * Encodes the given characters to the target {@link ByteBuffer} using UTF-8 encoding.
+ *
+ * <p>Selects an optimal algorithm based on the type of {@link ByteBuffer} (i.e. heap or direct)
+ * and the capabilities of the platform.
+ *
+ * @param in the source string to be encoded
+ * @param out the target buffer to receive the encoded string.
+ */
+ public abstract void encodeUtf8(CharSequence in, ByteBuffer out);
+
+ /**
+ * Decodes the given UTF-8 portion of the {@link ByteBuffer} into a {@link String}.
+ *
+ * @throws IllegalArgumentException if the input is not valid UTF-8.
+ */
+ public abstract String decodeUtf8(ByteBuffer buffer, int offset, int length);
+
+ private static Utf8 DEFAULT;
+
+ /**
+ * Get the default UTF-8 processor.
+ * @return the default processor
+ */
+ public static Utf8 getDefault() {
+ if (DEFAULT == null) {
+ DEFAULT = new Utf8Safe();
+ }
+ return DEFAULT;
+ }
+
+ /**
+ * Set the default instance of the UTF-8 processor.
+ * @param instance the new instance to use
+ */
+ public static void setDefault(Utf8 instance) {
+ DEFAULT = instance;
+ }
+
+ /**
+ * Utility methods for decoding bytes into {@link String}. Callers are responsible for extracting
+ * bytes (possibly using Unsafe methods), and checking remaining bytes. All other UTF-8 validity
+ * checks and codepoint conversion happen in this class.
+ */
+ static class DecodeUtil {
+
+ /**
+ * Returns whether this is a single-byte codepoint (i.e., ASCII) with the form '0XXXXXXX'.
+ */
+ static boolean isOneByte(byte b) {
+ return b >= 0;
+ }
+
+ /**
+ * Returns whether this is a two-byte codepoint with the form '10XXXXXX'.
+ */
+ static boolean isTwoBytes(byte b) {
+ return b < (byte) 0xE0;
+ }
+
+ /**
+ * Returns whether this is a three-byte codepoint with the form '110XXXXX'.
+ */
+ static boolean isThreeBytes(byte b) {
+ return b < (byte) 0xF0;
+ }
+
+ static void handleOneByte(byte byte1, char[] resultArr, int resultPos) {
+ resultArr[resultPos] = (char) byte1;
+ }
+
+ static void handleTwoBytes(
+ byte byte1, byte byte2, char[] resultArr, int resultPos)
+ throws IllegalArgumentException {
+ // Simultaneously checks for illegal trailing-byte in leading position (<= '11000000') and
+ // overlong 2-byte, '11000001'.
+ if (byte1 < (byte) 0xC2
+ || isNotTrailingByte(byte2)) {
+ throw new IllegalArgumentException("Invalid UTF-8");
+ }
+ resultArr[resultPos] = (char) (((byte1 & 0x1F) << 6) | trailingByteValue(byte2));
+ }
+
+ static void handleThreeBytes(
+ byte byte1, byte byte2, byte byte3, char[] resultArr, int resultPos)
+ throws IllegalArgumentException {
+ if (isNotTrailingByte(byte2)
+ // overlong? 5 most significant bits must not all be zero
+ || (byte1 == (byte) 0xE0 && byte2 < (byte) 0xA0)
+ // check for illegal surrogate codepoints
+ || (byte1 == (byte) 0xED && byte2 >= (byte) 0xA0)
+ || isNotTrailingByte(byte3)) {
+ throw new IllegalArgumentException("Invalid UTF-8");
+ }
+ resultArr[resultPos] = (char)
+ (((byte1 & 0x0F) << 12) | (trailingByteValue(byte2) << 6) | trailingByteValue(byte3));
+ }
+
+ static void handleFourBytes(
+ byte byte1, byte byte2, byte byte3, byte byte4, char[] resultArr, int resultPos)
+ throws IllegalArgumentException{
+ if (isNotTrailingByte(byte2)
+ // Check that 1 <= plane <= 16. Tricky optimized form of:
+ // valid 4-byte leading byte?
+ // if (byte1 > (byte) 0xF4 ||
+ // overlong? 4 most significant bits must not all be zero
+ // byte1 == (byte) 0xF0 && byte2 < (byte) 0x90 ||
+ // codepoint larger than the highest code point (U+10FFFF)?
+ // byte1 == (byte) 0xF4 && byte2 > (byte) 0x8F)
+ || (((byte1 << 28) + (byte2 - (byte) 0x90)) >> 30) != 0
+ || isNotTrailingByte(byte3)
+ || isNotTrailingByte(byte4)) {
+ throw new IllegalArgumentException("Invalid UTF-8");
+ }
+ int codepoint = ((byte1 & 0x07) << 18)
+ | (trailingByteValue(byte2) << 12)
+ | (trailingByteValue(byte3) << 6)
+ | trailingByteValue(byte4);
+ resultArr[resultPos] = DecodeUtil.highSurrogate(codepoint);
+ resultArr[resultPos + 1] = DecodeUtil.lowSurrogate(codepoint);
+ }
+
+ /**
+ * Returns whether the byte is not a valid continuation of the form '10XXXXXX'.
+ */
+ private static boolean isNotTrailingByte(byte b) {
+ return b > (byte) 0xBF;
+ }
+
+ /**
+ * Returns the actual value of the trailing byte (removes the prefix '10') for composition.
+ */
+ private static int trailingByteValue(byte b) {
+ return b & 0x3F;
+ }
+
+ private static char highSurrogate(int codePoint) {
+ return (char) ((MIN_HIGH_SURROGATE - (MIN_SUPPLEMENTARY_CODE_POINT >>> 10))
+ + (codePoint >>> 10));
+ }
+
+ private static char lowSurrogate(int codePoint) {
+ return (char) (MIN_LOW_SURROGATE + (codePoint & 0x3ff));
+ }
+ }
+
+ // These UTF-8 handling methods are copied from Guava's Utf8Unsafe class with a modification to throw
+ // a protocol buffer local exception. This exception is then caught in CodedOutputStream so it can
+ // fallback to more lenient behavior.
+ static class UnpairedSurrogateException extends IllegalArgumentException {
+ UnpairedSurrogateException(int index, int length) {
+ super("Unpaired surrogate at index " + index + " of " + length);
+ }
+ }
+}
diff --git a/java/com/google/flatbuffers/Utf8Old.java b/java/com/google/flatbuffers/Utf8Old.java
new file mode 100644
index 00000000..97933332
--- /dev/null
+++ b/java/com/google/flatbuffers/Utf8Old.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2014 Google Inc. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.flatbuffers;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * This class implements the Utf8 API using the Java Utf8 encoder. Use
+ * Utf8.setDefault(new Utf8Old()); to use it.
+ */
+public class Utf8Old extends Utf8 {
+
+ private static class Cache {
+ final CharsetEncoder encoder;
+ final CharsetDecoder decoder;
+ CharSequence lastInput = null;
+ ByteBuffer lastOutput = null;
+
+ Cache() {
+ encoder = StandardCharsets.UTF_8.newEncoder();
+ decoder = StandardCharsets.UTF_8.newDecoder();
+ }
+ }
+
+ private static final ThreadLocal<Cache> CACHE =
+ ThreadLocal.withInitial(() -> new Cache());
+
+ // Play some games so that the old encoder doesn't pay twice for computing
+ // the length of the encoded string.
+
+ @Override
+ public int encodedLength(CharSequence in) {
+ final Cache cache = CACHE.get();
+ int estimated = (int) (in.length() * cache.encoder.maxBytesPerChar());
+ if (cache.lastOutput == null || cache.lastOutput.capacity() < estimated) {
+ cache.lastOutput = ByteBuffer.allocate(Math.max(128, estimated));
+ }
+ cache.lastOutput.clear();
+ cache.lastInput = in;
+ CharBuffer wrap = (in instanceof CharBuffer) ?
+ (CharBuffer) in : CharBuffer.wrap(in);
+ CoderResult result = cache.encoder.encode(wrap, cache.lastOutput, true);
+ if (result.isError()) {
+ try {
+ result.throwException();
+ } catch (CharacterCodingException e) {
+ throw new IllegalArgumentException("bad character encoding", e);
+ }
+ }
+ return cache.lastOutput.remaining();
+ }
+
+ @Override
+ public void encodeUtf8(CharSequence in, ByteBuffer out) {
+ final Cache cache = CACHE.get();
+ if (cache.lastInput != in) {
+ // Update the lastOutput to match our input, although flatbuffer should
+ // never take this branch.
+ encodedLength(in);
+ }
+ out.put(cache.lastOutput);
+ }
+
+ @Override
+ public String decodeUtf8(ByteBuffer buffer, int offset, int length) {
+ CharsetDecoder decoder = CACHE.get().decoder;
+ decoder.reset();
+ buffer = buffer.duplicate();
+ buffer.position(offset);
+ buffer.limit(offset + length);
+ try {
+ CharBuffer result = decoder.decode(buffer);
+ result.flip();
+ return result.toString();
+ } catch (CharacterCodingException e) {
+ throw new IllegalArgumentException("Bad encoding", e);
+ }
+ }
+}
diff --git a/java/com/google/flatbuffers/Utf8Safe.java b/java/com/google/flatbuffers/Utf8Safe.java
new file mode 100644
index 00000000..06ea420b
--- /dev/null
+++ b/java/com/google/flatbuffers/Utf8Safe.java
@@ -0,0 +1,451 @@
+// 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.flatbuffers;
+
+import java.nio.ByteBuffer;
+import static java.lang.Character.MAX_SURROGATE;
+import static java.lang.Character.MIN_SUPPLEMENTARY_CODE_POINT;
+import static java.lang.Character.MIN_SURROGATE;
+import static java.lang.Character.isSurrogatePair;
+import static java.lang.Character.toCodePoint;
+
+/**
+ * A set of low-level, high-performance static utility methods related
+ * to the UTF-8 character encoding. This class has no dependencies
+ * outside of the core JDK libraries.
+ *
+ * <p>There are several variants of UTF-8. The one implemented by
+ * this class is the restricted definition of UTF-8 introduced in
+ * Unicode 3.1, which mandates the rejection of "overlong" byte
+ * sequences as well as rejection of 3-byte surrogate codepoint byte
+ * sequences. Note that the UTF-8 decoder included in Oracle's JDK
+ * has been modified to also reject "overlong" byte sequences, but (as
+ * of 2011) still accepts 3-byte surrogate codepoint byte sequences.
+ *
+ * <p>The byte sequences considered valid by this class are exactly
+ * those that can be roundtrip converted to Strings and back to bytes
+ * using the UTF-8 charset, without loss: <pre> {@code
+ * Arrays.equals(bytes, new String(bytes, Internal.UTF_8).getBytes(Internal.UTF_8))
+ * }</pre>
+ *
+ * <p>See the Unicode Standard,</br>
+ * Table 3-6. <em>UTF-8 Bit Distribution</em>,</br>
+ * Table 3-7. <em>Well Formed UTF-8 Byte Sequences</em>.
+ */
+final public class Utf8Safe extends Utf8 {
+
+ /**
+ * Returns the number of bytes in the UTF-8-encoded form of {@code sequence}. For a string,
+ * this method is equivalent to {@code string.getBytes(UTF_8).length}, but is more efficient in
+ * both time and space.
+ *
+ * @throws IllegalArgumentException if {@code sequence} contains ill-formed UTF-16 (unpaired
+ * surrogates)
+ */
+ private static int computeEncodedLength(CharSequence sequence) {
+ // Warning to maintainers: this implementation is highly optimized.
+ int utf16Length = sequence.length();
+ int utf8Length = utf16Length;
+ int i = 0;
+
+ // This loop optimizes for pure ASCII.
+ while (i < utf16Length && sequence.charAt(i) < 0x80) {
+ i++;
+ }
+
+ // This loop optimizes for chars less than 0x800.
+ for (; i < utf16Length; i++) {
+ char c = sequence.charAt(i);
+ if (c < 0x800) {
+ utf8Length += ((0x7f - c) >>> 31); // branch free!
+ } else {
+ utf8Length += encodedLengthGeneral(sequence, i);
+ break;
+ }
+ }
+
+ if (utf8Length < utf16Length) {
+ // Necessary and sufficient condition for overflow because of maximum 3x expansion
+ throw new IllegalArgumentException("UTF-8 length does not fit in int: "
+ + (utf8Length + (1L << 32)));
+ }
+ return utf8Length;
+ }
+
+ private static int encodedLengthGeneral(CharSequence sequence, int start) {
+ int utf16Length = sequence.length();
+ int utf8Length = 0;
+ for (int i = start; i < utf16Length; i++) {
+ char c = sequence.charAt(i);
+ if (c < 0x800) {
+ utf8Length += (0x7f - c) >>> 31; // branch free!
+ } else {
+ utf8Length += 2;
+ // jdk7+: if (Character.isSurrogate(c)) {
+ if (Character.MIN_SURROGATE <= c && c <= Character.MAX_SURROGATE) {
+ // Check that we have a well-formed surrogate pair.
+ int cp = Character.codePointAt(sequence, i);
+ if (cp < MIN_SUPPLEMENTARY_CODE_POINT) {
+ throw new Utf8Safe.UnpairedSurrogateException(i, utf16Length);
+ }
+ i++;
+ }
+ }
+ }
+ return utf8Length;
+ }
+
+ private static String decodeUtf8Array(byte[] bytes, int index, int size) {
+ // Bitwise OR combines the sign bits so any negative value fails the check.
+ if ((index | size | bytes.length - index - size) < 0) {
+ throw new ArrayIndexOutOfBoundsException(
+ String.format("buffer length=%d, index=%d, size=%d", bytes.length, index, size));
+ }
+
+ int offset = index;
+ final int limit = offset + size;
+
+ // The longest possible resulting String is the same as the number of input bytes, when it is
+ // all ASCII. For other cases, this over-allocates and we will truncate in the end.
+ char[] resultArr = new char[size];
+ int resultPos = 0;
+
+ // Optimize for 100% ASCII (Hotspot loves small simple top-level loops like this).
+ // This simple loop stops when we encounter a byte >= 0x80 (i.e. non-ASCII).
+ while (offset < limit) {
+ byte b = bytes[offset];
+ if (!DecodeUtil.isOneByte(b)) {
+ break;
+ }
+ offset++;
+ DecodeUtil.handleOneByte(b, resultArr, resultPos++);
+ }
+
+ while (offset < limit) {
+ byte byte1 = bytes[offset++];
+ if (DecodeUtil.isOneByte(byte1)) {
+ DecodeUtil.handleOneByte(byte1, resultArr, resultPos++);
+ // It's common for there to be multiple ASCII characters in a run mixed in, so add an
+ // extra optimized loop to take care of these runs.
+ while (offset < limit) {
+ byte b = bytes[offset];
+ if (!DecodeUtil.isOneByte(b)) {
+ break;
+ }
+ offset++;
+ DecodeUtil.handleOneByte(b, resultArr, resultPos++);
+ }
+ } else if (DecodeUtil.isTwoBytes(byte1)) {
+ if (offset >= limit) {
+ throw new IllegalArgumentException("Invalid UTF-8");
+ }
+ DecodeUtil.handleTwoBytes(byte1, /* byte2 */ bytes[offset++], resultArr, resultPos++);
+ } else if (DecodeUtil.isThreeBytes(byte1)) {
+ if (offset >= limit - 1) {
+ throw new IllegalArgumentException("Invalid UTF-8");
+ }
+ DecodeUtil.handleThreeBytes(
+ byte1,
+ /* byte2 */ bytes[offset++],
+ /* byte3 */ bytes[offset++],
+ resultArr,
+ resultPos++);
+ } else {
+ if (offset >= limit - 2) {
+ throw new IllegalArgumentException("Invalid UTF-8");
+ }
+ DecodeUtil.handleFourBytes(
+ byte1,
+ /* byte2 */ bytes[offset++],
+ /* byte3 */ bytes[offset++],
+ /* byte4 */ bytes[offset++],
+ resultArr,
+ resultPos++);
+ // 4-byte case requires two chars.
+ resultPos++;
+ }
+ }
+
+ return new String(resultArr, 0, resultPos);
+ }
+
+ private static String decodeUtf8Buffer(ByteBuffer buffer, int offset,
+ int length) {
+ // Bitwise OR combines the sign bits so any negative value fails the check.
+ if ((offset | length | buffer.limit() - offset - length) < 0) {
+ throw new ArrayIndexOutOfBoundsException(
+ String.format("buffer limit=%d, index=%d, limit=%d", buffer.limit(),
+ offset, length));
+ }
+
+ final int limit = offset + length;
+
+ // The longest possible resulting String is the same as the number of input bytes, when it is
+ // all ASCII. For other cases, this over-allocates and we will truncate in the end.
+ char[] resultArr = new char[length];
+ int resultPos = 0;
+
+ // Optimize for 100% ASCII (Hotspot loves small simple top-level loops like this).
+ // This simple loop stops when we encounter a byte >= 0x80 (i.e. non-ASCII).
+ while (offset < limit) {
+ byte b = buffer.get(offset);
+ if (!DecodeUtil.isOneByte(b)) {
+ break;
+ }
+ offset++;
+ DecodeUtil.handleOneByte(b, resultArr, resultPos++);
+ }
+
+ while (offset < limit) {
+ byte byte1 = buffer.get(offset++);
+ if (DecodeUtil.isOneByte(byte1)) {
+ DecodeUtil.handleOneByte(byte1, resultArr, resultPos++);
+ // It's common for there to be multiple ASCII characters in a run mixed in, so add an
+ // extra optimized loop to take care of these runs.
+ while (offset < limit) {
+ byte b = buffer.get(offset);
+ if (!DecodeUtil.isOneByte(b)) {
+ break;
+ }
+ offset++;
+ DecodeUtil.handleOneByte(b, resultArr, resultPos++);
+ }
+ } else if (DecodeUtil.isTwoBytes(byte1)) {
+ if (offset >= limit) {
+ throw new IllegalArgumentException("Invalid UTF-8");
+ }
+ DecodeUtil.handleTwoBytes(
+ byte1, /* byte2 */ buffer.get(offset++), resultArr, resultPos++);
+ } else if (DecodeUtil.isThreeBytes(byte1)) {
+ if (offset >= limit - 1) {
+ throw new IllegalArgumentException("Invalid UTF-8");
+ }
+ DecodeUtil.handleThreeBytes(
+ byte1,
+ /* byte2 */ buffer.get(offset++),
+ /* byte3 */ buffer.get(offset++),
+ resultArr,
+ resultPos++);
+ } else {
+ if (offset >= limit - 2) {
+ throw new IllegalArgumentException("Invalid UTF-8");
+ }
+ DecodeUtil.handleFourBytes(
+ byte1,
+ /* byte2 */ buffer.get(offset++),
+ /* byte3 */ buffer.get(offset++),
+ /* byte4 */ buffer.get(offset++),
+ resultArr,
+ resultPos++);
+ // 4-byte case requires two chars.
+ resultPos++;
+ }
+ }
+
+ return new String(resultArr, 0, resultPos);
+ }
+
+ @Override
+ public int encodedLength(CharSequence in) {
+ return computeEncodedLength(in);
+ }
+
+ /**
+ * Decodes the given UTF-8 portion of the {@link ByteBuffer} into a {@link String}.
+ *
+ * @throws IllegalArgumentException if the input is not valid UTF-8.
+ */
+ @Override
+ public String decodeUtf8(ByteBuffer buffer, int offset, int length)
+ throws IllegalArgumentException {
+ if (buffer.hasArray()) {
+ return decodeUtf8Array(buffer.array(), buffer.arrayOffset() + offset, length);
+ } else {
+ return decodeUtf8Buffer(buffer, offset, length);
+ }
+ }
+
+
+ private static void encodeUtf8Buffer(CharSequence in, ByteBuffer out) {
+ final int inLength = in.length();
+ int outIx = out.position();
+ int inIx = 0;
+
+ // Since ByteBuffer.putXXX() already checks boundaries for us, no need to explicitly check
+ // access. Assume the buffer is big enough and let it handle the out of bounds exception
+ // if it occurs.
+ try {
+ // Designed to take advantage of
+ // https://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination
+ for (char c; inIx < inLength && (c = in.charAt(inIx)) < 0x80; ++inIx) {
+ out.put(outIx + inIx, (byte) c);
+ }
+ if (inIx == inLength) {
+ // Successfully encoded the entire string.
+ out.position(outIx + inIx);
+ return;
+ }
+
+ outIx += inIx;
+ for (char c; inIx < inLength; ++inIx, ++outIx) {
+ c = in.charAt(inIx);
+ if (c < 0x80) {
+ // One byte (0xxx xxxx)
+ out.put(outIx, (byte) c);
+ } else if (c < 0x800) {
+ // Two bytes (110x xxxx 10xx xxxx)
+
+ // Benchmarks show put performs better than putShort here (for HotSpot).
+ out.put(outIx++, (byte) (0xC0 | (c >>> 6)));
+ out.put(outIx, (byte) (0x80 | (0x3F & c)));
+ } else if (c < MIN_SURROGATE || MAX_SURROGATE < c) {
+ // Three bytes (1110 xxxx 10xx xxxx 10xx xxxx)
+ // Maximum single-char code point is 0xFFFF, 16 bits.
+
+ // Benchmarks show put performs better than putShort here (for HotSpot).
+ out.put(outIx++, (byte) (0xE0 | (c >>> 12)));
+ out.put(outIx++, (byte) (0x80 | (0x3F & (c >>> 6))));
+ out.put(outIx, (byte) (0x80 | (0x3F & c)));
+ } else {
+ // Four bytes (1111 xxxx 10xx xxxx 10xx xxxx 10xx xxxx)
+
+ // Minimum code point represented by a surrogate pair is 0x10000, 17 bits, four UTF-8
+ // bytes
+ final char low;
+ if (inIx + 1 == inLength || !isSurrogatePair(c, (low = in.charAt(++inIx)))) {
+ throw new UnpairedSurrogateException(inIx, inLength);
+ }
+ // TODO(nathanmittler): Consider using putInt() to improve performance.
+ int codePoint = toCodePoint(c, low);
+ out.put(outIx++, (byte) ((0xF << 4) | (codePoint >>> 18)));
+ out.put(outIx++, (byte) (0x80 | (0x3F & (codePoint >>> 12))));
+ out.put(outIx++, (byte) (0x80 | (0x3F & (codePoint >>> 6))));
+ out.put(outIx, (byte) (0x80 | (0x3F & codePoint)));
+ }
+ }
+
+ // Successfully encoded the entire string.
+ out.position(outIx);
+ } catch (IndexOutOfBoundsException e) {
+ // TODO(nathanmittler): Consider making the API throw IndexOutOfBoundsException instead.
+
+ // If we failed in the outer ASCII loop, outIx will not have been updated. In this case,
+ // use inIx to determine the bad write index.
+ int badWriteIndex = out.position() + Math.max(inIx, outIx - out.position() + 1);
+ throw new ArrayIndexOutOfBoundsException(
+ "Failed writing " + in.charAt(inIx) + " at index " + badWriteIndex);
+ }
+ }
+
+ private static int encodeUtf8Array(CharSequence in, byte[] out,
+ int offset, int length) {
+ int utf16Length = in.length();
+ int j = offset;
+ int i = 0;
+ int limit = offset + length;
+ // Designed to take advantage of
+ // https://wikis.oracle.com/display/HotSpotInternals/RangeCheckElimination
+ for (char c; i < utf16Length && i + j < limit && (c = in.charAt(i)) < 0x80; i++) {
+ out[j + i] = (byte) c;
+ }
+ if (i == utf16Length) {
+ return j + utf16Length;
+ }
+ j += i;
+ for (char c; i < utf16Length; i++) {
+ c = in.charAt(i);
+ if (c < 0x80 && j < limit) {
+ out[j++] = (byte) c;
+ } else if (c < 0x800 && j <= limit - 2) { // 11 bits, two UTF-8 bytes
+ out[j++] = (byte) ((0xF << 6) | (c >>> 6));
+ out[j++] = (byte) (0x80 | (0x3F & c));
+ } else if ((c < Character.MIN_SURROGATE || Character.MAX_SURROGATE < c) && j <= limit - 3) {
+ // Maximum single-char code point is 0xFFFF, 16 bits, three UTF-8 bytes
+ out[j++] = (byte) ((0xF << 5) | (c >>> 12));
+ out[j++] = (byte) (0x80 | (0x3F & (c >>> 6)));
+ out[j++] = (byte) (0x80 | (0x3F & c));
+ } else if (j <= limit - 4) {
+ // Minimum code point represented by a surrogate pair is 0x10000, 17 bits,
+ // four UTF-8 bytes
+ final char low;
+ if (i + 1 == in.length()
+ || !Character.isSurrogatePair(c, (low = in.charAt(++i)))) {
+ throw new UnpairedSurrogateException((i - 1), utf16Length);
+ }
+ int codePoint = Character.toCodePoint(c, low);
+ out[j++] = (byte) ((0xF << 4) | (codePoint >>> 18));
+ out[j++] = (byte) (0x80 | (0x3F & (codePoint >>> 12)));
+ out[j++] = (byte) (0x80 | (0x3F & (codePoint >>> 6)));
+ out[j++] = (byte) (0x80 | (0x3F & codePoint));
+ } else {
+ // If we are surrogates and we're not a surrogate pair, always throw an
+ // UnpairedSurrogateException instead of an ArrayOutOfBoundsException.
+ if ((Character.MIN_SURROGATE <= c && c <= Character.MAX_SURROGATE)
+ && (i + 1 == in.length()
+ || !Character.isSurrogatePair(c, in.charAt(i + 1)))) {
+ throw new UnpairedSurrogateException(i, utf16Length);
+ }
+ throw new ArrayIndexOutOfBoundsException("Failed writing " + c + " at index " + j);
+ }
+ }
+ return j;
+ }
+
+ /**
+ * Encodes the given characters to the target {@link ByteBuffer} using UTF-8 encoding.
+ *
+ * <p>Selects an optimal algorithm based on the type of {@link ByteBuffer} (i.e. heap or direct)
+ * and the capabilities of the platform.
+ *
+ * @param in the source string to be encoded
+ * @param out the target buffer to receive the encoded string.
+ */
+ @Override
+ public void encodeUtf8(CharSequence in, ByteBuffer out) {
+ if (out.hasArray()) {
+ int start = out.arrayOffset();
+ int end = encodeUtf8Array(in, out.array(), start + out.position(),
+ out.remaining());
+ out.position(end - start);
+ } else {
+ encodeUtf8Buffer(in, out);
+ }
+ }
+
+ // These UTF-8 handling methods are copied from Guava's Utf8Unsafe class with
+ // a modification to throw a local exception. This exception can be caught
+ // to fallback to more lenient behavior.
+ static class UnpairedSurrogateException extends IllegalArgumentException {
+ UnpairedSurrogateException(int index, int length) {
+ super("Unpaired surrogate at index " + index + " of " + length);
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 795b38c0..3e919115 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.google.flatbuffers</groupId>
<artifactId>flatbuffers-java</artifactId>
- <version>1.10.0</version>
+ <version>1.10.1-SNAPSHOT</version>
<packaging>bundle</packaging>
<name>FlatBuffers Java API</name>
<description>
@@ -78,6 +78,10 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
+ <configuration>
+ <additionalparam>-Xdoclint:none</additionalparam>
+ <additionalOptions>-Xdoclint:none</additionalOptions>
+ </configuration>
<executions>
<execution>
<id>attach-javadocs</id>