This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/json-key in repository https://gitbox.apache.org/repos/asf/cxf.git
commit c95f0e7750739135a7dae15eceabd435fbf87d3a Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Mon May 25 14:08:31 2026 +0100 Improve JSON key parsing --- .../json/basic/JsonMapObjectReaderWriter.java | 46 +++++++++++++++++++++- .../json/basic/JsonMapObjectReaderWriterTest.java | 27 +++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/rt/rs/extensions/json-basic/src/main/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriter.java b/rt/rs/extensions/json-basic/src/main/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriter.java index f6c8abe4145..bbd6852302f 100644 --- a/rt/rs/extensions/json-basic/src/main/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriter.java +++ b/rt/rs/extensions/json-basic/src/main/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriter.java @@ -190,9 +190,10 @@ public class JsonMapObjectReaderWriter { continue; } - int closingQuote = json.indexOf(DQUOTE, i + 1); + int closingQuote = json.charAt(i) == DQUOTE + ? findClosingQuote(json, i) : json.indexOf(DQUOTE, i + 1); int from = json.charAt(i) == DQUOTE ? i + 1 : i; - String name = json.substring(from, closingQuote); + String name = unescapeKeyName(json.substring(from, closingQuote)); int sepIndex = json.indexOf(COLON, closingQuote + 1); if (sepIndex == -1) { throw new UncheckedIOException(new IOException("Error in parsing json")); @@ -398,6 +399,47 @@ public class JsonMapObjectReaderWriter { } + /** + * Returns the index of the closing {@code "} that matches the opening quote at + * {@code openQuoteIndex}, correctly skipping over escaped quotes ({@code \"}) and + * escaped backslashes ({@code \\}) inside the string by counting consecutive + * backslashes immediately before each candidate {@code "}: an odd count means the + * quote is escaped; an even count means it is a real string delimiter. + */ + private static int findClosingQuote(String json, int openQuoteIndex) { + for (int i = openQuoteIndex + 1; i < json.length(); i++) { + if (json.charAt(i) == DQUOTE) { + int backslashCount = 0; + int k = i - 1; + while (k > openQuoteIndex && json.charAt(k) == ESCAPE) { + backslashCount++; + k--; + } + if (backslashCount % 2 == 0) { + return i; + } + } + } + return json.length(); // malformed — treat end-of-string as sentinel + } + + /** + * Decodes the JSON escape sequences that may appear in a key name: + * {@code \/} → {@code /}, {@code \"} → {@code "}, {@code \\} → {@code \}. + */ + private static String unescapeKeyName(String name) { + if (name.contains("\\/")) { + name = name.replace("\\/", "/"); + } + if (name.contains("\\\"")) { + name = name.replace("\\\"", "\""); + } + if (name.contains("\\\\")) { + name = name.replace("\\\\", "\\"); + } + return name; + } + private String escapeJson(String value) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < value.length(); i++) { diff --git a/rt/rs/extensions/json-basic/src/test/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriterTest.java b/rt/rs/extensions/json-basic/src/test/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriterTest.java index c2908b1a75d..a680ecba04d 100644 --- a/rt/rs/extensions/json-basic/src/test/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriterTest.java +++ b/rt/rs/extensions/json-basic/src/test/java/org/apache/cxf/jaxrs/json/basic/JsonMapObjectReaderWriterTest.java @@ -239,6 +239,33 @@ public class JsonMapObjectReaderWriterTest { assertEquals(Boolean.TRUE, map.get("admin")); } + /** + * Regression test for "Key Names With Escaped Quotes Parsed Incorrectly". + * + * <p>{@code readJsonObjectAsSettable} extracts a key name with a plain + * {@code json.indexOf(DQUOTE, i + 1)}, which stops at the first {@code "} it finds + * regardless of whether that quote is escaped. A key that contains an embedded + * escaped quote — e.g. {@code "foo\"bar"} — is therefore truncated: the method + * finds the {@code "} in {@code \"} and returns {@code foo\} instead of + * {@code foo"bar}. + * + * <p>The parser then searches for the value separator {@code :} starting from the + * wrong offset, so the remainder of the key ({@code bar}) and the colon are + * consumed as a suffix of the (wrong) key name. The resulting map contains an + * entry with the wrong key and the test assertion on {@code map.get("foo\"bar")} + * returns {@code null}. + */ + @Test + public void testKeyWithEscapedQuoteIsParsedCorrectly() throws Exception { + // JSON: {"foo\"bar":"value"} — key contains an embedded double-quote character + // Bug: indexOf('"') stops at the \" inside the key, producing truncated key "foo\" + // instead of the correct key foo"bar. + String json = "{\"foo\\\"bar\":\"value\"}"; + Map<String, Object> map = new JsonMapObjectReaderWriter().fromJson(json); + assertEquals(1, map.size()); + assertEquals("value", map.get("foo\"bar")); + } + @Test public void testAlreadyEscapedBackslash() throws Exception { JsonMapObjectReaderWriter jsonMapObjectReaderWriter = new JsonMapObjectReaderWriter();
