nearenough.protocol.RtMessage.java Source code

Java tutorial

Introduction

Here is the source code for nearenough.protocol.RtMessage.java

Source

/*
 * Copyright (c) 2017 int08h LLC. All rights reserved.
 *
 * int08h LLC licenses Nearenough (the "Software") to you under the Apache License, version 2.0
 * (the "License"); you may not use this Software except in compliance with the License. You may
 * obtain a copy of the License from the LICENSE file included with the Software or 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 nearenough.protocol;

import static nearenough.util.Preconditions.checkNotNull;
import static nearenough.util.Preconditions.checkState;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufUtil;

import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import nearenough.protocol.exceptions.InvalidNumTagsException;
import nearenough.protocol.exceptions.MessageTooShortException;
import nearenough.protocol.exceptions.MessageUnalignedException;
import nearenough.protocol.exceptions.TagOffsetOverflowException;
import nearenough.protocol.exceptions.TagOffsetUnalignedException;
import nearenough.protocol.exceptions.TagsNotIncreasingException;

/**
 * An immutable Roughtime protocol message.
 * <p>
 * Roughtime messages are a map of uint32's to arbitrary byte-strings.
 *
 * @see <a href="https://roughtime.googlesource.com/roughtime">Roughtime project site</a> for the
 * specification and more details.
 */
public final class RtMessage {

    public static RtMessageBuilder builder() {
        return new RtMessageBuilder();
    }

    /**
     * @return A new {@code RtMessage} by parsing the contents of the provided {@code byte[]}. The
     * contents of {@code srcBytes} must be a well-formed Roughtime message.
     */
    public static RtMessage fromBytes(byte[] srcBytes) {
        checkNotNull(srcBytes, "srcBytes");

        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(srcBytes.length);
        buf.writeBytes(srcBytes);
        return new RtMessage(buf);
    }

    /**
     * @return A new {@code RtMessage} by parsing the contents of the provided {@code ByteBuffer},
     * which can be direct or heap based. The contents of {@code srcBuf} must be a well-formed
     * Roughtime message.
     */
    public static RtMessage fromByteBuffer(ByteBuffer srcBuf) {
        checkNotNull(srcBuf, "srcBuf");

        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(srcBuf.remaining());
        buf.writeBytes(srcBuf);
        return new RtMessage(buf);
    }

    private final int numTags;
    private final Map<RtTag, byte[]> map;

    /**
     * Create a new {@code RtMessage} by parsing the contents of the provided {@code ByteBuf}. The
     * contents of {@code msg} must be a well-formed Roughtime message.
     */
    public RtMessage(ByteBuf msg) {
        checkNotNull(msg, "msg");
        checkMessageLength(msg);

        this.numTags = extractNumTags(msg);

        if (numTags == 0) {
            this.map = Collections.emptyMap();
            return;
        }

        checkSufficientPayload(msg);

        if (numTags == 1) {
            this.map = extractSingleMapping(msg);
        } else {
            this.map = extractMultiMapping(msg);
        }
    }

    RtMessage(RtMessageBuilder builder) {
        this.map = builder.mapping();
        this.numTags = map.size();
    }

    /**
     * @return Number of protocol tags in this message
     */
    public int numTags() {
        return numTags;
    }

    /**
     * @return The byte-string associated with {@code tag}, or {@code null} if no mapping exists.
     */
    public byte[] get(RtTag tag) {
        return map.get(tag);
    }

    /**
     * @return The message's mapping.
     */
    public Map<RtTag, byte[]> mapping() {
        return map;
    }

    private int extractNumTags(ByteBuf msg) {
        long readNumTags = msg.readUnsignedIntLE();

        // Spec says max # tags can be 2^32-1, but capping at 64k tags in this implementation
        if (readNumTags > 0xffffL) {
            throw new InvalidNumTagsException("invalid num_tags value " + readNumTags);
        }

        return (int) readNumTags;
    }

    private Map<RtTag, byte[]> extractSingleMapping(ByteBuf msg) {
        long uintTag = msg.readUnsignedInt();
        RtTag tag = RtTag.fromUnsignedInt((int) uintTag);

        byte[] value = new byte[msg.readableBytes()];
        msg.readBytes(value);

        return Collections.singletonMap(tag, value);
    }

    private Map<RtTag, byte[]> extractMultiMapping(ByteBuf msg) {
        // extractOffsets will leave the reader index positioned at the first tag
        int[] offsets = extractOffsets(msg);

        int startOfValues = msg.readerIndex() + (4 * numTags);
        Map<RtTag, byte[]> mapping = new LinkedHashMap<>(numTags);
        RtTag prevTag = null;

        for (int i = 0; i < offsets.length; i++) {
            long uintCurrTag = msg.readUnsignedInt();
            RtTag currTag = RtTag.fromUnsignedInt((int) uintCurrTag);

            if ((prevTag != null) && currTag.isLessThan(prevTag)) {
                String exMsg = String.format(
                        "tags not strictly increasing: current '%s' (0x%08x), previous '%s' (0x%08x)", currTag,
                        currTag.valueLE(), prevTag, prevTag.valueLE());
                throw new TagsNotIncreasingException(exMsg);
            }

            int valueIdx = startOfValues + offsets[i];
            int valueLen = ((i + 1) == offsets.length) ? msg.readableBytes() - offsets[i]
                    : offsets[i + 1] - offsets[i];
            byte[] valueBytes = new byte[valueLen];
            msg.getBytes(valueIdx, valueBytes);

            mapping.put(currTag, valueBytes);
            prevTag = currTag;
        }

        return Collections.unmodifiableMap(mapping);
    }

    private int[] extractOffsets(ByteBuf msg) {
        int numOffsets = numTags - 1;
        int endOfPayload = msg.readableBytes() - (4 * numOffsets);

        // Offset for every tag, including the value of tag 0 which starts immediately after header.
        int[] offsets = new int[numTags];
        offsets[0] = 0;

        for (int i = 0; i < numOffsets; i++) {
            long offset = msg.readUnsignedIntLE();

            if ((offset % 4) != 0) {
                throw new TagOffsetUnalignedException("offset " + i + " not multiple of 4: " + offset);
            }
            if (offset > endOfPayload) {
                throw new TagOffsetOverflowException("offset " + i + " overflow: " + offset);
            }

            checkState((int) offset >= 0, "impossible, negative offset");
            offsets[i + 1] = (int) offset;
        }

        return offsets;
    }

    private void checkMessageLength(ByteBuf msg) {
        int readableBytes = msg.readableBytes();

        if (readableBytes < 4) {
            throw new MessageTooShortException("too short, <4 bytes total");
        }
        if ((readableBytes % 4) != 0) {
            throw new MessageUnalignedException("message length not multiple of 4: " + readableBytes);
        }
    }

    private void checkSufficientPayload(ByteBuf msg) {
        int expectedReadable = 4 * ((numTags - 1) + numTags);

        if (msg.readableBytes() < expectedReadable) {
            throw new MessageTooShortException("too short, insufficient length for numTags of " + numTags);
        }
    }

    @Override
    public String toString() {
        return toString(1);
    }

    public String toString(int indentLevel) {
        char[] indent1 = new char[2 * (indentLevel - 1)];
        char[] indent2 = new char[2 * indentLevel];
        Arrays.fill(indent1, ' ');
        Arrays.fill(indent2, ' ');

        StringBuilder sb = new StringBuilder("RtMessage|").append(numTags).append("|{\n");

        if (map != null) {
            map.forEach((tag, value) -> {
                sb.append(indent2).append(tag.name()).append("(").append(value.length).append(") = ");
                if (tag.isNested()) {
                    sb.append(fromBytes(value).toString(indentLevel + 1));
                } else {
                    sb.append(ByteBufUtil.hexDump(value)).append('\n');
                }
            });
        }
        sb.append(indent1).append("}\n");
        return sb.toString();
    }
}