Skip to content

Commit 4519e60

Browse files
committed
Version 2.1.0
1 parent 6f28558 commit 4519e60

File tree

8 files changed

+213
-42
lines changed

8 files changed

+213
-42
lines changed

README.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ This is a reference implementation, capable of parsing JSON5 data according to t
1010

1111
## Getting started
1212

13-
In order to use the code, you can either [download the jar](https://github.com/Synt4xErr0r4/json5/releases/download/2.0.0/json5-2.0.0.jar), or use the Maven dependency:
13+
In order to use the code, you can either [download the jar](https://github.com/Synt4xErr0r4/json5/releases/download/2.1.0/json5-2.1.0.jar), or use the Maven dependency:
1414

15-
*Note:* Starting with version 2.0.0, the Maven dependency is now also available from [Maven Central](https://central.sonatype.com/artifact/at.syntaxerror/json5/2.0.0).
15+
*Note:* Starting with version 2.0.0, the Maven dependency is now also available from [Maven Central](https://central.sonatype.com/artifact/at.syntaxerror/json5/2.1.0).
1616

1717
```xml
1818
<!-- Repository -->
@@ -27,7 +27,7 @@ In order to use the code, you can either [download the jar](https://github.com/S
2727
<dependency>
2828
<groupId>at.syntaxerror</groupId>
2929
<artifactId>json5</artifactId>
30-
<version>2.0.0</version>
30+
<version>2.1.0</version>
3131
</dependency>
3232
```
3333

@@ -259,6 +259,14 @@ When the behavior is `DUPLICATE`, the snippet above is effectively equal to the
259259

260260
Note regarding digit separators: Digit separators may neither occur next to each other, nor at the beginning nor the end of a literal. They can also be used within binary/octal/hexadecimal and hexadecimal floating-point literals.
261261

262+
### v2.1.0
263+
264+
- `JSONObject`s now store entries in the order they were added ([#7](https://github.com/Synt4xErr0r4/json5/issues/7))
265+
- added option `allowTrailingData` (*Parser-only*, default `false`):
266+
- if `true`, trailing data after a JSON object or array (e.g. `abc` in `{ }abc`) will be ignored
267+
- otherwise, an exception will be thrown
268+
- various small improvements ([#7](https://github.com/Synt4xErr0r4/json5/issues/7), [#8](https://github.com/Synt4xErr0r4/json5/issues/8), [#9](https://github.com/Synt4xErr0r4/json5/issues/9))
269+
262270
## Documentation
263271

264272
The JavaDoc for the latest version can be found [here](https://javadoc.syntaxerror.at/json5/latest).

pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<modelVersion>4.0.0</modelVersion>
55
<groupId>at.syntaxerror</groupId>
66
<artifactId>json5</artifactId>
7-
<version>2.0.0</version>
7+
<version>2.1.0</version>
88
<packaging>jar</packaging>
99

1010
<name>JSON5 for Java</name>

src/main/java/at/syntaxerror/json5/JSONArray.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@ public JSONArray(JSONParser parser) {
8282
case 0:
8383
throw parser.syntaxError("A JSONArray must end with ']'");
8484
case ']':
85+
if(parser.root && !parser.options.isAllowTrailingData() && parser.nextClean() != 0) {
86+
throw parser.syntaxError("Trailing data after JSONArray");
87+
}
8588
return;
8689
default:
8790
parser.back();
@@ -97,7 +100,7 @@ public JSONArray(JSONParser parser) {
97100
return;
98101

99102
if(c != ',')
100-
throw parser.syntaxError("Expected ',' or ']' after value, got '" + c + "' instead");
103+
throw parser.syntaxError("Expected ',' or ']' after value, got " + JSONParser.charToString(c) + " instead");
101104
}
102105
}
103106

src/main/java/at/syntaxerror/json5/JSONObject.java

+8-5
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@
2727
import java.math.BigInteger;
2828
import java.time.DateTimeException;
2929
import java.time.Instant;
30-
import java.util.HashMap;
3130
import java.util.HashSet;
3231
import java.util.Iterator;
32+
import java.util.LinkedHashMap;
3333
import java.util.Map;
3434
import java.util.Map.Entry;
3535
import java.util.Set;
@@ -56,7 +56,7 @@ public class JSONObject implements Iterable<Map.Entry<String, Object>> {
5656
* Constructs a new JSONObject
5757
*/
5858
public JSONObject() {
59-
values = new HashMap<>();
59+
values = new LinkedHashMap<>();
6060
}
6161

6262
/**
@@ -93,6 +93,9 @@ public JSONObject(JSONParser parser) {
9393
case 0:
9494
throw parser.syntaxError("A JSONObject must end with '}'");
9595
case '}':
96+
if(parser.root && !parser.options.isAllowTrailingData() && parser.nextClean() != 0) {
97+
throw parser.syntaxError("Trailing data after JSONObject");
98+
}
9699
return;
97100
default:
98101
parser.back();
@@ -107,7 +110,7 @@ public JSONObject(JSONParser parser) {
107110
c = parser.nextClean();
108111

109112
if(c != ':')
110-
throw parser.syntaxError("Expected ':' after a key, got '" + c + "' instead");
113+
throw parser.syntaxError("Expected ':' after a key, got " + JSONParser.charToString(c) + " instead");
111114

112115
Object value = parser.nextValue();
113116

@@ -136,7 +139,7 @@ public JSONObject(JSONParser parser) {
136139
return;
137140

138141
if(c != ',')
139-
throw parser.syntaxError("Expected ',' or '}' after value, got '" + c + "' instead");
142+
throw parser.syntaxError("Expected ',' or '}' after value, got " + JSONParser.charToString(c) + " instead");
140143
}
141144
}
142145

@@ -188,7 +191,7 @@ else if(value instanceof JSONObject)
188191
* @return a map of the entries of this object
189192
*/
190193
public Map<String, Object> toMap() {
191-
Map<String, Object> map = new HashMap<>();
194+
Map<String, Object> map = new LinkedHashMap<>();
192195

193196
for(Entry<String, Object> entry : this) {
194197
Object value = entry.getValue();

src/main/java/at/syntaxerror/json5/JSONOptions.java

+24
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,30 @@ public class JSONOptions {
259259
@Builder.Default
260260
DuplicateBehavior duplicateBehaviour = DuplicateBehavior.UNIQUE;
261261

262+
/**
263+
* Specifies whether trailing data should be allowed.<br>
264+
* If {@code false}, parsing the following will produce an error
265+
* due to the trailing {@code abc}:
266+
*
267+
* <pre><code>{ }abc</code></pre>
268+
*
269+
* If {@code true}, however, this will be interpreted as an empty
270+
* {@link JSONObject} and any trailing will be ignored.
271+
* <p>
272+
* Whitespace never counts as trailing data.
273+
* <p>
274+
* Default: {@code false}
275+
* <p>
276+
* <i>This is a {@link JSONParser Parser}-only option</i>
277+
*
278+
* @param allowTrailingData a boolean
279+
*
280+
* @return whether trailing data should be allowed
281+
* @since 2.1.0
282+
*/
283+
@Builder.Default
284+
boolean allowTrailingData = false;
285+
262286
/**
263287
* An enum containing all supported behaviors for duplicate keys
264288
*

src/main/java/at/syntaxerror/json5/JSONParser.java

+85-31
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ public class JSONParser {
4141

4242
private final Reader reader;
4343
protected final JSONOptions options;
44+
45+
/** whether we're currently parsing the root object/array */
46+
protected boolean root = true;
4447

4548
/** whether the end of the file has been reached */
4649
private boolean eof;
@@ -273,7 +276,7 @@ private void nextSingleLineComment() {
273276
public char nextClean() {
274277
while(true) {
275278
if(!more())
276-
throw syntaxError("Unexpected end of data");
279+
return 0;
277280

278281
char n = next();
279282

@@ -302,11 +305,11 @@ private String nextCleanTo(String delimiters) {
302305
StringBuilder result = new StringBuilder();
303306

304307
while(true) {
305-
if(!more())
306-
throw syntaxError("Unexpected end of data");
307-
308308
char n = nextClean();
309309

310+
if(n == 0)
311+
return null;
312+
310313
if(delimiters.indexOf(n) > -1 || isWhitespace(n)) {
311314
back();
312315
break;
@@ -372,7 +375,7 @@ private String nextString(char quote) {
372375

373376
while(true) {
374377
if(!more())
375-
throw syntaxError("Unexpected end of data");
378+
throw syntaxError("Expected '" + quote + "' to close string, got EOF instead");
376379

377380
prev = n;
378381
n = next();
@@ -397,6 +400,9 @@ private String nextString(char quote) {
397400
}
398401

399402
else switch(n) {
403+
case 0:
404+
throw syntaxError("Expected escape sequence in string, got EOF instead");
405+
400406
case '\'':
401407
case '"':
402408
case '\\':
@@ -436,6 +442,10 @@ else switch(n) {
436442

437443
for(int i = 0; i < 2; ++i) {
438444
n = next();
445+
446+
if(n == 0)
447+
throw syntaxError("Expected hexadecimal digit for hexadecimal escape sequence in string, got EOF instead");
448+
439449
value += n;
440450

441451
int hex = dehex(n);
@@ -519,23 +529,26 @@ public String nextMemberName() {
519529
char prev;
520530
char n = next();
521531

532+
if(n == 0)
533+
throw syntaxError("Expected key, got EOF instead");
534+
522535
if(n == '"' || n == '\'')
523536
return nextString(n);
524537

525538
back();
526539
n = 0;
527540

528-
while(true) {
529-
if(!more())
530-
throw syntaxError("Unexpected end of data");
531-
541+
do {
532542
boolean part = result.length() > 0;
533543

534544
prev = n;
535545
n = next();
536546

537547
if(n == '\\') { // unicode escape sequence
538548
n = next();
549+
550+
if(n == 0)
551+
throw syntaxError("Expected escape sequence in key, got EOF instead");
539552

540553
if(n != 'u' && n != 'U')
541554
throw syntaxError("Illegal escape sequence '\\" + n + "' in key");
@@ -560,10 +573,10 @@ else if(!isMemberNameChar(n, part)) {
560573
checkSurrogate(prev, n);
561574

562575
result.append(n);
563-
}
576+
} while(more());
564577

565578
if(result.length() == 0)
566-
throw syntaxError("Empty key");
579+
throw syntaxError("Expected key");
567580

568581
return result.toString();
569582
}
@@ -572,27 +585,37 @@ else if(!isMemberNameChar(n, part)) {
572585
* Reads a value from the source according to the
573586
* <a href="https://spec.json5.org/#prod-JSON5Value">JSON5 Specification</a>
574587
*
575-
* @return an member name
588+
* @return a value
576589
*/
577590
public Object nextValue() {
578591
char n = nextClean();
592+
boolean wasRoot = root;
579593

580-
switch(n) {
581-
case '"':
582-
case '\'':
583-
return nextString(n);
584-
case '{':
585-
back();
586-
return new JSONObject(this);
587-
case '[':
588-
back();
589-
return new JSONArray(this);
594+
try {
595+
switch(n) {
596+
case '"':
597+
case '\'':
598+
return nextString(n);
599+
case '{':
600+
back();
601+
root = false;
602+
return new JSONObject(this);
603+
case '[':
604+
back();
605+
root = false;
606+
return new JSONArray(this);
607+
}
608+
} finally {
609+
root = wasRoot;
590610
}
591611

592612
back();
593613

594614
String string = nextCleanTo(",]}");
595615

616+
if(string == null)
617+
throw syntaxError("Expected value, got EOF instead");
618+
596619
if(string.equals("null"))
597620
return null;
598621

@@ -636,17 +659,15 @@ else if(leading == '-') {
636659
if((leading >= '0' && leading <= '9') || leading == '.') {
637660
Number num = parseNumber(leading, rest);
638661

639-
if(num != null) {
640-
if(sign < 0) {
641-
if(num instanceof BigInteger)
642-
return ((BigInteger) num).negate();
643-
644-
if(num instanceof BigDecimal)
645-
return ((BigDecimal) num).negate();
646-
}
662+
if(sign < 0) {
663+
if(num instanceof BigInteger)
664+
return ((BigInteger) num).negate();
647665

648-
return num;
666+
if(num instanceof BigDecimal)
667+
return ((BigDecimal) num).negate();
649668
}
669+
670+
return num;
650671
}
651672
}
652673
}
@@ -1023,5 +1044,38 @@ private static boolean ishex(char c) {
10231044
|| (c >= 'a' && c <= 'f')
10241045
|| (c >= 'A' && c <= 'F');
10251046
}
1047+
1048+
/**
1049+
* Converts a character into a string representation:
1050+
*
1051+
* <ul>
1052+
* <li>if {@code c == 0}, {@code "EOF"} is returned</li>
1053+
* <li>if {@code c} fulfills one of the following conditions, {@code "'x'"}
1054+
* is returned, where {@code x} is the value returned by {@link Character#toString(char)}:
1055+
* <ul>
1056+
* <li>if {@code c} is an extended ASCII character ({@code U+0001-U+00FF}),
1057+
* except {@link Character#isISOControl(char) control characters}</li>
1058+
* <li>if {@code c} is a {@link Character#isLetter(char) Unicode letter}</li>
1059+
* <li>if {@code c} is a {@link Character#isDigit(char) Unicode digit}</li>
1060+
* </ul>
1061+
* </li>
1062+
* <li>otherwise, {@code "U+XXXX"} is returned, where {@code XXXX} is the uppercase
1063+
* hexadecimal representation of {@code c}'s Unicode codepoint, padded with zeros
1064+
* ({@code 0}) to a length of 4 characters</li>
1065+
* </ul>
1066+
*
1067+
* @param c the character
1068+
* @return the string representation
1069+
* @since 2.1.0
1070+
*/
1071+
protected static String charToString(char c) {
1072+
if(c == 0)
1073+
return "EOF";
1074+
1075+
if((c <= 0xFF && !Character.isISOControl(c)) || Character.isLetterOrDigit(c))
1076+
return "'" + c + "'";
1077+
1078+
return String.format("U+%04X", (int) c);
1079+
}
10261080

10271081
}

src/main/java/at/syntaxerror/json5/JSONStringify.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,9 @@ private static String quote(String string, JSONOptions options) {
288288

289289
quoted.append(qt);
290290

291-
for(char c : string.toCharArray()) {
291+
for(int i = 0, n = string.length(); i < n; ++i) {
292+
char c = string.charAt(i);
293+
292294
if(c == qt) {
293295
quoted.append('\\');
294296
quoted.append(c);

0 commit comments

Comments
 (0)