From becf8e0137cee89be60f71dc823e8985f66d1b9a Mon Sep 17 00:00:00 2001 From: piexel Date: Tue, 27 Oct 2026 17:47:16 +0100 Subject: [PATCH] Skip JSONC comments in JSONTokener --- .../arcane/volmlib/util/json/JSONTokener.java | 41 +++++++++++++- .../volmlib/util/json/JSONCommentTest.java | 56 +++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 shared/src/test/java/art/arcane/volmlib/util/json/JSONCommentTest.java diff --git a/shared/src/main/java/art/arcane/volmlib/util/json/JSONTokener.java b/shared/src/main/java/art/arcane/volmlib/util/json/JSONTokener.java index c32eea36..48120d81 100644 --- a/shared/src/main/java/art/arcane/volmlib/util/json/JSONTokener.java +++ b/shared/src/main/java/art/arcane/volmlib/util/json/JSONTokener.java @@ -208,19 +208,58 @@ public String next(int n) throws JSONException { } /** - * Get the next char in the string, skipping whitespace. + * Get the next char in the string, skipping whitespace and JSONC comments. * * @return A character, or 0 if there are no more characters. */ public char nextClean() throws JSONException { for (; ; ) { char c = this.next(); + if (c == '/' && this.skipComment()) { + continue; + } if (c == 0 || c > ' ') { return c; } } } + private boolean skipComment() throws JSONException { + switch (this.next()) { + case '/': + this.skipLineComment(); + return true; + case '*': + this.skipBlockComment(); + return true; + default: + this.back(); + return false; + } + } + + private void skipLineComment() throws JSONException { + char c; + do { + c = this.next(); + } while (c != '\n' && c != '\r' && c != 0); + } + + private void skipBlockComment() throws JSONException { + for (; ; ) { + char c = this.next(); + if (c == 0) { + throw this.syntaxError("Unclosed comment"); + } + if (c == '*') { + if (this.next() == '/') { + return; + } + this.back(); + } + } + } + /** * Return the characters up to the next close quote character. Backslash * processing is done. The formal JSON format does not allow strings in diff --git a/shared/src/test/java/art/arcane/volmlib/util/json/JSONCommentTest.java b/shared/src/test/java/art/arcane/volmlib/util/json/JSONCommentTest.java new file mode 100644 index 00000000..65eee989 --- /dev/null +++ b/shared/src/test/java/art/arcane/volmlib/util/json/JSONCommentTest.java @@ -0,0 +1,56 @@ +package art.arcane.volmlib.util.json; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class JSONCommentTest { + @Test + public void lineCommentsAreSkippedAroundAndBetweenTokens() { + JSONObject object = new JSONObject(""" + // Folder-specific settings + { + "a": 1, // trailing note + // whole-line note + "b": [2, // inside array + 3] + } + // after root + """); + + assertEquals(1, object.getInt("a")); + assertEquals(2, object.getJSONArray("b").length()); + assertEquals(3, object.getJSONArray("b").getInt(1)); + } + + @Test + public void blockCommentsAreSkippedIncludingMultiLineAndStarRuns() { + JSONObject object = new JSONObject(""" + /* header + * spanning lines **/ + {"a": /* before value */ 1, /* between pairs */ "b" /* before colon */ : 2} + """); + + assertEquals(1, object.getInt("a")); + assertEquals(2, object.getInt("b")); + } + + @Test + public void commentMarkersInsideStringsAreKept() { + JSONObject object = new JSONObject("{\"url\": \"https://volmit.com//x\", \"text\": \"/* not a comment */\"}"); + + assertEquals("https://volmit.com//x", object.getString("url")); + assertEquals("/* not a comment */", object.getString("text")); + } + + @Test + public void unclosedBlockCommentIsASyntaxError() { + assertThrows(JSONException.class, () -> new JSONObject("{\"a\": 1 /* never closed")); + } + + @Test + public void loneSlashIsStillASyntaxError() { + assertThrows(JSONException.class, () -> new JSONObject("{\"a\": 1 / \"b\": 2}")); + } +}