--- /dev/null
+/*
+ * Copyright (c) 2013 Cisco Systems, Inc. and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.netconf.nettyutil.handler;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.CompositeByteBuf;
+import io.netty.buffer.Unpooled;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.ByteToMessageDecoder;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class NetconfChunkAggregator extends ByteToMessageDecoder {
+ private static final Logger LOG = LoggerFactory.getLogger(NetconfChunkAggregator.class);
+ private static final String GOT_PARAM_WHILE_WAITING_FOR_PARAM = "Got byte {} while waiting for {}";
+ private static final String GOT_PARAM_WHILE_WAITING_FOR_PARAM_PARAM = "Got byte {} while waiting for {}-{}";
+ public static final int DEFAULT_MAXIMUM_CHUNK_SIZE = 16 * 1024 * 1024;
+
+ private enum State {
+ HEADER_ONE, // \n
+ HEADER_TWO, // #
+ HEADER_LENGTH_FIRST, // [1-9]
+ HEADER_LENGTH_OTHER, // [0-9]*\n
+ DATA,
+ FOOTER_ONE, // \n
+ FOOTER_TWO, // #
+ FOOTER_THREE, // #
+ FOOTER_FOUR, // \n
+ }
+
+ private final int maxChunkSize = DEFAULT_MAXIMUM_CHUNK_SIZE;
+ private State state = State.HEADER_ONE;
+ private long chunkSize;
+ private CompositeByteBuf chunk;
+
+ private static void checkNewLine(final byte byteToCheck, final String errorMessage) {
+ if (byteToCheck != '\n') {
+ LOG.debug(GOT_PARAM_WHILE_WAITING_FOR_PARAM, byteToCheck, (byte)'\n');
+ throw new IllegalStateException(errorMessage);
+ }
+ }
+
+ private static void checkHash(final byte byteToCheck, final String errorMessage) {
+ if (byteToCheck != '#') {
+ LOG.debug(GOT_PARAM_WHILE_WAITING_FOR_PARAM, byteToCheck, (byte)'#');
+ throw new IllegalStateException(errorMessage);
+ }
+ }
+
+ private void checkChunkSize() {
+ if (chunkSize > maxChunkSize) {
+ LOG.debug("Parsed chunk size {}, maximum allowed is {}", chunkSize, maxChunkSize);
+ throw new IllegalStateException("Maximum chunk size exceeded");
+ }
+ }
+
+ @Override
+ protected void decode(final ChannelHandlerContext ctx,
+ final ByteBuf in, final List<Object> out) throws IllegalStateException {
+ while (in.isReadable()) {
+ switch (state) {
+ case HEADER_ONE:
+ {
+ final byte b = in.readByte();
+ checkNewLine(b, "Malformed chunk header encountered (byte 0)");
+ state = State.HEADER_TWO;
+ initChunk();
+ break;
+ }
+ case HEADER_TWO:
+ {
+ final byte b = in.readByte();
+ checkHash(b, "Malformed chunk header encountered (byte 1)");
+ state = State.HEADER_LENGTH_FIRST;
+ break;
+ }
+ case HEADER_LENGTH_FIRST:
+ {
+ final byte b = in.readByte();
+ chunkSize = processHeaderLengthFirst(b);
+ state = State.HEADER_LENGTH_OTHER;
+ break;
+ }
+ case HEADER_LENGTH_OTHER:
+ {
+ final byte b = in.readByte();
+ if (b == '\n') {
+ state = State.DATA;
+ break;
+ }
+ if (b < '0' || b > '9') {
+ LOG.debug(GOT_PARAM_WHILE_WAITING_FOR_PARAM_PARAM, b, (byte)'0', (byte)'9');
+ throw new IllegalStateException("Invalid chunk size encountered");
+ }
+ chunkSize *= 10;
+ chunkSize += b - '0';
+ checkChunkSize();
+ break;
+ }
+ case DATA:
+ /*
+ * FIXME: this gathers all data into one big chunk before passing
+ * it on. Make sure the pipeline can work with partial data
+ * and then change this piece to pass the data on as it
+ * comes through.
+ */
+ if (in.readableBytes() < chunkSize) {
+ LOG.debug("Buffer has {} bytes, need {} to complete chunk", in.readableBytes(), chunkSize);
+ in.discardReadBytes();
+ return;
+ }
+ aggregateChunks(in.readBytes((int) chunkSize));
+ state = State.FOOTER_ONE;
+ break;
+ case FOOTER_ONE:
+ {
+ final byte b = in.readByte();
+ checkNewLine(b,"Malformed chunk footer encountered (byte 0)");
+ state = State.FOOTER_TWO;
+ chunkSize = 0;
+ break;
+ }
+ case FOOTER_TWO:
+ {
+ final byte b = in.readByte();
+ checkHash(b,"Malformed chunk footer encountered (byte 1)");
+ state = State.FOOTER_THREE;
+ break;
+ }
+ case FOOTER_THREE:
+ {
+ final byte b = in.readByte();
+ // In this state, either header-of-new-chunk or message-end is expected
+ // Depends on the next character
+ extractNewChunkOrMessageEnd(b);
+ break;
+ }
+ case FOOTER_FOUR:
+ {
+ final byte b = in.readByte();
+ checkNewLine(b,"Malformed chunk footer encountered (byte 3)");
+ state = State.HEADER_ONE;
+ out.add(chunk);
+ chunk = null;
+ break;
+ }
+ default :
+ {
+ LOG.info("Unknown state.");
+ }
+ }
+ }
+
+ in.discardReadBytes();
+ }
+
+ private void extractNewChunkOrMessageEnd(final byte byteToCheck) {
+ if (isHeaderLengthFirst(byteToCheck)) {
+ // Extract header length#1 from new chunk
+ chunkSize = processHeaderLengthFirst(byteToCheck);
+ // Proceed with next chunk processing
+ state = State.HEADER_LENGTH_OTHER;
+ } else if (byteToCheck == '#') {
+ state = State.FOOTER_FOUR;
+ } else {
+ LOG.debug(GOT_PARAM_WHILE_WAITING_FOR_PARAM_PARAM, byteToCheck, (byte) '#', (byte) '1', (byte) '9');
+ throw new IllegalStateException("Malformed chunk footer encountered (byte 2)");
+ }
+ }
+
+ private void initChunk() {
+ chunk = Unpooled.compositeBuffer();
+ }
+
+ private void aggregateChunks(final ByteBuf newChunk) {
+ chunk.addComponent(chunk.numComponents(), newChunk);
+
+ // Update writer index, addComponent does not update it
+ chunk.writerIndex(chunk.writerIndex() + newChunk.readableBytes());
+ }
+
+ private static int processHeaderLengthFirst(final byte byteToCheck) {
+ if (!isHeaderLengthFirst(byteToCheck)) {
+ LOG.debug(GOT_PARAM_WHILE_WAITING_FOR_PARAM_PARAM, byteToCheck, (byte)'1', (byte)'9');
+ throw new IllegalStateException("Invalid chunk size encountered (byte 0)");
+ }
+
+ return byteToCheck - '0';
+ }
+
+ private static boolean isHeaderLengthFirst(final byte byteToCheck) {
+ return byteToCheck >= '1' && byteToCheck <= '9';
+ }
+}