|
1 | 1 | package tools.jackson.core.unittest.constraints; |
2 | 2 |
|
3 | 3 | import java.io.IOException; |
| 4 | +import java.nio.ByteBuffer; |
| 5 | +import java.util.Arrays; |
4 | 6 |
|
5 | 7 | import org.junit.jupiter.api.Test; |
6 | 8 |
|
7 | 9 | import tools.jackson.core.JsonParser; |
| 10 | +import tools.jackson.core.JsonToken; |
8 | 11 | import tools.jackson.core.ObjectReadContext; |
9 | 12 | import tools.jackson.core.StreamReadConstraints; |
| 13 | +import tools.jackson.core.async.ByteArrayFeeder; |
| 14 | +import tools.jackson.core.async.ByteBufferFeeder; |
10 | 15 | import tools.jackson.core.exc.StreamConstraintsException; |
11 | 16 | import tools.jackson.core.json.JsonFactory; |
12 | 17 | import tools.jackson.core.unittest.async.AsyncTestBase; |
@@ -108,6 +113,140 @@ void largeNameWithSmallLimitAsync() throws Exception |
108 | 113 | } |
109 | 114 | } |
110 | 115 |
|
| 116 | + // [core#1642] maxDocumentLength must also be enforced when the caller feeds |
| 117 | + // the whole document via a single feedInput() call (e.g. pre-buffered input), |
| 118 | + // not just when input arrives split across multiple feedInput() calls. |
| 119 | + @Test |
| 120 | + void largeNameWithSmallLimitAsyncSingleFeed() throws Exception |
| 121 | + { |
| 122 | + final byte[] doc = utf8Bytes(generateJSON(12_000)); |
| 123 | + |
| 124 | + // first with byte[] backend: bytesPerRead >= doc.length so the whole |
| 125 | + // document goes through in exactly one feedInput() call |
| 126 | + try (AsyncReaderWrapper p = asyncForBytes(JSON_F_DOC_10K, doc.length, doc, 1)) { |
| 127 | + consumeAsync(p); |
| 128 | + fail("expected StreamConstraintsException"); |
| 129 | + } catch (StreamConstraintsException e) { |
| 130 | + verifyMaxDocLen(JSON_F_DOC_10K, e); |
| 131 | + } |
| 132 | + |
| 133 | + // then with byte buffer backend, same single-call condition |
| 134 | + try (AsyncReaderWrapper p = asyncForByteBuffer(JSON_F_DOC_10K, doc.length, doc, 1)) { |
| 135 | + consumeAsync(p); |
| 136 | + fail("expected StreamConstraintsException"); |
| 137 | + } catch (StreamConstraintsException e) { |
| 138 | + verifyMaxDocLen(JSON_F_DOC_10K, e); |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + // [core#1642] Boundary check: a single feedInput() call carrying EXACTLY |
| 143 | + // maxDocumentLength bytes must still parse successfully -- validateDocumentLength() |
| 144 | + // rejects only len > maxDocumentLength, so the limit itself is inclusive. |
| 145 | + // This pins down "bytes fed, not consumed" semantics and guards against a |
| 146 | + // future off-by-one in the single-feed fix. |
| 147 | + @Test |
| 148 | + void largeNameWithSmallLimitAsyncSingleFeedAtBoundary() throws Exception |
| 149 | + { |
| 150 | + final long limit = JSON_F_DOC_10K.streamReadConstraints().getMaxDocumentLength(); |
| 151 | + final byte[] doc = utf8Bytes(generateExactLengthJSON((int) limit)); |
| 152 | + assertEquals(limit, doc.length); |
| 153 | + |
| 154 | + // first with byte[] backend: bytesPerRead >= doc.length so the whole |
| 155 | + // document goes through in exactly one feedInput() call |
| 156 | + try (AsyncReaderWrapper p = asyncForBytes(JSON_F_DOC_10K, doc.length, doc, 1)) { |
| 157 | + consumeAsync(p); |
| 158 | + } |
| 159 | + |
| 160 | + // then with byte buffer backend, same single-call condition |
| 161 | + try (AsyncReaderWrapper p = asyncForByteBuffer(JSON_F_DOC_10K, doc.length, doc, 1)) { |
| 162 | + consumeAsync(p); |
| 163 | + } |
| 164 | + } |
| 165 | + |
| 166 | + // [core#1642] Same boundary, but reached across MANY feedInput() calls: bytes |
| 167 | + // fed must accumulate to exactly maxDocumentLength and still parse, verifying |
| 168 | + // the single-feed fix did not start double-counting incrementally fed buffers. |
| 169 | + @Test |
| 170 | + void largeNameWithSmallLimitAsyncMultiFeedAtBoundary() throws Exception |
| 171 | + { |
| 172 | + final long limit = JSON_F_DOC_10K.streamReadConstraints().getMaxDocumentLength(); |
| 173 | + final byte[] doc = utf8Bytes(generateExactLengthJSON((int) limit)); |
| 174 | + assertEquals(limit, doc.length); |
| 175 | + |
| 176 | + // 1000 bytes per call, so exactly 10 feedInput() calls totalling the limit |
| 177 | + try (AsyncReaderWrapper p = asyncForBytes(JSON_F_DOC_10K, 1000, doc, 1)) { |
| 178 | + consumeAsync(p); |
| 179 | + } |
| 180 | + try (AsyncReaderWrapper p = asyncForByteBuffer(JSON_F_DOC_10K, 1000, doc, 1)) { |
| 181 | + consumeAsync(p); |
| 182 | + } |
| 183 | + } |
| 184 | + |
| 185 | + // [core#1642] A rejected feedInput() must not corrupt the running byte count: |
| 186 | + // validation happens BEFORE any state is updated, so a caller that catches the |
| 187 | + // StreamConstraintsException and keeps feeding still gets an accurate total |
| 188 | + // (the rejected call's predecessor must not be counted twice). |
| 189 | + @Test |
| 190 | + void docLengthCountIntactAfterRejectedFeedBytes() throws Exception |
| 191 | + { |
| 192 | + try (JsonParser p = JSON_F_DOC_10K.createNonBlockingByteArrayParser(ObjectReadContext.empty())) { |
| 193 | + final ByteArrayFeeder feeder = (ByteArrayFeeder) p.nonBlockingInputFeeder(); |
| 194 | + |
| 195 | + // 5000 fed, well under the 10000 limit |
| 196 | + feeder.feedInput(whitespace(5000), 0, 5000); |
| 197 | + assertToken(JsonToken.NOT_AVAILABLE, p.nextToken()); |
| 198 | + |
| 199 | + // would reach 11000: rejected, and must leave the count at 5000 |
| 200 | + try { |
| 201 | + feeder.feedInput(whitespace(6000), 0, 6000); |
| 202 | + fail("expected StreamConstraintsException"); |
| 203 | + } catch (StreamConstraintsException e) { |
| 204 | + verifyMaxDocLen(JSON_F_DOC_10K, e); |
| 205 | + } |
| 206 | + |
| 207 | + // 5000 more == 10000 total: at the limit, so must still be accepted |
| 208 | + feeder.feedInput(whitespace(5000), 0, 5000); |
| 209 | + assertToken(JsonToken.NOT_AVAILABLE, p.nextToken()); |
| 210 | + |
| 211 | + // and one byte past it must report the true total, not an inflated one |
| 212 | + try { |
| 213 | + feeder.feedInput(whitespace(1), 0, 1); |
| 214 | + fail("expected StreamConstraintsException"); |
| 215 | + } catch (StreamConstraintsException e) { |
| 216 | + verifyException(e, "Document length (10001)"); |
| 217 | + } |
| 218 | + } |
| 219 | + } |
| 220 | + |
| 221 | + // [core#1642] as above, for the ByteBuffer-backed parser |
| 222 | + @Test |
| 223 | + void docLengthCountIntactAfterRejectedFeedByteBuffer() throws Exception |
| 224 | + { |
| 225 | + try (JsonParser p = JSON_F_DOC_10K.createNonBlockingByteBufferParser(ObjectReadContext.empty())) { |
| 226 | + final ByteBufferFeeder feeder = (ByteBufferFeeder) p.nonBlockingInputFeeder(); |
| 227 | + |
| 228 | + feeder.feedInput(ByteBuffer.wrap(whitespace(5000))); |
| 229 | + assertToken(JsonToken.NOT_AVAILABLE, p.nextToken()); |
| 230 | + |
| 231 | + try { |
| 232 | + feeder.feedInput(ByteBuffer.wrap(whitespace(6000))); |
| 233 | + fail("expected StreamConstraintsException"); |
| 234 | + } catch (StreamConstraintsException e) { |
| 235 | + verifyMaxDocLen(JSON_F_DOC_10K, e); |
| 236 | + } |
| 237 | + |
| 238 | + feeder.feedInput(ByteBuffer.wrap(whitespace(5000))); |
| 239 | + assertToken(JsonToken.NOT_AVAILABLE, p.nextToken()); |
| 240 | + |
| 241 | + try { |
| 242 | + feeder.feedInput(ByteBuffer.wrap(whitespace(1))); |
| 243 | + fail("expected StreamConstraintsException"); |
| 244 | + } catch (StreamConstraintsException e) { |
| 245 | + verifyException(e, "Document length (10001)"); |
| 246 | + } |
| 247 | + } |
| 248 | + } |
| 249 | + |
111 | 250 | // [core#1575] DataInput with maxDocumentLength should enforce the limit |
112 | 251 | @Test |
113 | 252 | void dataInputWithDocLengthLimitEnforced() throws Exception |
@@ -157,6 +296,31 @@ private void consumeAsync(AsyncReaderWrapper w) throws IOException { |
157 | 296 | } |
158 | 297 | } |
159 | 298 |
|
| 299 | + // Builds a valid JSON array whose UTF-8 byte length is exactly {@code exactLen}, |
| 300 | + // using trailing whitespace padding before the closing bracket (all-ASCII content, |
| 301 | + // so char length == byte length). |
| 302 | + private String generateExactLengthJSON(final int exactLen) { |
| 303 | + final StringBuilder sb = new StringBuilder(); |
| 304 | + sb.append('['); |
| 305 | + while (sb.length() < exactLen - 10) { |
| 306 | + sb.append("1,"); |
| 307 | + } |
| 308 | + sb.append('1'); |
| 309 | + while (sb.length() < exactLen - 1) { |
| 310 | + sb.append(' '); |
| 311 | + } |
| 312 | + sb.append(']'); |
| 313 | + return sb.toString(); |
| 314 | + } |
| 315 | + |
| 316 | + // Content that is valid-but-tokenless, so buffers can be fed and fully consumed |
| 317 | + // without producing tokens: lets tests exercise feedInput() accounting directly. |
| 318 | + private byte[] whitespace(final int len) { |
| 319 | + final byte[] b = new byte[len]; |
| 320 | + Arrays.fill(b, (byte) ' '); |
| 321 | + return b; |
| 322 | + } |
| 323 | + |
160 | 324 | private String generateJSON(final int docLen) { |
161 | 325 | final StringBuilder sb = new StringBuilder(); |
162 | 326 | sb.append("["); |
|
0 commit comments