/* * Copyright 2017 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.firebase.database.tubesock; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; import java.util.ArrayList; import java.util.List; /** * Instances provide a builder for a full WebSocketMessage that could be split across multiple * websocket frames. Depending on the opcode, the returned builders will buffer and assemble either * bytes or a String. */ class MessageBuilderFactory { static Builder builder(byte opcode) { if (opcode == WebSocket.OPCODE_BINARY) { return new BinaryBuilder(); } else { // Text return new TextBuilder(); } } interface Builder { boolean appendBytes(byte[] bytes); WebSocketMessage toMessage(); } static class BinaryBuilder implements Builder { private List<byte[]> pendingBytes; private int pendingByteCount = 0; BinaryBuilder() { pendingBytes = new ArrayList<>(); } @Override public boolean appendBytes(byte[] bytes) { pendingBytes.add(bytes); pendingByteCount += bytes.length; return true; } @Override public WebSocketMessage toMessage() { byte[] payload = new byte[pendingByteCount]; int offset = 0; for (int i = 0; i < pendingBytes.size(); ++i) { byte[] segment = pendingBytes.get(i); System.arraycopy(segment, 0, payload, offset, segment.length); offset += segment.length; } return new WebSocketMessage(payload); } } static class TextBuilder implements Builder { private static ThreadLocal<CharsetDecoder> localDecoder = new ThreadLocal<CharsetDecoder>() { @Override protected CharsetDecoder initialValue() { Charset utf8 = Charset.forName("UTF8"); CharsetDecoder decoder = utf8.newDecoder(); decoder.onMalformedInput(CodingErrorAction.REPORT); decoder.onUnmappableCharacter(CodingErrorAction.REPORT); return decoder; } }; private static ThreadLocal<CharsetEncoder> localEncoder = new ThreadLocal<CharsetEncoder>() { @Override protected CharsetEncoder initialValue() { Charset utf8 = Charset.forName("UTF8"); CharsetEncoder encoder = utf8.newEncoder(); encoder.onMalformedInput(CodingErrorAction.REPORT); encoder.onUnmappableCharacter(CodingErrorAction.REPORT); return encoder; } }; private StringBuilder builder; private ByteBuffer carryOver; TextBuilder() { builder = new StringBuilder(); } @Override public boolean appendBytes(byte[] bytes) { // Uncomment if you want slower but more precise decoding. Useful if you're splitting // multi-byte utf8 chars // across websocket frames //String nextFrame = decodeStringStreaming(bytes); String nextFrame = decodeString(bytes); if (nextFrame != null) { builder.append(nextFrame); return true; } return false; } @Override public WebSocketMessage toMessage() { if (carryOver != null) { return null; } return new WebSocketMessage(builder.toString()); } /** * Quicker but less precise utf8 decoding. Does not handle characters split across websocket * frames. * * @param bytes Bytes representing a utf8 string * @return The decoded string */ private String decodeString(byte[] bytes) { try { ByteBuffer input = ByteBuffer.wrap(bytes); CharBuffer buf = localDecoder.get().decode(input); String text = buf.toString(); return text; } catch (CharacterCodingException e) { return null; } } /** * Left in for reference. Less efficient, but potentially catches more errors. Behavior is * largely dependent on how strict the JVM's utf8 decoder is. It is possible on some JVMs to * decode a string that then throws an error when attempting to return it to bytes. * * @param bytes Bytes representing a utf8 string * @return The decoded string */ private String decodeStringStreaming(byte[] bytes) { try { ByteBuffer input = getBuffer(bytes); int bufSize = (int) (input.remaining() * localDecoder.get().averageCharsPerByte()); CharBuffer output = CharBuffer.allocate(bufSize); for (; ; ) { CoderResult result = localDecoder.get().decode(input, output, false); if (result.isError()) { return null; } if (result.isUnderflow()) { break; } if (result.isOverflow()) { // We need more room in our output buffer bufSize = 2 * bufSize + 1; CharBuffer o = CharBuffer.allocate(bufSize); output.flip(); o.put(output); output = o; } } if (input.remaining() > 0) { carryOver = input; } // Re-encode to work around bugs in UTF-8 decoder CharBuffer test = CharBuffer.wrap(output); localEncoder.get().encode(test); output.flip(); String text = output.toString(); return text; } catch (CharacterCodingException e) { return null; } } private ByteBuffer getBuffer(byte[] bytes) { if (carryOver != null) { ByteBuffer buffer = ByteBuffer.allocate(bytes.length + carryOver.remaining()); buffer.put(carryOver); carryOver = null; buffer.put(bytes); buffer.flip(); return buffer; } else { return ByteBuffer.wrap(bytes); } } } }