1 /*
2 * Copyright 2014 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
17 package io.netty.handler.codec.json;
18
19 import static io.netty.util.internal.ObjectUtil.checkPositive;
20
21 import io.netty.buffer.ByteBuf;
22 import io.netty.buffer.ByteBufUtil;
23 import io.netty.channel.ChannelHandlerContext;
24 import io.netty.handler.codec.ByteToMessageDecoder;
25 import io.netty.handler.codec.CorruptedFrameException;
26 import io.netty.handler.codec.TooLongFrameException;
27 import io.netty.channel.ChannelPipeline;
28
29 import java.util.List;
30
31 /**
32 * Splits a byte stream of JSON objects and arrays into individual objects/arrays and passes them up the
33 * {@link ChannelPipeline}.
34 * <p>
35 * The byte stream is expected to be in UTF-8 character encoding or ASCII. The current implementation
36 * uses direct {@code byte} to {@code char} cast and then compares that {@code char} to a few low range
37 * ASCII characters like {@code '{'}, {@code '['} or {@code '"'}. UTF-8 is not using low range [0..0x7F]
38 * byte values for multibyte codepoint representations therefore fully supported by this implementation.
39 * <p>
40 * This class does not do any real parsing or validation. A sequence of bytes is considered a JSON object/array
41 * if it contains a matching number of opening and closing braces/brackets. It's up to a subsequent
42 * {@link ChannelHandler} to parse the JSON text into a more usable form i.e. a POJO.
43 */
44 public class JsonObjectDecoder extends ByteToMessageDecoder {
45
46 private static final int ST_CORRUPTED = -1;
47 private static final int ST_INIT = 0;
48 private static final int ST_DECODING_NORMAL = 1;
49 private static final int ST_DECODING_ARRAY_STREAM = 2;
50
51 private int openBraces;
52 private int idx;
53
54 private int lastReaderIndex;
55
56 private int state;
57 private boolean insideString;
58
59 private final int maxObjectLength;
60 private final boolean streamArrayElements;
61
62 public JsonObjectDecoder() {
63 // 1 MB
64 this(1024 * 1024);
65 }
66
67 public JsonObjectDecoder(int maxObjectLength) {
68 this(maxObjectLength, false);
69 }
70
71 public JsonObjectDecoder(boolean streamArrayElements) {
72 this(1024 * 1024, streamArrayElements);
73 }
74
75 /**
76 * @param maxObjectLength maximum number of bytes a JSON object/array may use (including braces and all).
77 * Objects exceeding this length are dropped and an {@link TooLongFrameException}
78 * is thrown.
79 * @param streamArrayElements if set to true and the "top level" JSON object is an array, each of its entries
80 * is passed through the pipeline individually and immediately after it was fully
81 * received, allowing for arrays with "infinitely" many elements.
82 *
83 */
84 public JsonObjectDecoder(int maxObjectLength, boolean streamArrayElements) {
85 this.maxObjectLength = checkPositive(maxObjectLength, "maxObjectLength");
86 this.streamArrayElements = streamArrayElements;
87 }
88
89 @Override
90 protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
91 if (state == ST_CORRUPTED) {
92 in.skipBytes(in.readableBytes());
93 return;
94 }
95
96 if (this.idx > in.readerIndex() && lastReaderIndex != in.readerIndex()) {
97 this.idx = in.readerIndex() + (idx - lastReaderIndex);
98 }
99
100 // index of next byte to process.
101 int idx = this.idx;
102 int wrtIdx = in.writerIndex();
103
104 if (wrtIdx > maxObjectLength) {
105 // buffer size exceeded maxObjectLength; discarding the complete buffer.
106 in.skipBytes(in.readableBytes());
107 reset();
108 throw new TooLongFrameException(
109 "object length exceeds " + maxObjectLength + ": " + wrtIdx + " bytes discarded");
110 }
111
112 for (/* use current idx */; idx < wrtIdx; idx++) {
113 byte c = in.getByte(idx);
114 if (state == ST_DECODING_NORMAL) {
115 decodeByte(c, in, idx);
116
117 // All opening braces/brackets have been closed. That's enough to conclude
118 // that the JSON object/array is complete.
119 if (openBraces == 0) {
120 ByteBuf json = extractObject(ctx, in, in.readerIndex(), idx + 1 - in.readerIndex());
121 if (json != null) {
122 out.add(json);
123 }
124
125 // The JSON object/array was extracted => discard the bytes from
126 // the input buffer.
127 in.readerIndex(idx + 1);
128 // Reset the object state to get ready for the next JSON object/text
129 // coming along the byte stream.
130 reset();
131 }
132 } else if (state == ST_DECODING_ARRAY_STREAM) {
133 decodeByte(c, in, idx);
134
135 if (!insideString && (openBraces == 1 && c == ',' || openBraces == 0 && c == ']')) {
136 // skip leading spaces. No range check is needed and the loop will terminate
137 // because the byte at position idx is not a whitespace.
138 for (int i = in.readerIndex(); Character.isWhitespace(in.getByte(i)); i++) {
139 in.skipBytes(1);
140 }
141
142 // skip trailing spaces.
143 int idxNoSpaces = idx - 1;
144 while (idxNoSpaces >= in.readerIndex() && Character.isWhitespace(in.getByte(idxNoSpaces))) {
145 idxNoSpaces--;
146 }
147
148 ByteBuf json = extractObject(ctx, in, in.readerIndex(), idxNoSpaces + 1 - in.readerIndex());
149 if (json != null) {
150 out.add(json);
151 }
152
153 in.readerIndex(idx + 1);
154
155 if (c == ']') {
156 reset();
157 }
158 }
159 // JSON object/array detected. Accumulate bytes until all braces/brackets are closed.
160 } else if (c == '{' || c == '[') {
161 initDecoding(c);
162
163 if (state == ST_DECODING_ARRAY_STREAM) {
164 // Discard the array bracket
165 in.skipBytes(1);
166 }
167 // Discard leading spaces in front of a JSON object/array.
168 } else if (Character.isWhitespace(c)) {
169 in.skipBytes(1);
170 } else {
171 state = ST_CORRUPTED;
172 throw new CorruptedFrameException(
173 "invalid JSON received at byte position " + idx + ": " + ByteBufUtil.hexDump(in));
174 }
175 }
176
177 if (in.readableBytes() == 0) {
178 this.idx = 0;
179 } else {
180 this.idx = idx;
181 }
182 this.lastReaderIndex = in.readerIndex();
183 }
184
185 /**
186 * Override this method if you want to filter the json objects/arrays that get passed through the pipeline.
187 */
188 @SuppressWarnings("UnusedParameters")
189 protected ByteBuf extractObject(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) {
190 return buffer.retainedSlice(index, length);
191 }
192
193 private void decodeByte(byte c, ByteBuf in, int idx) {
194 if ((c == '{' || c == '[') && !insideString) {
195 openBraces++;
196 } else if ((c == '}' || c == ']') && !insideString) {
197 openBraces--;
198 } else if (c == '"') {
199 // start of a new JSON string. It's necessary to detect strings as they may
200 // also contain braces/brackets and that could lead to incorrect results.
201 if (!insideString) {
202 insideString = true;
203 } else {
204 int backslashCount = 0;
205 idx--;
206 while (idx >= 0) {
207 if (in.getByte(idx) == '\\') {
208 backslashCount++;
209 idx--;
210 } else {
211 break;
212 }
213 }
214 // The double quote isn't escaped only if there are even "\"s.
215 if (backslashCount % 2 == 0) {
216 // Since the double quote isn't escaped then this is the end of a string.
217 insideString = false;
218 }
219 }
220 }
221 }
222
223 private void initDecoding(byte openingBrace) {
224 openBraces = 1;
225 if (openingBrace == '[' && streamArrayElements) {
226 state = ST_DECODING_ARRAY_STREAM;
227 } else {
228 state = ST_DECODING_NORMAL;
229 }
230 }
231
232 private void reset() {
233 insideString = false;
234 state = ST_INIT;
235 openBraces = 0;
236 }
237 }