diff --git a/codec/src/main/java/io/netty/handler/codec/xml/XmlFrameDecoder.java b/codec/src/main/java/io/netty/handler/codec/xml/XmlFrameDecoder.java new file mode 100644 index 0000000000..b3d5e2fb3e --- /dev/null +++ b/codec/src/main/java/io/netty/handler/codec/xml/XmlFrameDecoder.java @@ -0,0 +1,203 @@ +/* + * Copyright 2013 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.handler.codec.xml; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.handler.codec.TooLongFrameException; +import io.netty.util.CharsetUtil; + +import java.util.List; + +/** + * A frame decoder for single separate XML based message streams. + *

+ * A couple examples will better help illustrate + * what this decoder actually does. + *

+ * Given an input array of bytes split over 3 frames like this: + *

+ * +-----+-----+-----------+
+ * | <an | Xml | Element/> |
+ * +-----+-----+-----------+
+ * 
+ *

+ * this decoder would output a single frame: + *

+ *

+ * +-----------------+
+ * | <anXmlElement/> |
+ * +-----------------+
+ * 
+ * + * Given an input array of bytes split over 5 frames like this: + *
+ * +-----+-----+-----------+-----+----------------------------------+
+ * | <an | Xml | Element/> | <ro | ot><child>content</child></root> |
+ * +-----+-----+-----------+-----+----------------------------------+
+ * 
+ *

+ * this decoder would output two frames: + *

+ *

+ * +-----------------+-------------------------------------+
+ * | <anXmlElement/> | <root><child>content</child></root> |
+ * +-----------------+-------------------------------------+
+ * 
+ * + * Please note that this decoder is not suitable for + * xml streaming protocols such as + * XMPP, + * where an initial xml element opens the stream and only + * gets closed at the end of the session, although this class + * could probably allow for such type of message flow with + * minor modifications. + */ +public class XmlFrameDecoder extends ByteToMessageDecoder { + + private final int maxFrameLength; + + public XmlFrameDecoder(int maxFrameLength) { + if (maxFrameLength < 1) { + throw new IllegalArgumentException("maxFrameLength must be a positive int"); + } + this.maxFrameLength = maxFrameLength; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + boolean openingBracketFound = false; + boolean atLeastOneXmlElementFound = false; + long openBracketsCount = 0; + int length = 0; + int leadingWhiteSpaceCount = 0; + final int bufferLength = in.writerIndex(); + + if (bufferLength > maxFrameLength) { + // bufferLength exceeded maxFrameLength; dropping frame + fail(ctx, bufferLength); + in.skipBytes(in.readableBytes()); + return; + } + + for (int i = in.readerIndex(); i < bufferLength; i++) { + final byte readByte = in.getByte(i); + if (!openingBracketFound && Character.isWhitespace(readByte)) { + // xml has not started and whitespace char found + leadingWhiteSpaceCount ++; + } else if (!openingBracketFound && readByte != '<') { + // garbage found before xml start + fail(ctx); + in.skipBytes(in.readableBytes()); + return; + } else if (readByte == '<') { + openingBracketFound = true; + + if (i < bufferLength - 1) { + final byte peekAheadByte = in.getByte(i + 1); + if (peekAheadByte == '/') { + // found start found + openBracketsCount++; + } else if (peekAheadByte == '?') { + // start found + openBracketsCount++; + } + } + } else if (readByte == '/') { + if (i < bufferLength - 1 && in.getByte(i + 1) == '>') { + // found />, decrementing openBracketsCount + openBracketsCount--; + } + } else if (readByte == '>') { + length = i + 1; + + if (i - 1 > -1) { + final byte peekBehindByte = in.getByte(i - 1); + + if (peekBehindByte == '?') { + // an tag was closed + openBracketsCount--; + } else if (peekBehindByte == '-' && i - 2 > -1 && in.getByte(i - 2) == '-') { + // a was closed + openBracketsCount--; + } + } + + if (openingBracketFound && atLeastOneXmlElementFound && openBracketsCount == 0) { + // xml is balanced, bailing out + break; + } + } + } + + final int readerIndex = in.readerIndex(); + + if (openBracketsCount == 0 && length > 0) { + final ByteBuf frame = + extractFrame(in, readerIndex + leadingWhiteSpaceCount, length - leadingWhiteSpaceCount); + System.err.println(in + ", " + frame + ", " + frame.toString(CharsetUtil.UTF_8)); + in.skipBytes(length); + out.add(frame); + } + } + + private void fail(ChannelHandlerContext ctx, long frameLength) { + if (frameLength > 0) { + ctx.fireExceptionCaught( + new TooLongFrameException( + "frame length exceeds " + maxFrameLength + ": " + frameLength + " - discarded")); + } else { + ctx.fireExceptionCaught( + new TooLongFrameException( + "frame length exceeds " + maxFrameLength + " - discarding")); + } + } + + private static void fail(ChannelHandlerContext ctx) { + ctx.fireExceptionCaught(new CorruptedFrameException("frame contains content before the xml starts")); + } + + private static ByteBuf extractFrame(ByteBuf buffer, int index, int length) { + return buffer.copy(index, length); + } + + /** + * Asks whether the given byte is a valid + * start char for an xml element name. + *

+ * Please refer to the + * NameStartChar + * formal definition in the W3C XML spec for further info. + * + * @param b the input char + * @return true if the char is a valid start char + */ + private static boolean isValidStartCharForXmlElement(final byte b) { + return b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b == ':' || b == '_'; + } +} diff --git a/codec/src/test/java/io/netty/handler/codec/xml/XmlFrameDecoderTest.java b/codec/src/test/java/io/netty/handler/codec/xml/XmlFrameDecoderTest.java new file mode 100644 index 0000000000..835213ca0e --- /dev/null +++ b/codec/src/test/java/io/netty/handler/codec/xml/XmlFrameDecoderTest.java @@ -0,0 +1,785 @@ +/* + * Copyright 2013 The Netty Project + * + * The Netty Project licenses this file to you 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 io.netty.handler.codec.xml; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.handler.codec.TooLongFrameException; +import io.netty.util.CharsetUtil; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.*; + +public class XmlFrameDecoderTest { + + private final List xmlSamples = + Arrays.asList(SAMPLE_01, SAMPLE_02, SAMPLE_03, SAMPLE_04); + + @Test(expected = IllegalArgumentException.class) + public void testConstructorWithIllegalArgs01() { + new XmlFrameDecoder(0); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorWithIllegalArgs02() { + new XmlFrameDecoder(-23); + } + + @Test(expected = TooLongFrameException.class) + public void testDecodeWithFrameExceedingMaxLength() { + XmlFrameDecoder decoder = new XmlFrameDecoder(3); + EmbeddedChannel ch = new EmbeddedChannel(decoder); + ch.writeInbound(Unpooled.copiedBuffer("", CharsetUtil.UTF_8)); + } + + @Test(expected = CorruptedFrameException.class) + public void testDecodeWithInvalidInput() { + XmlFrameDecoder decoder = new XmlFrameDecoder(1048576); + EmbeddedChannel ch = new EmbeddedChannel(decoder); + ch.writeInbound(Unpooled.copiedBuffer("invalid XML", CharsetUtil.UTF_8)); + } + + @Test(expected = CorruptedFrameException.class) + public void testDecodeWithInvalidContentBeforeXml() { + XmlFrameDecoder decoder = new XmlFrameDecoder(1048576); + EmbeddedChannel ch = new EmbeddedChannel(decoder); + ch.writeInbound(Unpooled.copiedBuffer("invalid XML", CharsetUtil.UTF_8)); + } + + @Test + public void testDecodeShortValidXml() { + testDecodeWithXml("", ""); + } + + @Test + public void testDecodeShortValidXmlWithLeadingWhitespace01() { + testDecodeWithXml(" ", ""); + } + + @Test + public void testDecodeShortValidXmlWithLeadingWhitespace02() { + testDecodeWithXml(" \n\r \t\t", ""); + } + + @Test + public void testDecodeShortValidXmlWithLeadingWhitespace02AndTrailingGarbage() { + testDecodeWithXml(" \n\r \t\ttrash", "", CorruptedFrameException.class); + } + + @Test + public void testDecodeWithTwoMessages() { + testDecodeWithXml( + "\n" + + '\n' + + "\n" + + "\n" + + "", + "", + "\n" + + "\n" + + "" + ); + } + + @Test + public void testDecodeWithSampleXml() { + for (final String xmlSample : xmlSamples) { + testDecodeWithXml(xmlSample, xmlSample); + } + } + + private static void testDecodeWithXml(String xml, Object... expected) { + EmbeddedChannel ch = new EmbeddedChannel(new XmlFrameDecoder(1048576)); + Exception cause = null; + try { + ch.writeInbound(Unpooled.copiedBuffer(xml, CharsetUtil.UTF_8)); + } catch (Exception e) { + cause = e; + e.printStackTrace(); + } + List actual = new ArrayList(); + for (;;) { + ByteBuf buf = (ByteBuf) ch.readInbound(); + if (buf == null) { + break; + } + actual.add(buf.toString(CharsetUtil.UTF_8)); + } + + if (cause != null) { + actual.add(cause.getClass()); + } + + List expectedList = new ArrayList(); + Collections.addAll(expectedList, expected); + assertThat(actual, is(expectedList)); + } + + private static final String SAMPLE_01 = ""; + + private static final String SAMPLE_02 = "\n" + + "\n" + + ""; + + private static final String SAMPLE_03 = "\n" + + "\n" + + "\n" + + '\n' + + " 4.0.0\n" + + " \n" + + " org.sonatype.oss\n" + + " oss-parent\n" + + " 7\n" + + " \n" + + '\n' + + " io.netty\n" + + " netty-parent\n" + + " pom\n" + + " 4.0.0.Beta3-SNAPSHOT\n" + + '\n' + + " Netty\n" + + " http://netty.io/\n" + + " \n" + + " Netty is an asynchronous event-driven network application framework for \n" + + " rapid development of maintainable high performance protocol servers and\n" + + " clients.\n" + + " \n" + + '\n' + + " \n" + + " The Netty Project\n" + + " http://netty.io/\n" + + " \n" + + '\n' + + " \n" + + " \n" + + " Apache License, Version 2.0\n" + + " http://www.apache.org/licenses/LICENSE-2.0\n" + + " \n" + + " \n" + + " 2008\n" + + '\n' + + " \n" + + " https://github.com/netty/netty\n" + + " scm:git:git://github.com/netty/netty.git\n" + + " scm:git:ssh://git@github.com/netty/netty.git\n" + + " HEAD\n" + + " \n" + + '\n' + + " \n" + + " \n" + + " netty.io\n" + + " The Netty Project Contributors\n" + + " netty@googlegroups.com\n" + + " http://netty.io/\n" + + " The Netty Project\n" + + " http://netty.io/\n" + + " \n" + + " \n" + + '\n' + + " \n" + + " UTF-8\n" + + " UTF-8\n" + + " 1.3.14.GA\n" + + " \n" + + " \n" + + " \n" + + " common\n" + + " buffer\n" + + " codec\n" + + " codec-http\n" + + " codec-socks\n" + + " transport\n" + + " transport-rxtx\n" + + " transport-sctp\n" + + " transport-udt\n" + + " handler\n" + + " example\n" + + " testsuite\n" + + " testsuite-osgi\n" + + " microbench\n" + + " all\n" + + " tarball\n" + + " \n" + + '\n' + + " \n" + + " \n" + + " \n" + + " \n" + + " org.jboss.marshalling\n" + + " jboss-marshalling\n" + + " ${jboss.marshalling.version}\n" + + " compile\n" + + " true\n" + + " \n" + + " \n" + + " \n" + + " com.google.protobuf\n" + + " protobuf-java\n" + + " 2.4.1\n" + + " \n" + + " \n" + + " com.jcraft\n" + + " jzlib\n" + + " 1.1.2\n" + + " \n" + + '\n' + + " \n" + + " org.rxtx\n" + + " rxtx\n" + + " 2.1.7\n" + + " \n" + + '\n' + + " \n" + + " com.barchart.udt\n" + + " barchart-udt-bundle\n" + + " 2.2.2\n" + + " \n" + + '\n' + + " \n" + + " javax.servlet\n" + + " servlet-api\n" + + " 2.5\n" + + " \n" + + '\n' + + " \n" + + " org.slf4j\n" + + " slf4j-api\n" + + " 1.7.2\n" + + " \n" + + " \n" + + " commons-logging\n" + + " commons-logging\n" + + " 1.1.1\n" + + " \n" + + " \n" + + " log4j\n" + + " log4j\n" + + " 1.2.17\n" + + " \n" + + " \n" + + " mail\n" + + " javax.mail\n" + + " \n" + + " \n" + + " jms\n" + + " javax.jms\n" + + " \n" + + " \n" + + " jmxtools\n" + + " com.sun.jdmk\n" + + " \n" + + " \n" + + " jmxri\n" + + " com.sun.jmx\n" + + " \n" + + " \n" + + " true\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " com.yammer.metrics\n" + + " metrics-core\n" + + " 2.2.0\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " org.jboss.marshalling\n" + + " jboss-marshalling-serial\n" + + " ${jboss.marshalling.version}\n" + + " test\n" + + " \n" + + " \n" + + " org.jboss.marshalling\n" + + " jboss-marshalling-river\n" + + " ${jboss.marshalling.version}\n" + + " test\n" + + " \n" + + '\n' + + " \n" + + " \n" + + " com.google.caliper\n" + + " caliper\n" + + " 0.5-rc1\n" + + " test\n" + + " \n" + + " \n" + + " \n" + + '\n' + + " \n" + + " \n" + + " \n" + + " org.javassist\n" + + " javassist\n" + + " 3.17.1-GA\n" + + " compile\n" + + " true\n" + + " \n" + + '\n' + + " \n" + + " \n" + + " junit\n" + + " junit\n" + + " 4.10\n" + + " test\n" + + " \n" + + " \n" + + " \n" + + " org.hamcrest\n" + + " hamcrest-core\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " org.hamcrest\n" + + " hamcrest-library\n" + + " 1.3\n" + + " test\n" + + " \n" + + " \n" + + " org.easymock\n" + + " easymock\n" + + " 3.1\n" + + " test\n" + + " \n" + + " \n" + + " org.easymock\n" + + " easymockclassextension\n" + + " 3.1\n" + + " test\n" + + " \n" + + " \n" + + " org.jmock\n" + + " jmock-junit4\n" + + " 2.5.1\n" + + " test\n" + + " \n" + + " \n" + + " ch.qos.logback\n" + + " logback-classic\n" + + " 1.0.9\n" + + " test\n" + + " \n" + + " \n" + + '\n' + + " \n" + + " \n" + + " \n" + + " maven-enforcer-plugin\n" + + " 1.1\n" + + " \n" + + " \n" + + " enforce-tools\n" + + " \n" + + " enforce\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " [1.7.0,)\n" + + " \n" + + " \n" + + " [3.0.5,)\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " maven-compiler-plugin\n" + + " 2.5.1\n" + + " \n" + + " 1.7\n" + + " true\n" + + " 1.6\n" + + " 1.6\n" + + " true\n" + + " true\n" + + " true\n" + + " true\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " org.codehaus.mojo\n" + + " animal-sniffer-maven-plugin\n" + + " 1.8\n" + + " \n" + + " \n" + + " org.codehaus.mojo.signature\n" + + " java16\n" + + " 1.0\n" + + " \n" + + " \n" + + " sun.misc.Unsafe\n" + + " sun.misc.Cleaner\n" + + '\n' + + " java.util.zip.Deflater\n" + + '\n' + + " \n" + + " java.nio.channels.DatagramChannel\n" + + " java.nio.channels.MembershipKey\n" + + " java.net.StandardProtocolFamily\n" + + '\n' + + " \n" + + " java.nio.channels.AsynchronousChannel\n" + + " java.nio.channels.AsynchronousSocketChannel\n" + + " java.nio.channels.AsynchronousServerSocketChannel\n" + + " java.nio.channels.AsynchronousChannelGroup\n" + + " java.nio.channels.NetworkChannel\n" + + " java.nio.channels.InterruptedByTimeoutException\n" + + " java.net.StandardSocketOptions\n" + + " java.net.SocketOption\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " process-classes\n" + + " \n" + + " check\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " maven-checkstyle-plugin\n" + + " 2.9.1\n" + + " \n" + + " \n" + + " check-style\n" + + " \n" + + " check\n" + + " \n" + + " validate\n" + + " \n" + + " true\n" + + " true\n" + + " true\n" + + " true\n" + + " io/netty/checkstyle.xml\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " ${project.groupId}\n" + + " netty-build\n" + + " 17\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " maven-surefire-plugin\n" + + " \n" + + " \n" + + " **/*Test*.java\n" + + " **/*Benchmark*.java\n" + + " \n" + + " \n" + + " **/Abstract*\n" + + " **/TestUtil*\n" + + " \n" + + " random\n" + + " \n" + + " -server \n" + + " -Dio.netty.resourceLeakDetection\n" + + " -dsa -da -ea:io.netty...\n" + + " -XX:+AggressiveOpts\n" + + " -XX:+TieredCompilation\n" + + " -XX:+UseBiasedLocking\n" + + " -XX:+UseFastAccessorMethods\n" + + " -XX:+UseStringCache\n" + + " -XX:+OptimizeStringConcat\n" + + " -XX:+HeapDumpOnOutOfMemoryError\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " org.apache.felix\n" + + " maven-bundle-plugin\n" + + " 2.3.7\n" + + " true\n" + + " \n" + + " \n" + + " maven-source-plugin\n" + + " 2.1.2\n" + + " \n" + + " \n" + + " attach-sources\n" + + " \n" + + " jar\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " maven-javadoc-plugin\n" + + " 2.8.1\n" + + " \n" + + " false\n" + + " true\n" + + " false\n" + + " false\n" + + " true\n" + + " \n" + + " \n" + + " \n" + + " maven-deploy-plugin\n" + + " 2.7\n" + + " \n" + + " 10\n" + + " \n" + + " \n" + + " \n" + + " maven-release-plugin\n" + + " 2.3.2\n" + + " \n" + + " false\n" + + " -P release,sonatype-oss-release,full\n" + + " true\n" + + " true\n" + + " netty-@{project.version}\n" + + " \n" + + " \n" + + " \n" + + " \n" + + '\n' + + " \n" + + " \n" + + " \n" + + " \n" + + " maven-surefire-plugin\n" + + " 2.12\n" + + " \n" + + " \n" + + " \n" + + " maven-failsafe-plugin\n" + + " 2.12\n" + + " \n" + + " \n" + + " maven-clean-plugin\n" + + " 2.5\n" + + " \n" + + " \n" + + " maven-resources-plugin\n" + + " 2.5\n" + + " \n" + + " \n" + + " maven-jar-plugin\n" + + " 2.4\n" + + " \n" + + " \n" + + " maven-dependency-plugin\n" + + " 2.4\n" + + " \n" + + " \n" + + " maven-assembly-plugin\n" + + " 2.3\n" + + " \n" + + " \n" + + " maven-jxr-plugin\n" + + " 2.2\n" + + " \n" + + " \n" + + " maven-antrun-plugin\n" + + " 1.7\n" + + " \n" + + " \n" + + " ant-contrib\n" + + " ant-contrib\n" + + " 1.0b3\n" + + " \n" + + " \n" + + " ant\n" + + " ant\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " org.codehaus.mojo\n" + + " build-helper-maven-plugin\n" + + " 1.7\n" + + " \n" + + '\n' + + " \n" + + " \n" + + " org.eclipse.m2e\n" + + " lifecycle-mapping\n" + + " 1.0.0\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " org.apache.maven.plugins\n" + + " maven-checkstyle-plugin\n" + + " [1.0,)\n" + + " \n" + + " check\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " false\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " org.apache.maven.plugins\n" + + " maven-enforcer-plugin\n" + + " [1.0,)\n" + + " \n" + + " enforce\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " false\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " org.apache.maven.plugins\n" + + " maven-clean-plugin\n" + + " [1.0,)\n" + + " \n" + + " clean\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " false\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + private static final String SAMPLE_04 = "\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + +}