/* * Copyright 2015-present Facebook, 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.facebook.buck.bser; import static com.facebook.buck.bser.BserConstants.BSER_ARRAY; import static com.facebook.buck.bser.BserConstants.BSER_FALSE; import static com.facebook.buck.bser.BserConstants.BSER_INT16; import static com.facebook.buck.bser.BserConstants.BSER_INT32; import static com.facebook.buck.bser.BserConstants.BSER_INT64; import static com.facebook.buck.bser.BserConstants.BSER_INT8; import static com.facebook.buck.bser.BserConstants.BSER_NULL; import static com.facebook.buck.bser.BserConstants.BSER_OBJECT; import static com.facebook.buck.bser.BserConstants.BSER_REAL; import static com.facebook.buck.bser.BserConstants.BSER_STRING; import static com.facebook.buck.bser.BserConstants.BSER_TRUE; import com.google.common.collect.Iterables; import com.google.common.io.BaseEncoding; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.CharsetEncoder; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.Map; /** * Encoder for the BSER binary JSON format used by the Watchman service: * * <p>https://facebook.github.io/watchman/docs/bser.html */ public class BserSerializer { private static final int INITIAL_BUFFER_SIZE = 8192; private static final byte[] EMPTY_HEADER = BaseEncoding.base16().decode("00010500000000"); private enum BserIntegralEncodedSize { INT8(1), INT16(2), INT32(4), INT64(8); public final int size; private BserIntegralEncodedSize(int size) { this.size = size; } } private final CharsetEncoder utf8Encoder; public BserSerializer() { this.utf8Encoder = StandardCharsets.UTF_8.newEncoder().onMalformedInput(CodingErrorAction.REPORT); } /** Serializes an object using BSER encoding to the stream. */ public void serializeToStream(Object value, OutputStream outputStream) throws IOException { ByteBuffer buffer = ByteBuffer.allocate(INITIAL_BUFFER_SIZE).order(ByteOrder.nativeOrder()); buffer = serializeToBuffer(value, buffer); buffer.flip(); outputStream.write(buffer.array(), 0, buffer.limit()); } /** * Serializes an object using BSER encoding. If possible, writes the object to the provided byte * buffer and returns it. If the buffer is not big enough to hold the object, returns a new * buffer. * * <p>After returning, buffer.position() is advanced past the last encoded byte. */ public ByteBuffer serializeToBuffer(Object value, ByteBuffer buffer) throws IOException { buffer = increaseBufferCapacityIfNeeded(buffer, EMPTY_HEADER.length); buffer.put(EMPTY_HEADER); buffer = appendRecursive(buffer, value, utf8Encoder); int encodedLength = buffer.position() - EMPTY_HEADER.length; // Overwrite the 32-bit length field at position 3 with the actual length of the object. buffer.putInt(3, encodedLength); return buffer; } @SuppressWarnings("unchecked") private static ByteBuffer appendRecursive( ByteBuffer buffer, Object value, CharsetEncoder utf8Encoder) throws IOException { if (value instanceof Boolean) { buffer = increaseBufferCapacityIfNeeded(buffer, 1); buffer.put(((boolean) value) ? BSER_TRUE : BSER_FALSE); } else if (value == null) { buffer = increaseBufferCapacityIfNeeded(buffer, 1); buffer.put(BSER_NULL); } else if (value instanceof String) { buffer = appendString(buffer, (String) value, utf8Encoder); } else if (value instanceof Double || value instanceof Float) { buffer = increaseBufferCapacityIfNeeded(buffer, 9); buffer.put(BSER_REAL); buffer.putDouble((double) value); } else if (value instanceof Long) { buffer = appendLong(buffer, (long) value); } else if (value instanceof Integer) { buffer = appendLong(buffer, (int) value); } else if (value instanceof Short) { buffer = appendLong(buffer, (short) value); } else if (value instanceof Byte) { buffer = appendLong(buffer, (byte) value); } else if (value instanceof Map<?, ?>) { Map<Object, Object> map = (Map<Object, Object>) value; int mapLen = map.size(); BserIntegralEncodedSize encodedSize = getEncodedSize(mapLen); buffer = increaseBufferCapacityIfNeeded(buffer, 2 + encodedSize.size); buffer.put(BSER_OBJECT); buffer = appendLongWithSize(buffer, mapLen, encodedSize); for (Map.Entry<Object, Object> entry : map.entrySet()) { if (!(entry.getKey() instanceof String)) { throw new IOException( String.format( "Unrecognized map key type %s, expected string", entry.getKey().getClass())); } buffer = appendString(buffer, (String) entry.getKey(), utf8Encoder); buffer = appendRecursive(buffer, entry.getValue(), utf8Encoder); } } else if (value instanceof Iterable<?>) { Iterable<Object> iterable = (Iterable<Object>) value; int len = Iterables.size(iterable); BserIntegralEncodedSize encodedSize = getEncodedSize(len); buffer = increaseBufferCapacityIfNeeded(buffer, 2 + encodedSize.size); buffer.put(BSER_ARRAY); buffer = appendLongWithSize(buffer, len, encodedSize); for (Object obj : iterable) { buffer = appendRecursive(buffer, obj, utf8Encoder); } } else { throw new RuntimeException("Cannot encode object: " + value); } return buffer; } private static ByteBuffer appendString( ByteBuffer buffer, String value, CharsetEncoder utf8Encoder) throws CharacterCodingException { CharBuffer valueBuffer = CharBuffer.wrap(value); ByteBuffer utf8String = utf8Encoder.encode(valueBuffer); int utf8StringLenBytes = utf8String.remaining(); BserIntegralEncodedSize utf8StringLenSize = getEncodedSize(utf8StringLenBytes); buffer = increaseBufferCapacityIfNeeded(buffer, 2 + utf8StringLenSize.size + utf8StringLenBytes); buffer.put(BSER_STRING); buffer = appendLongWithSize(buffer, utf8StringLenBytes, utf8StringLenSize); buffer.put(utf8String); return buffer; } private static ByteBuffer appendLong(ByteBuffer buffer, long value) { BserIntegralEncodedSize encodedSize = getEncodedSize(value); buffer = increaseBufferCapacityIfNeeded(buffer, 1 + encodedSize.size); return appendLongWithSize(buffer, value, encodedSize); } private static BserIntegralEncodedSize getEncodedSize(long value) { if (value >= -0x80 && value <= 0x7F) { return BserIntegralEncodedSize.INT8; } else if (value >= -0x8000 && value <= 0x7FFF) { return BserIntegralEncodedSize.INT16; } else if (value >= -0x80000000 && value <= 0x7FFFFFFF) { return BserIntegralEncodedSize.INT32; } else if (value >= -0x8000000000000000L && value <= 0x7FFFFFFFFFFFFFFFL) { return BserIntegralEncodedSize.INT64; } else { // We shouldn't be able to reach here. throw new RuntimeException("Unhandled long value: " + value); } } private static ByteBuffer appendLongWithSize( ByteBuffer buffer, long value, BserIntegralEncodedSize encodedSize) { // We assume we've already increased the size of the buffer to hold // the encoded size. switch (encodedSize) { case INT8: buffer.put(BSER_INT8); buffer.put((byte) value); break; case INT16: buffer.put(BSER_INT16); buffer.putShort((short) value); break; case INT32: buffer.put(BSER_INT32); buffer.putInt((int) value); break; case INT64: buffer.put(BSER_INT64); buffer.putLong(value); break; } return buffer; } private static ByteBuffer increaseBufferCapacityIfNeeded(ByteBuffer buffer, int amount) { int remaining = buffer.remaining(); if (remaining < amount) { int capacity = buffer.capacity(); while (remaining < amount) { remaining += capacity; capacity *= 2; } buffer = resizeBufferWithCapacity(buffer, capacity); } return buffer; } private static ByteBuffer resizeBufferWithCapacity(ByteBuffer buffer, int capacity) { buffer.flip(); return ByteBuffer.allocate(capacity).order(buffer.order()).put(buffer); } }