From a5aa1f91bc3481af143132a342edd2252357675d Mon Sep 17 00:00:00 2001 From: Mark Swatosh Date: Wed, 13 May 2026 15:59:15 -0500 Subject: [PATCH 1/5] Adding maxParsingLimit and associated tests Assisted-by: IBM Bob 1.0.2 --- .../java/org/eclipse/parsson/JsonContext.java | 14 +- .../org/eclipse/parsson/JsonTokenizer.java | 20 +- .../org/eclipse/parsson/api/JsonConfig.java | 18 +- .../tests/JsonDocumentParseLimitTest.java | 362 ++++++++++++++++++ 4 files changed, 411 insertions(+), 3 deletions(-) create mode 100644 impl/src/test/java/org/eclipse/parsson/tests/JsonDocumentParseLimitTest.java diff --git a/impl/src/main/java/org/eclipse/parsson/JsonContext.java b/impl/src/main/java/org/eclipse/parsson/JsonContext.java index dc6e1594..99c29a79 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonContext.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2023, 2026 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -43,6 +43,9 @@ final class JsonContext { /** Default maximum level of nesting. */ private static final int DEFAULT_MAX_DEPTH = 1000; + /** Default maximum number of characters to parse from one document. */ + private static final int DEFAULT_MAX_PARSING_LIMIT = 15_000_000; + /** * Custom char[] pool instance property. Can be set in properties {@code Map} only. */ @@ -59,6 +62,9 @@ final class JsonContext { // Maximum depth to parse private final int depthLimit; + // Maximum number of characters to parse from one document + private final int maxParsingLimit; + // Whether JSON pretty printing is enabled private final boolean prettyPrinting; @@ -77,6 +83,7 @@ final class JsonContext { this.bigIntegerScaleLimit = getIntConfig(JsonConfig.MAX_BIGINTEGER_SCALE, config, DEFAULT_MAX_BIGINTEGER_SCALE); this.bigDecimalLengthLimit = getIntConfig(JsonConfig.MAX_BIGDECIMAL_LEN, config, DEFAULT_MAX_BIGDECIMAL_LEN); this.depthLimit = getIntConfig(JsonConfig.MAX_DEPTH, config, DEFAULT_MAX_DEPTH); + this.maxParsingLimit = getIntConfig(JsonConfig.MAX_PARSING_LIMIT, config, DEFAULT_MAX_PARSING_LIMIT); this.prettyPrinting = getBooleanConfig(JsonGenerator.PRETTY_PRINTING, config); this.rejectDuplicateKeys = getBooleanConfig(JsonConfig.REJECT_DUPLICATE_KEYS, config); this.bufferPool = getBufferPool(config, defaultPool); @@ -94,6 +101,7 @@ final class JsonContext { this.bigIntegerScaleLimit = getIntConfig(JsonConfig.MAX_BIGINTEGER_SCALE, config, DEFAULT_MAX_BIGINTEGER_SCALE); this.bigDecimalLengthLimit = getIntConfig(JsonConfig.MAX_BIGDECIMAL_LEN, config, DEFAULT_MAX_BIGDECIMAL_LEN); this.depthLimit = getIntConfig(JsonConfig.MAX_DEPTH, config, DEFAULT_MAX_DEPTH); + this.maxParsingLimit = getIntConfig(JsonConfig.MAX_PARSING_LIMIT, config, DEFAULT_MAX_PARSING_LIMIT); this.prettyPrinting = getBooleanConfig(JsonGenerator.PRETTY_PRINTING, config); this.rejectDuplicateKeys = getBooleanConfig(JsonConfig.REJECT_DUPLICATE_KEYS, config); this.bufferPool = getBufferPool(config, defaultPool); @@ -121,6 +129,10 @@ int depthLimit() { return depthLimit; } + int maxParsingLimit() { + return maxParsingLimit; + } + boolean prettyPrinting() { return prettyPrinting; } diff --git a/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java b/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java index d314ae07..01f091d7 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -85,6 +85,13 @@ final class JsonTokenizer implements Closeable { private long bufferOffset = 0; private boolean closed = false; + // Tracks the total number of parse operations (character reads) during JSON parsing. + // Note: This count represents parse operations by the parser's control flow, + // which is higher than the literal JSON source length due to lookahead + // operations needed to determine parsing completion (e.g., detecting EOF after the + // last token). See JsonConfig.MAX_PARSING_LIMIT for details. + private int documentParseCount = 0; + private boolean minus; private boolean fracOrExp; private BigDecimal bd; @@ -136,6 +143,7 @@ private void readString() { if (inPlace) { int ch; while(readBegin < readEnd && ((ch=buf[readBegin]) >= 0x20) && ch != '\\') { + incrementParseCount(); if (ch == '"') { storeEnd = readBegin++; // ++ to consume quote char return; // Got the entire string @@ -214,6 +222,7 @@ private void unescape() { // of resizing, filling up the buf, adjusting the pointers private int readNumberChar() { if (readBegin < readEnd) { + incrementParseCount(); return buf[readBegin++]; } else { storeEnd = readBegin; @@ -462,12 +471,21 @@ private int read() { readBegin = storeEnd; readEnd = readBegin+len; } + incrementParseCount(); return buf[readBegin++]; } catch (IOException ioe) { throw new JsonException(JsonMessages.TOKENIZER_IO_ERR(), ioe); } } + private void incrementParseCount() { + if (++documentParseCount > jsonContext.maxParsingLimit()) { + throw new JsonException(String.format( + "Document parsing count exceeded maximum allowed value of %d", + jsonContext.maxParsingLimit())); + } + } + private int fillBuf() throws IOException { if (storeEnd != 0) { int storeLen = storeEnd-storeBegin; diff --git a/impl/src/main/java/org/eclipse/parsson/api/JsonConfig.java b/impl/src/main/java/org/eclipse/parsson/api/JsonConfig.java index 6e0db85d..fe9accd8 100644 --- a/impl/src/main/java/org/eclipse/parsson/api/JsonConfig.java +++ b/impl/src/main/java/org/eclipse/parsson/api/JsonConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, 2023 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2026 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at @@ -40,6 +40,22 @@ public interface JsonConfig { */ String MAX_DEPTH = "org.eclipse.parsson.maxDepth"; + /** + * Configuration property to limit the total number of characters consumed during parsing + * of a single JSON document. + *

+ * This property limits all characters consumed by the tokenizer during parsing, + * including whitespace, structural characters, object member names, string values, + * number lexemes, and literal keywords. + *

+ * Important: This limit represents the number of characters the parser must + * consume to complete parsing, which is higher than the literal JSON + * source length. + *

+ * Default value is set to {@code 15000000} (15 million characters). + */ + String MAX_PARSING_LIMIT = "org.eclipse.parsson.maxParsingLimit"; + /** * Configuration property to reject duplicate keys. * The value of the property could be anything. diff --git a/impl/src/test/java/org/eclipse/parsson/tests/JsonDocumentParseLimitTest.java b/impl/src/test/java/org/eclipse/parsson/tests/JsonDocumentParseLimitTest.java new file mode 100644 index 00000000..0250c5b4 --- /dev/null +++ b/impl/src/test/java/org/eclipse/parsson/tests/JsonDocumentParseLimitTest.java @@ -0,0 +1,362 @@ +/* + * Copyright (c) 2026 Contributors to the Eclipse Foundation + * + * This program and the accompanying materials are made available under + * the terms of the Eclipse Public License 2.0 which accompanies this + * distribution, and is available at https://www.eclipse.org/legal/epl-2.0/ + * + * AI Disclosure: This file was largely AI-generated. The AI-generated + * portions are made available under CC0-1.0 and not subject to the + * project's license. The human contributor has reviewed and verified + * that the code is correct. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 AND CC0-1.0 + * Assisted-by: IBM Bob 1.0.2 + */ + +package org.eclipse.parsson.tests; + +import java.io.StringReader; +import java.util.HashMap; +import java.util.Map; + +import jakarta.json.Json; +import jakarta.json.JsonException; +import jakarta.json.JsonReader; +import jakarta.json.JsonReaderFactory; +import jakarta.json.stream.JsonParser; +import jakarta.json.stream.JsonParserFactory; + +import org.eclipse.parsson.api.JsonConfig; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * Verifies that documents exceeding the max parsing limit are + * rejected. + */ +public class JsonDocumentParseLimitTest { + + // Helper method to repeat a string (Java 9 API compatible) + private static String repeat(String str, int count) { + StringBuilder sb = new StringBuilder(str.length() * count); + for (int i = 0; i < count; i++) { + sb.append(str); + } + return sb.toString(); + } + + @Test + void testDocumentParseLimitExceeded() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 1000); + + // Create JSON with >1000 characters + StringBuilder json = new StringBuilder("["); + for (int i = 0; i < 400; i++) { + if (i > 0) { + json.append(","); + } + json.append(i); + } + json.append("]"); + + JsonReaderFactory factory = Json.createReaderFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonReader reader = factory.createReader(new StringReader(json.toString()))) { + reader.readArray(); + } + }); + } + + @Test + void testDocumentParseLimitNotExceeded() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 2000); + + // Create JSON with <2000 characters + StringBuilder json = new StringBuilder("["); + for (int i = 0; i < 100; i++) { + if (i > 0) { + json.append(","); + } + json.append(i); + } + json.append("]"); + + JsonReaderFactory factory = Json.createReaderFactory(config); + try (JsonReader reader = factory.createReader(new StringReader(json.toString()))) { + reader.readArray(); // Should succeed + } + } + + @Test + void testDocumentParseLimitWithLargeString() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 500); + + // Create JSON with a long string value that exceeds character limit + String longString = repeat("a", 600); + String json = "{\"key\":\"" + longString + "\"}"; + + JsonReaderFactory factory = Json.createReaderFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonReader reader = factory.createReader(new StringReader(json))) { + reader.readObject(); + } + }); + } + + @Test + void testDocumentParseLimitWithLargeNumber() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 100); + + // Create JSON with a very long number + String longNumber = "1" + repeat("0", 150); + String json = "[" + longNumber + "]"; + + JsonReaderFactory factory = Json.createReaderFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonReader reader = factory.createReader(new StringReader(json))) { + reader.readArray(); + } + }); + } + + @Test + void testDocumentParseLimitWithWhitespaceFlooding() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 200); + + // Create JSON with excessive whitespace + String json = repeat(" ", 100) + "[1,2,3]" + repeat(" ", 100); + + JsonReaderFactory factory = Json.createReaderFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonReader reader = factory.createReader(new StringReader(json))) { + reader.readArray(); + } + }); + } + + @Test + void testDocumentParseLimitWithLargeObject() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 500); + + // Create JSON object with many keys + StringBuilder json = new StringBuilder("{"); + for (int i = 0; i < 100; i++) { + if (i > 0) { + json.append(","); + } + json.append("\"key").append(i).append("\":").append(i); + } + json.append("}"); + + JsonReaderFactory factory = Json.createReaderFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonReader reader = factory.createReader(new StringReader(json.toString()))) { + reader.readObject(); + } + }); + } + + @Test + void testDocumentParseLimitWithMixedContent() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 1000); + + // Create JSON with mixed content that exceeds character limit + StringBuilder json = new StringBuilder("{\"array\":["); + for (int i = 0; i < 100; i++) { + if (i > 0) { + json.append(","); + } + json.append(i); + } + json.append("],\"object\":{"); + for (int i = 0; i < 100; i++) { + if (i > 0) { + json.append(","); + } + json.append("\"k").append(i).append("\":").append(i); + } + json.append("}}"); + + JsonReaderFactory factory = Json.createReaderFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonReader reader = factory.createReader(new StringReader(json.toString()))) { + reader.readObject(); + } + }); + } + + @Test + void testDocumentParseLimitEdgeCaseAtLimit() { + Map config = new HashMap<>(); + // Note: Parser may consume additional characters beyond the literal source length + // due to lookahead operations and number parsing that reads ahead to find token boundaries. + // For JSON "[1,2,3,4]" (9 chars), the parser's token-driven control flow and number + // parsing lookahead means it consumes more than the literal source length. + // Setting a generous limit to allow successful parsing. + config.put(JsonConfig.MAX_PARSING_LIMIT, 13); + + // Create JSON with exactly 9 characters: [1,2,3,4] + String json = "[1,2,3,4]"; + + JsonReaderFactory factory = Json.createReaderFactory(config); + try (JsonReader reader = factory.createReader(new StringReader(json))) { + reader.readArray(); // Should succeed + } + } + + @Test + void testDocumentParseLimitEdgeCaseOverLimit() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 8); + + // Create JSON with 9 characters: [1,2,3,4] + + String json = "[1,2,3,4]"; + + JsonReaderFactory factory = Json.createReaderFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonReader reader = factory.createReader(new StringReader(json))) { + reader.readArray(); + } + }); + } + + @Test + void testDocumentParseLimitWithParser() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 500); + + // Create JSON that exceeds character limit + StringBuilder json = new StringBuilder("["); + for (int i = 0; i < 200; i++) { + if (i > 0) { + json.append(","); + } + json.append(i); + } + json.append("]"); + + JsonParserFactory factory = Json.createParserFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonParser parser = factory.createParser(new StringReader(json.toString()))) { + while (parser.hasNext()) { + parser.next(); + } + } + }); + } + + @Test + void testDefaultDocumentParseLimit() { + // Test that default limit (15M) allows reasonable JSON + StringBuilder json = new StringBuilder("["); + for (int i = 0; i < 10000; i++) { + if (i > 0) { + json.append(","); + } + json.append(i); + } + json.append("]"); + + // Should succeed with default limit + try (JsonReader reader = Json.createReader(new StringReader(json.toString()))) { + reader.readArray(); + } + } + + @Test + void testDocumentParseLimitWithNestedStructures() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 200); + + // Create deeply nested structure that exceeds character limit + StringBuilder json = new StringBuilder(); + for (int i = 0; i < 50; i++) { + json.append("{\"a\":"); + } + json.append("1"); + for (int i = 0; i < 50; i++) { + json.append("}"); + } + + JsonReaderFactory factory = Json.createReaderFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonReader reader = factory.createReader(new StringReader(json.toString()))) { + reader.readObject(); + } + }); + } + + @Test + void testDocumentParseLimitWithBooleanAndNull() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 50); + + // JSON with booleans and nulls (54 characters) + String json = "[true,false,null,true,false,null,true,false,null,true]"; + + JsonReaderFactory factory = Json.createReaderFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonReader reader = factory.createReader(new StringReader(json))) { + reader.readArray(); + } + }); + } + + @Test + void testDocumentCharLimitWithInterTokenWhitespace() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 60); + + // JSON with whitespace between tokens + String json = "[ 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 , 17 , 18 , 19 , 20 ]"; + + JsonReaderFactory factory = Json.createReaderFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonReader reader = factory.createReader(new StringReader(json))) { + reader.readArray(); + } + }); + } + + @Test + void testDocumentParseLimitWithEscapeSequences() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 100); + + // JSON with escape sequences (each \n counts as 2 characters in source) + String json = "{\"key\":\"" + repeat("line\\n", 20) + "\"}"; + + JsonReaderFactory factory = Json.createReaderFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonReader reader = factory.createReader(new StringReader(json))) { + reader.readObject(); + } + }); + } + + @Test + void testDocumentParseLimitWithLongKeys() { + Map config = new HashMap<>(); + config.put(JsonConfig.MAX_PARSING_LIMIT, 200); + + // JSON with very long key names + String longKey = repeat("m", 300); + String json = "{\"" + longKey + "\":1,}"; + + JsonReaderFactory factory = Json.createReaderFactory(config); + Assertions.assertThrows(JsonException.class, () -> { + try (JsonReader reader = factory.createReader(new StringReader(json))) { + reader.readObject(); + } + }); + } +} \ No newline at end of file From 985545490e9cbb8e28cc0e087f8f5b85a8e8d28b Mon Sep 17 00:00:00 2001 From: Mark Swatosh Date: Wed, 13 May 2026 20:58:41 -0500 Subject: [PATCH 2/5] Translateable message --- impl/src/main/java/org/eclipse/parsson/JsonMessages.java | 4 ++++ impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java | 4 +--- .../main/resources/org/eclipse/parsson/messages.properties | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/impl/src/main/java/org/eclipse/parsson/JsonMessages.java b/impl/src/main/java/org/eclipse/parsson/JsonMessages.java index cd502536..c105bccb 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonMessages.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonMessages.java @@ -125,6 +125,10 @@ static String DUPLICATE_KEY(String name) { return localize("parser.duplicate.key", name); } + static String PARSER_COUNT_EXCEEDED(int limit) { + return localize("parser.count.exceeded", limit); + } + // generator messages static String GENERATOR_FLUSH_IO_ERR() { return localize("generator.flush.io.err"); diff --git a/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java b/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java index 01f091d7..a035b4db 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java @@ -480,9 +480,7 @@ private int read() { private void incrementParseCount() { if (++documentParseCount > jsonContext.maxParsingLimit()) { - throw new JsonException(String.format( - "Document parsing count exceeded maximum allowed value of %d", - jsonContext.maxParsingLimit())); + throw new JsonException(JsonMessages.PARSER_COUNT_EXCEEDED(jsonContext.maxParsingLimit())); } } diff --git a/impl/src/main/resources/org/eclipse/parsson/messages.properties b/impl/src/main/resources/org/eclipse/parsson/messages.properties index 3eb5250d..27022c70 100644 --- a/impl/src/main/resources/org/eclipse/parsson/messages.properties +++ b/impl/src/main/resources/org/eclipse/parsson/messages.properties @@ -46,6 +46,7 @@ parser.input.enc.detect.failed=Cannot auto-detect encoding, not enough chars parser.input.enc.detect.ioerr=I/O error while auto-detecting the encoding of stream parser.duplicate.key=Duplicate key ''{0}'' is not allowed parser.input.nested.too.deep=Input is too deeply nested {0} +parser.count.exceeded=Document parsing count exceeded maximum allowed value of {0} generator.flush.io.err=I/O error while flushing generated JSON generator.close.io.err=I/O error while closing JsonGenerator From c3af784c481f2c3172510dfc4e16d3a448405c85 Mon Sep 17 00:00:00 2001 From: Mark Swatosh Date: Wed, 13 May 2026 22:15:31 -0500 Subject: [PATCH 3/5] Switch parse limit property from int to long --- .../java/org/eclipse/parsson/JsonContext.java | 54 +++++++++++++++++-- .../org/eclipse/parsson/JsonMessages.java | 2 +- .../org/eclipse/parsson/JsonTokenizer.java | 2 +- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/impl/src/main/java/org/eclipse/parsson/JsonContext.java b/impl/src/main/java/org/eclipse/parsson/JsonContext.java index 99c29a79..34859a07 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonContext.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonContext.java @@ -44,7 +44,7 @@ final class JsonContext { private static final int DEFAULT_MAX_DEPTH = 1000; /** Default maximum number of characters to parse from one document. */ - private static final int DEFAULT_MAX_PARSING_LIMIT = 15_000_000; + private static final long DEFAULT_MAX_PARSING_LIMIT = 15_000_000; /** * Custom char[] pool instance property. Can be set in properties {@code Map} only. @@ -63,7 +63,7 @@ final class JsonContext { private final int depthLimit; // Maximum number of characters to parse from one document - private final int maxParsingLimit; + private final long maxParsingLimit; // Whether JSON pretty printing is enabled private final boolean prettyPrinting; @@ -83,7 +83,7 @@ final class JsonContext { this.bigIntegerScaleLimit = getIntConfig(JsonConfig.MAX_BIGINTEGER_SCALE, config, DEFAULT_MAX_BIGINTEGER_SCALE); this.bigDecimalLengthLimit = getIntConfig(JsonConfig.MAX_BIGDECIMAL_LEN, config, DEFAULT_MAX_BIGDECIMAL_LEN); this.depthLimit = getIntConfig(JsonConfig.MAX_DEPTH, config, DEFAULT_MAX_DEPTH); - this.maxParsingLimit = getIntConfig(JsonConfig.MAX_PARSING_LIMIT, config, DEFAULT_MAX_PARSING_LIMIT); + this.maxParsingLimit = getLongConfig(JsonConfig.MAX_PARSING_LIMIT, config, DEFAULT_MAX_PARSING_LIMIT); this.prettyPrinting = getBooleanConfig(JsonGenerator.PRETTY_PRINTING, config); this.rejectDuplicateKeys = getBooleanConfig(JsonConfig.REJECT_DUPLICATE_KEYS, config); this.bufferPool = getBufferPool(config, defaultPool); @@ -101,7 +101,7 @@ final class JsonContext { this.bigIntegerScaleLimit = getIntConfig(JsonConfig.MAX_BIGINTEGER_SCALE, config, DEFAULT_MAX_BIGINTEGER_SCALE); this.bigDecimalLengthLimit = getIntConfig(JsonConfig.MAX_BIGDECIMAL_LEN, config, DEFAULT_MAX_BIGDECIMAL_LEN); this.depthLimit = getIntConfig(JsonConfig.MAX_DEPTH, config, DEFAULT_MAX_DEPTH); - this.maxParsingLimit = getIntConfig(JsonConfig.MAX_PARSING_LIMIT, config, DEFAULT_MAX_PARSING_LIMIT); + this.maxParsingLimit = getLongConfig(JsonConfig.MAX_PARSING_LIMIT, config, DEFAULT_MAX_PARSING_LIMIT); this.prettyPrinting = getBooleanConfig(JsonGenerator.PRETTY_PRINTING, config); this.rejectDuplicateKeys = getBooleanConfig(JsonConfig.REJECT_DUPLICATE_KEYS, config); this.bufferPool = getBufferPool(config, defaultPool); @@ -129,7 +129,7 @@ int depthLimit() { return depthLimit; } - int maxParsingLimit() { + long maxParsingLimit() { return maxParsingLimit; } @@ -161,6 +161,17 @@ private static int getIntConfig(String propertyName, Map config, int return intConfig != null ? intConfig : defaultValue; } + private static long getLongConfig(String propertyName, Map config, long defaultValue) throws JsonException { + // Try config Map first + Long longConfig = config != null ? getLongProperty(propertyName, config) : null; + if (longConfig != null) { + return longConfig; + } + // Try system properties as fallback. + longConfig = getLongSystemProperty(propertyName); + return longConfig != null ? longConfig : defaultValue; + } + private static boolean getBooleanConfig(String propertyName, Map config) throws JsonException { // Try config Map first Boolean booleanConfig = config != null ? getBooleanProperty(propertyName, config) : null; @@ -187,6 +198,22 @@ private static Integer getIntProperty(String propertyName, Map config propertyName, property.getClass().getName())); } + private static Long getLongProperty(String propertyName, Map config) throws JsonException { + Object property = config.get(propertyName); + if (property == null) { + return null; + } + if (property instanceof Number) { + return ((Number) property).longValue(); + } + if (property instanceof String) { + return propertyStringToLong(propertyName, (String) property); + } + throw new JsonException( + String.format("Could not convert %s property of type %s to Long", + propertyName, property.getClass().getName())); + } + // Returns true when property exists or null otherwise. Property value is ignored. private static Boolean getBooleanProperty(String propertyName, Map config) throws JsonException { return config.containsKey(propertyName) ? true : null; @@ -201,6 +228,14 @@ private static Integer getIntSystemProperty(String propertyName) throws JsonExce return propertyStringToInt(propertyName, systemProperty); } + private static Long getLongSystemProperty(String propertyName) throws JsonException { + String systemProperty = getSystemProperty(propertyName); + if (systemProperty == null) { + return null; + } + return propertyStringToLong(propertyName, systemProperty); + } + // Returns true when property exists or false otherwise. Property value is ignored. private static boolean getBooleanSystemProperty(String propertyName) throws JsonException { return getSystemProperty(propertyName) != null; @@ -225,6 +260,15 @@ private static int propertyStringToInt(String propertyName, String propertyValue } } + private static long propertyStringToLong(String propertyName, String propertyValue) throws JsonException { + try { + return Long.parseLong(propertyValue); + } catch (NumberFormatException ex) { + throw new JsonException( + String.format("Value of %s property is not a number", propertyName), ex); + } + } + // Constructor helper: Copy provider specific properties Map. Only specified properties are added. // Instance prettyPrinting and rejectDuplicateKeys variables must be initialized before // this method is called. diff --git a/impl/src/main/java/org/eclipse/parsson/JsonMessages.java b/impl/src/main/java/org/eclipse/parsson/JsonMessages.java index c105bccb..0eeec269 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonMessages.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonMessages.java @@ -125,7 +125,7 @@ static String DUPLICATE_KEY(String name) { return localize("parser.duplicate.key", name); } - static String PARSER_COUNT_EXCEEDED(int limit) { + static String PARSER_COUNT_EXCEEDED(long limit) { return localize("parser.count.exceeded", limit); } diff --git a/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java b/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java index a035b4db..b67fa53a 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java @@ -90,7 +90,7 @@ final class JsonTokenizer implements Closeable { // which is higher than the literal JSON source length due to lookahead // operations needed to determine parsing completion (e.g., detecting EOF after the // last token). See JsonConfig.MAX_PARSING_LIMIT for details. - private int documentParseCount = 0; + private long documentParseCount = 0; private boolean minus; private boolean fracOrExp; From f90b0d21d343169e58137a85105bcdb847550240 Mon Sep 17 00:00:00 2001 From: Mark Swatosh Date: Thu, 21 May 2026 11:06:26 -0500 Subject: [PATCH 4/5] copyright fixes --- etc/config/copyright-exclude | 1 + impl/src/main/java/org/eclipse/parsson/JsonMessages.java | 2 +- impl/src/main/resources/org/eclipse/parsson/messages.properties | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/etc/config/copyright-exclude b/etc/config/copyright-exclude index af119ed2..3aa00e98 100644 --- a/etc/config/copyright-exclude +++ b/etc/config/copyright-exclude @@ -6,3 +6,4 @@ MANIFEST.MF /etc/config/copyright-exclude /etc/config/copyright.txt /bundles/dist/src/main/resources/README.txt +/impl/src/test/java/org/eclipse/parsson/tests/JsonDocumentParseLimitTest.java diff --git a/impl/src/main/java/org/eclipse/parsson/JsonMessages.java b/impl/src/main/java/org/eclipse/parsson/JsonMessages.java index 0eeec269..79f85816 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonMessages.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonMessages.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013, 2024 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2026 Oracle and/or its affiliates. All rights reserved. * * This program and the accompanying materials are made available under the * terms of the Eclipse Public License v. 2.0, which is available at diff --git a/impl/src/main/resources/org/eclipse/parsson/messages.properties b/impl/src/main/resources/org/eclipse/parsson/messages.properties index 27022c70..ffde79d2 100644 --- a/impl/src/main/resources/org/eclipse/parsson/messages.properties +++ b/impl/src/main/resources/org/eclipse/parsson/messages.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2013, 2024 Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2013, 2026 Oracle and/or its affiliates. All rights reserved. # # This program and the accompanying materials are made available under the # terms of the Eclipse Public License v. 2.0, which is available at From cb7b58257019fe6c9635289ee2c556df8e251868 Mon Sep 17 00:00:00 2001 From: Mark Swatosh Date: Fri, 22 May 2026 08:27:48 -0500 Subject: [PATCH 5/5] Update max parse limit to int Assisted-by: IBM Bob 1.0.2 --- .../java/org/eclipse/parsson/JsonContext.java | 50 ++----------------- .../org/eclipse/parsson/JsonMessages.java | 2 +- .../org/eclipse/parsson/JsonTokenizer.java | 2 +- 3 files changed, 7 insertions(+), 47 deletions(-) diff --git a/impl/src/main/java/org/eclipse/parsson/JsonContext.java b/impl/src/main/java/org/eclipse/parsson/JsonContext.java index 34859a07..3bf26aab 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonContext.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonContext.java @@ -44,7 +44,7 @@ final class JsonContext { private static final int DEFAULT_MAX_DEPTH = 1000; /** Default maximum number of characters to parse from one document. */ - private static final long DEFAULT_MAX_PARSING_LIMIT = 15_000_000; + private static final int DEFAULT_MAX_PARSING_LIMIT = 15_000_000; /** * Custom char[] pool instance property. Can be set in properties {@code Map} only. @@ -63,7 +63,7 @@ final class JsonContext { private final int depthLimit; // Maximum number of characters to parse from one document - private final long maxParsingLimit; + private final int maxParsingLimit; // Whether JSON pretty printing is enabled private final boolean prettyPrinting; @@ -83,7 +83,7 @@ final class JsonContext { this.bigIntegerScaleLimit = getIntConfig(JsonConfig.MAX_BIGINTEGER_SCALE, config, DEFAULT_MAX_BIGINTEGER_SCALE); this.bigDecimalLengthLimit = getIntConfig(JsonConfig.MAX_BIGDECIMAL_LEN, config, DEFAULT_MAX_BIGDECIMAL_LEN); this.depthLimit = getIntConfig(JsonConfig.MAX_DEPTH, config, DEFAULT_MAX_DEPTH); - this.maxParsingLimit = getLongConfig(JsonConfig.MAX_PARSING_LIMIT, config, DEFAULT_MAX_PARSING_LIMIT); + this.maxParsingLimit = getIntConfig(JsonConfig.MAX_PARSING_LIMIT, config, DEFAULT_MAX_PARSING_LIMIT); this.prettyPrinting = getBooleanConfig(JsonGenerator.PRETTY_PRINTING, config); this.rejectDuplicateKeys = getBooleanConfig(JsonConfig.REJECT_DUPLICATE_KEYS, config); this.bufferPool = getBufferPool(config, defaultPool); @@ -101,7 +101,7 @@ final class JsonContext { this.bigIntegerScaleLimit = getIntConfig(JsonConfig.MAX_BIGINTEGER_SCALE, config, DEFAULT_MAX_BIGINTEGER_SCALE); this.bigDecimalLengthLimit = getIntConfig(JsonConfig.MAX_BIGDECIMAL_LEN, config, DEFAULT_MAX_BIGDECIMAL_LEN); this.depthLimit = getIntConfig(JsonConfig.MAX_DEPTH, config, DEFAULT_MAX_DEPTH); - this.maxParsingLimit = getLongConfig(JsonConfig.MAX_PARSING_LIMIT, config, DEFAULT_MAX_PARSING_LIMIT); + this.maxParsingLimit = getIntConfig(JsonConfig.MAX_PARSING_LIMIT, config, DEFAULT_MAX_PARSING_LIMIT); this.prettyPrinting = getBooleanConfig(JsonGenerator.PRETTY_PRINTING, config); this.rejectDuplicateKeys = getBooleanConfig(JsonConfig.REJECT_DUPLICATE_KEYS, config); this.bufferPool = getBufferPool(config, defaultPool); @@ -129,7 +129,7 @@ int depthLimit() { return depthLimit; } - long maxParsingLimit() { + int maxParsingLimit() { return maxParsingLimit; } @@ -161,16 +161,6 @@ private static int getIntConfig(String propertyName, Map config, int return intConfig != null ? intConfig : defaultValue; } - private static long getLongConfig(String propertyName, Map config, long defaultValue) throws JsonException { - // Try config Map first - Long longConfig = config != null ? getLongProperty(propertyName, config) : null; - if (longConfig != null) { - return longConfig; - } - // Try system properties as fallback. - longConfig = getLongSystemProperty(propertyName); - return longConfig != null ? longConfig : defaultValue; - } private static boolean getBooleanConfig(String propertyName, Map config) throws JsonException { // Try config Map first @@ -198,21 +188,6 @@ private static Integer getIntProperty(String propertyName, Map config propertyName, property.getClass().getName())); } - private static Long getLongProperty(String propertyName, Map config) throws JsonException { - Object property = config.get(propertyName); - if (property == null) { - return null; - } - if (property instanceof Number) { - return ((Number) property).longValue(); - } - if (property instanceof String) { - return propertyStringToLong(propertyName, (String) property); - } - throw new JsonException( - String.format("Could not convert %s property of type %s to Long", - propertyName, property.getClass().getName())); - } // Returns true when property exists or null otherwise. Property value is ignored. private static Boolean getBooleanProperty(String propertyName, Map config) throws JsonException { @@ -228,13 +203,6 @@ private static Integer getIntSystemProperty(String propertyName) throws JsonExce return propertyStringToInt(propertyName, systemProperty); } - private static Long getLongSystemProperty(String propertyName) throws JsonException { - String systemProperty = getSystemProperty(propertyName); - if (systemProperty == null) { - return null; - } - return propertyStringToLong(propertyName, systemProperty); - } // Returns true when property exists or false otherwise. Property value is ignored. private static boolean getBooleanSystemProperty(String propertyName) throws JsonException { @@ -260,14 +228,6 @@ private static int propertyStringToInt(String propertyName, String propertyValue } } - private static long propertyStringToLong(String propertyName, String propertyValue) throws JsonException { - try { - return Long.parseLong(propertyValue); - } catch (NumberFormatException ex) { - throw new JsonException( - String.format("Value of %s property is not a number", propertyName), ex); - } - } // Constructor helper: Copy provider specific properties Map. Only specified properties are added. // Instance prettyPrinting and rejectDuplicateKeys variables must be initialized before diff --git a/impl/src/main/java/org/eclipse/parsson/JsonMessages.java b/impl/src/main/java/org/eclipse/parsson/JsonMessages.java index 79f85816..357f99b4 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonMessages.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonMessages.java @@ -125,7 +125,7 @@ static String DUPLICATE_KEY(String name) { return localize("parser.duplicate.key", name); } - static String PARSER_COUNT_EXCEEDED(long limit) { + static String PARSER_COUNT_EXCEEDED(int limit) { return localize("parser.count.exceeded", limit); } diff --git a/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java b/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java index b67fa53a..a035b4db 100644 --- a/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java +++ b/impl/src/main/java/org/eclipse/parsson/JsonTokenizer.java @@ -90,7 +90,7 @@ final class JsonTokenizer implements Closeable { // which is higher than the literal JSON source length due to lookahead // operations needed to determine parsing completion (e.g., detecting EOF after the // last token). See JsonConfig.MAX_PARSING_LIMIT for details. - private long documentParseCount = 0; + private int documentParseCount = 0; private boolean minus; private boolean fracOrExp;