View Javadoc
1   /*
2    * Copyright 2013 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty.handler.codec.xml;
17  
18  import static io.netty.util.internal.ObjectUtil.checkPositive;
19  
20  import io.netty.buffer.ByteBuf;
21  import io.netty.channel.ChannelHandlerContext;
22  import io.netty.handler.codec.ByteToMessageDecoder;
23  import io.netty.handler.codec.CorruptedFrameException;
24  import io.netty.handler.codec.TooLongFrameException;
25  
26  import java.util.List;
27  
28  /**
29   * A frame decoder for single separate XML based message streams.
30   * <p/>
31   * A couple examples will better help illustrate
32   * what this decoder actually does.
33   * <p/>
34   * Given an input array of bytes split over 3 frames like this:
35   * <pre>
36   * +-----+-----+-----------+
37   * | &lt;an | Xml | Element/&gt; |
38   * +-----+-----+-----------+
39   * </pre>
40   * <p/>
41   * this decoder would output a single frame:
42   * <p/>
43   * <pre>
44   * +-----------------+
45   * | &lt;anXmlElement/&gt; |
46   * +-----------------+
47   * </pre>
48   *
49   * Given an input array of bytes split over 5 frames like this:
50   * <pre>
51   * +-----+-----+-----------+-----+----------------------------------+
52   * | &lt;an | Xml | Element/&gt; | &lt;ro | ot&gt;&lt;child&gt;content&lt;/child&gt;&lt;/root&gt; |
53   * +-----+-----+-----------+-----+----------------------------------+
54   * </pre>
55   * <p/>
56   * this decoder would output two frames:
57   * <p/>
58   * <pre>
59   * +-----------------+-------------------------------------+
60   * | &lt;anXmlElement/&gt; | &lt;root&gt;&lt;child&gt;content&lt;/child&gt;&lt;/root&gt; |
61   * +-----------------+-------------------------------------+
62   * </pre>
63   *
64   * <p/>
65   * The byte stream is expected to be in UTF-8 character encoding or ASCII. The current implementation
66   * uses direct {@code byte} to {@code char} cast and then compares that {@code char} to a few low range
67   * ASCII characters like {@code '<'}, {@code '>'} or {@code '/'}. UTF-8 is not using low range [0..0x7F]
68   * byte values for multibyte codepoint representations therefore fully supported by this implementation.
69   * <p/>
70   * Please note that this decoder is not suitable for
71   * xml streaming protocols such as
72   * <a href="https://xmpp.org/rfcs/rfc6120.html">XMPP</a>,
73   * where an initial xml element opens the stream and only
74   * gets closed at the end of the session, although this class
75   * could probably allow for such type of message flow with
76   * minor modifications.
77   */
78  public class XmlFrameDecoder extends ByteToMessageDecoder {
79  
80      private final int maxFrameLength;
81  
82      private boolean openingBracketFound;
83      private boolean atLeastOneXmlElementFound;
84      private boolean inCDATASection;
85      private boolean inCommentBlock;
86      private boolean inProcessingInstruction;
87      private boolean inClosingTag;
88      private long openBracketsCount;
89  
90      /**
91       * Index, relative to the current reader index, of the byte right after the last {@code '>'}
92       * seen so far. Netty's cumulation buffer may be compacted (rebasing both the reader index and
93       * the bytes) in between two {@link #decode(ChannelHandlerContext, ByteBuf, List)} invocations,
94       * which would silently invalidate an absolute buffer index stored across calls; storing this
95       * relative to the reader index keeps it valid across such compactions, just like {@link
96       * #scanOffset}.
97       */
98      private int length;
99      private int leadingWhiteSpaceCount;
100 
101     /**
102      * Index, relative to the current reader index, of the next byte that still needs to be scanned.
103      * This lets {@link #decode(ChannelHandlerContext, ByteBuf, List)} resume scanning where the
104      * previous invocation left off instead of rescanning the whole accumulated buffer every time,
105      * which would otherwise make a slowly trickled, never-balanced element run in quadratic time.
106      */
107     private int scanOffset;
108 
109     public XmlFrameDecoder(int maxFrameLength) {
110         this.maxFrameLength = checkPositive(maxFrameLength, "maxFrameLength");
111     }
112 
113     @Override
114     protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
115         final int bufferLength = in.writerIndex();
116 
117         if (bufferLength > maxFrameLength) {
118             // bufferLength exceeded maxFrameLength; dropping frame
119             in.skipBytes(in.readableBytes());
120             resetState();
121             fail(bufferLength);
122             return;
123         }
124 
125         final int readerIndex = in.readerIndex();
126         int i;
127         scan:
128         for (i = readerIndex + scanOffset; i < bufferLength; i++) {
129             final byte readByte = in.getByte(i);
130             if (!openingBracketFound && Character.isWhitespace(readByte)) {
131                 // xml has not started and whitespace char found
132                 leadingWhiteSpaceCount++;
133             } else if (!openingBracketFound && readByte != '<') {
134                 // garbage found before xml start
135                 fail(ctx);
136                 in.skipBytes(in.readableBytes());
137                 resetState();
138                 return;
139             } else if (inClosingTag && readByte == '<') {
140                 fail(ctx);
141                 in.skipBytes(in.readableBytes());
142                 resetState();
143                 return;
144             } else if (!inCDATASection && !inCommentBlock && !inProcessingInstruction && readByte == '<') {
145                 openingBracketFound = true;
146 
147                 if (i < bufferLength - 1) {
148                     final byte peekAheadByte = in.getByte(i + 1);
149                     if (peekAheadByte == '/') {
150                         // found </, we must check if it is enclosed
151                         inClosingTag = true;
152                     } else if (isValidStartCharForXmlElement(peekAheadByte)) {
153                         atLeastOneXmlElementFound = true;
154                         // char after < is a valid xml element start char,
155                         // incrementing openBracketsCount
156                         openBracketsCount++;
157                     } else if (peekAheadByte == '!') {
158                         final BangMarkupKind markupKind = classifyBangMarkup(in, i, bufferLength);
159                         switch (markupKind) {
160                             case INDETERMINATE:
161                                 // Not enough data has been received yet to tell whether this is
162                                 // a <!-- comment -->, a <![CDATA[ block, or some other markup
163                                 // declaration (e.g. <!DOCTYPE ...> or an unknown <!x>); wait for
164                                 // more bytes before resuming the scan from this position.
165                                 break scan;
166                             case COMMENT:
167                                 // <!-- comment --> start found
168                                 openBracketsCount++;
169                                 inCommentBlock = true;
170                                 break;
171                             case CDATA:
172                                 // <![CDATA[ start found
173                                 openBracketsCount++;
174                                 inCDATASection = true;
175                                 break;
176                             case NONE:
177                                 // some other markup declaration (e.g. <!DOCTYPE ...> or <!x>);
178                                 // treated as inert content, same as everything else between tags.
179                                 break;
180                             default:
181                                 throw new Error();
182                         }
183                     } else if (peekAheadByte == '?') {
184                         // <?xml ?> start found
185                         openBracketsCount++;
186                         inProcessingInstruction = true;
187                     }
188                 } else {
189                     // not enough data yet to peek at the byte following '<'; wait for more
190                     // and resume the scan from this same position.
191                     break;
192                 }
193             } else if (!inCDATASection && !inCommentBlock && !inProcessingInstruction && readByte == '/') {
194                 if (i < bufferLength - 1) {
195                     if (in.getByte(i + 1) == '>') {
196                         // found />, decrementing openBracketsCount
197                         openBracketsCount--;
198                     }
199                 } else {
200                     // not enough data yet to peek at the byte following '/'; wait for more
201                     // and resume the scan from this same position.
202                     break;
203                 }
204             } else if (readByte == '>') {
205                 length = i + 1 - readerIndex;
206 
207                 if (i - 1 > -1) {
208                     final byte peekBehindByte = in.getByte(i - 1);
209 
210                     if (inCommentBlock) {
211                         if (peekBehindByte == '-' && i - 2 > -1 && in.getByte(i - 2) == '-') {
212                             // a <!-- comment --> was closed
213                             openBracketsCount--;
214                             inCommentBlock = false;
215                         }
216                     } else if (inProcessingInstruction) {
217                         if (peekBehindByte == '?') {
218                             // an <?xml ?> tag was closed
219                             openBracketsCount--;
220                             inProcessingInstruction = false;
221                         }
222                     } else if (inClosingTag) {
223                         openBracketsCount--;
224                         inClosingTag = false;
225                     } else if (!inCDATASection) {
226                         if (peekBehindByte == '?') {
227                             // an <?xml ?> tag was closed
228                             openBracketsCount--;
229                         } else if (peekBehindByte == '-' && i - 2 > -1 && in.getByte(i - 2) == '-') {
230                             // a <!-- comment --> was closed
231                             openBracketsCount--;
232                         }
233                     } else if (inCDATASection && peekBehindByte == ']' && i - 2 > -1 && in.getByte(i - 2) == ']') {
234                         // a <![CDATA[...]]> block was closed
235                         openBracketsCount--;
236                         inCDATASection = false;
237                     }
238                 }
239 
240                 if (atLeastOneXmlElementFound && openBracketsCount == 0) {
241                     // xml is balanced, bailing out
242                     break;
243                 }
244             }
245         }
246 
247         int xmlElementLength = length;
248 
249         if (openBracketsCount == 0 && xmlElementLength > 0) {
250             if (readerIndex + xmlElementLength >= bufferLength) {
251                 xmlElementLength = in.readableBytes();
252             }
253             final ByteBuf frame =
254                     extractFrame(in, readerIndex + leadingWhiteSpaceCount, xmlElementLength - leadingWhiteSpaceCount);
255             in.skipBytes(xmlElementLength);
256             // a full element was extracted; reset all parser state (including the scan offset)
257             // so the next invocation starts scanning a fresh element from the current reader index.
258             resetState();
259             out.add(frame);
260         } else {
261             // no complete, balanced element yet; remember how far we scanned so the next
262             // invocation resumes from here instead of rescanning the whole buffer.
263             scanOffset = i - readerIndex;
264         }
265     }
266 
267     private void resetState() {
268         openingBracketFound = false;
269         atLeastOneXmlElementFound = false;
270         inCDATASection = false;
271         inCommentBlock = false;
272         inProcessingInstruction = false;
273         inClosingTag = false;
274         openBracketsCount = 0;
275         length = 0;
276         leadingWhiteSpaceCount = 0;
277         scanOffset = 0;
278     }
279 
280     private void fail(long frameLength) {
281         if (frameLength > 0) {
282             throw new TooLongFrameException(
283                             "frame length exceeds " + maxFrameLength + ": " + frameLength + " - discarded");
284         } else {
285             throw new TooLongFrameException(
286                             "frame length exceeds " + maxFrameLength + " - discarding");
287         }
288     }
289 
290     private static void fail(ChannelHandlerContext ctx) {
291         ctx.fireExceptionCaught(new CorruptedFrameException("frame contains content before the xml starts"));
292     }
293 
294     private static ByteBuf extractFrame(ByteBuf buffer, int index, int length) {
295         return buffer.copy(index, length);
296     }
297 
298     /**
299      * Asks whether the given byte is a valid
300      * start char for an xml element name.
301      * <p/>
302      * Please refer to the
303      * <a href="https://www.w3.org/TR/2004/REC-xml11-20040204/#NT-NameStartChar">NameStartChar</a>
304      * formal definition in the W3C XML spec for further info.
305      *
306      * @param b the input char
307      * @return true if the char is a valid start char
308      */
309     private static boolean isValidStartCharForXmlElement(final byte b) {
310         return b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b == ':' || b == '_';
311     }
312 
313     /**
314      * The outcome of classifying a {@code '<!'} construct against the {@code <!--} and
315      * {@code <![CDATA[} start markers.
316      */
317     private enum BangMarkupKind {
318         /** Not enough data has been received yet to determine the kind of construct. */
319         INDETERMINATE,
320         /** Neither a comment nor a CDATA start, e.g. {@code <!DOCTYPE ...>} or {@code <!x>}. */
321         NONE,
322         /** A {@code <!-- comment -->} start. */
323         COMMENT,
324         /** A {@code <![CDATA[} start. */
325         CDATA
326     }
327 
328     private static final byte[] CDATA_START_SUFFIX = {'C', 'D', 'A', 'T', 'A', '['};
329 
330     /**
331      * Classifies the markup declaration starting at {@code in.getByte(i) == '<'} followed by
332      * {@code '!'}, matching it incrementally against the {@code <!--} and {@code <![CDATA[}
333      * prefixes so a genuinely different declaration (e.g. {@code <!DOCTYPE ...>} or an unknown
334      * {@code <!x>}) is recognised as such immediately, without waiting for bytes that would only
335      * be needed to confirm a comment or CDATA start.
336      */
337     private static BangMarkupKind classifyBangMarkup(final ByteBuf in, final int i, final int bufferLength) {
338         final int commentOrCDATAMarker = i + 2;
339         if (commentOrCDATAMarker >= bufferLength) {
340             return BangMarkupKind.INDETERMINATE;
341         }
342         final byte b = in.getByte(commentOrCDATAMarker);
343         if (b == '-') {
344             final int secondDash = i + 3;
345             if (secondDash >= bufferLength) {
346                 return BangMarkupKind.INDETERMINATE;
347             }
348             return in.getByte(secondDash) == '-' ? BangMarkupKind.COMMENT : BangMarkupKind.NONE;
349         }
350         if (b == '[') {
351             for (int k = 0; k < CDATA_START_SUFFIX.length; k++) {
352                 final int idx = i + 3 + k;
353                 if (idx >= bufferLength) {
354                     return BangMarkupKind.INDETERMINATE;
355                 }
356                 if (in.getByte(idx) != CDATA_START_SUFFIX[k]) {
357                     return BangMarkupKind.NONE;
358                 }
359             }
360             return BangMarkupKind.CDATA;
361         }
362         return BangMarkupKind.NONE;
363     }
364 
365 }