Skip to content

Commit c50edd6

Browse files
authored
Escape all control chars (#36)
1 parent dd8dad4 commit c50edd6

File tree

7 files changed

+51
-85
lines changed

7 files changed

+51
-85
lines changed

assets/charset2_result.hjson

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
uescape: "\u0000,\u0001,\uffff"
3+
"um\u000blaut": äöüßÄÖÜ
4+
hex: ģ䕧覫췯ꯍ
5+
}

assets/charset2_result.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"uescape": "\u0000,\u0001,\uffff",
3+
"um\u000blaut": "äöüßÄÖÜ",
4+
"hex": "ģ䕧覫췯ꯍ"
5+
}

assets/charset2_test.hjson

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
uescape: "\u0000,\u0001,\uffff"
3+
"um\u000blaut": äöüßÄÖÜ
4+
hex: "\u0123\u4567\u89AB\uCDEF\uabcd\uef4A"
5+
}

assets/testlist.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
charset_test.hjson
2+
charset2_test.hjson
23
comments_test.hjson
34
empty_test.hjson
45
failCharset1_test.hjson
@@ -83,4 +84,4 @@ stringify/quotes_strings_ml_test.json
8384
stringify/quotes_strings_test.hjson
8485
extra/notabs_test.json
8586
extra/root_test.hjson
86-
extra/separator_test.json
87+
extra/separator_test.json

src/main/org/hjson/HjsonWriter.java

Lines changed: 19 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ class HjsonWriter {
3030

3131
private IHjsonDsfProvider[] dsfProviders;
3232

33+
static String commonRange = "\\x7f-\\x9f\\x{00ad}\\x{0600}-\\x{0604}\\x{070f}\\x{17b4}\\x{17b5}\\x{200c}-\\x{200f}\\x{2028}-\\x{202f}\\x{2060}-\\x{206f}\\x{feff}\\x{fff0}-\\x{ffff}";
34+
// needsEscape tests if the string can be written without escapes
35+
static Pattern needsEscape = Pattern.compile("[\\\\\\\"\\x00-\\x1f" + commonRange + "]");
36+
// needsQuotes tests if the string can be written as a quoteless string (includes needsEscape but without \\\\ and \\")
37+
static Pattern needsQuotes = Pattern.compile("^\\s|^\"|^'|^#|^/\\*|^//|^\\{|^\\}|^\\[|^\\]|^:|^,|\\s$|[\\x00-\\x1f\\x7f-\\x9f\\x{00ad}\\x{0600}-\\x{0604}\\x{070f}\\x{17b4}\\x{17b5}\\x{200c}-\\x{200f}\\x{2028}-\\x{202f}\\x{2060}-\\x{206f}\\x{feff}\\x{fff0}-\\x{ffff}]");
38+
// needsEscapeML tests if the string can be written as a multiline string (like needsEscape but without \\n, \\\\, \\", \\t)
39+
static Pattern needsEscapeML = Pattern.compile("'''|^[\\s]+$|[\\x00-\\x08\\x0b-\\x1f" + commonRange + "]");
3340
static Pattern needsEscapeName=Pattern.compile("[,\\{\\[\\}\\]\\s:#\"']|//|/\\*");
3441

3542
public HjsonWriter(HjsonOptions options) {
@@ -104,22 +111,21 @@ public void save(JsonValue value, Writer tw, int level, String separator, boolea
104111
}
105112

106113
static String escapeName(String name) {
107-
if (name.length()==0 || needsEscapeName.matcher(name).find())
114+
if (name.length()==0 || needsEscapeName.matcher(name).find() ||
115+
needsEscape.matcher(name).find())
116+
{
108117
return "\""+JsonWriter.escapeString(name)+"\"";
109-
else
118+
} else {
110119
return name;
120+
}
111121
}
112122

113123
void writeString(String value, Writer tw, int level, String separator) throws IOException {
114124
if (value.length()==0) { tw.write(separator+"\"\""); return; }
115125

116126
char left=value.charAt(0), right=value.charAt(value.length()-1);
117127
char left1=value.length()>1?value.charAt(1):'\0', left2=value.length()>2?value.charAt(2):'\0';
118-
boolean doEscape=false;
119-
char[] valuec=value.toCharArray();
120-
for(char ch : valuec) {
121-
if (needsQuotes(ch)) { doEscape=true; break; }
122-
}
128+
boolean doEscape=needsQuotes.matcher(value).find();
123129

124130
if (doEscape ||
125131
HjsonParser.isWhiteSpace(left) || HjsonParser.isWhiteSpace(right) ||
@@ -136,17 +142,13 @@ void writeString(String value, Writer tw, int level, String separator) throws IO
136142
// format or we must replace the offending characters with safe escape
137143
// sequences.
138144

139-
boolean noEscape=true;
140-
for(char ch : valuec) { if (needsEscape(ch)) { noEscape=false; break; } }
141-
if (noEscape) { tw.write(separator+"\""+value+"\""); return; }
142-
143-
boolean noEscapeML=true, allWhite=true;
144-
for(char ch : valuec) {
145-
if (needsEscapeML(ch)) { noEscapeML=false; break; }
146-
else if (!HjsonParser.isWhiteSpace(ch)) allWhite=false;
145+
if (!needsEscape.matcher(value).find()) {
146+
tw.write(separator+"\""+value+"\"");
147+
} else if (!needsEscapeML.matcher(value).find()) {
148+
writeMLString(value, tw, level, separator);
149+
} else {
150+
tw.write(separator+"\""+JsonWriter.escapeString(value)+"\"");
147151
}
148-
if (noEscapeML && !allWhite && !value.contains("'''")) writeMLString(value, tw, level, separator);
149-
else tw.write(separator+"\""+JsonWriter.escapeString(value)+"\"");
150152
}
151153
else tw.write(separator+value);
152154
}
@@ -183,38 +185,4 @@ static boolean startsWithKeyword(String text) {
183185
char ch=text.charAt(p);
184186
return ch==',' || ch=='}' || ch==']' || ch=='#' || ch=='/' && (text.length()>p+1 && (text.charAt(p+1)=='/' || text.charAt(p+1)=='*'));
185187
}
186-
187-
static boolean needsQuotes(char c) {
188-
switch (c) {
189-
case '\t':
190-
case '\f':
191-
case '\b':
192-
case '\n':
193-
case '\r':
194-
return true;
195-
default:
196-
return false;
197-
}
198-
}
199-
200-
static boolean needsEscape(char c) {
201-
switch (c) {
202-
case '\"':
203-
case '\\':
204-
return true;
205-
default:
206-
return needsQuotes(c);
207-
}
208-
}
209-
210-
static boolean needsEscapeML(char c) {
211-
switch (c) {
212-
case '\n':
213-
case '\r':
214-
case '\t':
215-
return false;
216-
default:
217-
return needsQuotes(c);
218-
}
219-
}
220188
}

src/main/org/hjson/JsonWriter.java

Lines changed: 15 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import java.io.IOException;
2626
import java.io.Writer;
27+
import java.util.regex.Matcher;
2728

2829

2930
class JsonWriter {
@@ -90,42 +91,25 @@ public void save(JsonValue value, Writer tw, int level) throws IOException {
9091
}
9192
}
9293

93-
static String escapeName(String name) {
94-
boolean needsEscape=name.length()==0;
95-
for(char ch : name.toCharArray()) {
96-
if (HjsonParser.isWhiteSpace(ch) || ch=='{' || ch=='}' || ch=='[' || ch==']' || ch==',' || ch==':') {
97-
needsEscape=true;
98-
break;
99-
}
100-
}
101-
if (needsEscape) return "\""+JsonWriter.escapeString(name)+"\"";
102-
else return name;
103-
}
104-
10594
static String escapeString(String src) {
10695
if (src==null) return null;
10796

108-
for (int i=0; i<src.length(); i++) {
109-
if (getEscapedChar(src.charAt(i))!=null) {
110-
StringBuilder sb=new StringBuilder();
111-
if (i>0) sb.append(src, 0, i);
112-
return doEscapeString(sb, src, i);
113-
}
97+
int i = 0;
98+
StringBuilder sb=new StringBuilder();
99+
Matcher m = HjsonWriter.needsEscape.matcher(src);
100+
101+
while (m.find()) {
102+
// Assume all matches are single chars.
103+
sb.append(src, i, m.start()).append(getEscapedChar(m.group().charAt(0)));
104+
i = m.end();
114105
}
115-
return src;
116-
}
117106

118-
private static String doEscapeString(StringBuilder sb, String src, int cur) {
119-
int start=cur;
120-
for (int i=cur; i<src.length(); i++) {
121-
String escaped=getEscapedChar(src.charAt(i));
122-
if (escaped!=null) {
123-
sb.append(src, start, i);
124-
sb.append(escaped);
125-
start=i+1;
126-
}
107+
if (i < 1) {
108+
return src;
127109
}
128-
sb.append(src, start, src.length());
110+
111+
sb.append(src, i, src.length());
112+
129113
return sb.toString();
130114
}
131115

@@ -138,7 +122,7 @@ private static String getEscapedChar(char c) {
138122
case '\f': return "\\f";
139123
case '\b': return "\\b";
140124
case '\\': return "\\\\";
141-
default: return null;
125+
default: return "\\u" + String.format("%04x", (int) c);
142126
}
143127
}
144128
}

src/test/org/hjson/test/Main.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,6 @@ private static String load(String file, boolean cr) throws Exception {
2828
}
2929

3030
private static boolean test(String name, String file, boolean inputCr, boolean outputCr) throws Exception {
31-
int extIdx=file.lastIndexOf('.');
32-
boolean isJson=extIdx>=0 && file.substring(extIdx).equals(".json");
3331
boolean shouldFail=name.startsWith("fail");
3432

3533
JsonValue.setEol(outputCr?"\r\n":"\n");

0 commit comments

Comments
 (0)