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 * | <an | Xml | Element/> |
38 * +-----+-----+-----------+
39 * </pre>
40 * <p/>
41 * this decoder would output a single frame:
42 * <p/>
43 * <pre>
44 * +-----------------+
45 * | <anXmlElement/> |
46 * +-----------------+
47 * </pre>
48 *
49 * Given an input array of bytes split over 5 frames like this:
50 * <pre>
51 * +-----+-----+-----------+-----+----------------------------------+
52 * | <an | Xml | Element/> | <ro | ot><child>content</child></root> |
53 * +-----+-----+-----------+-----+----------------------------------+
54 * </pre>
55 * <p/>
56 * this decoder would output two frames:
57 * <p/>
58 * <pre>
59 * +-----------------+-------------------------------------+
60 * | <anXmlElement/> | <root><child>content</child></root> |
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 }