Skip to content

Commit ae661e6

Browse files
committed
fix: proper JSON and CSV escaping for filenames in metrics output
- Added escapeJson() method to handle quotes, backslashes, and control characters - Added escapeCsv() method to handle commas, quotes, and newlines - Fixed malformed string literals in escape sequences - Prevents broken output when filenames contain special characters - Ensures downstream parsing of JSON/CSV metrics works correctly The escaping properly handles: - JSON: quotes, backslashes, control characters, Unicode - CSV: commas, quotes, newlines (wraps in quotes when needed)
1 parent 2b5bdfe commit ae661e6

File tree

1 file changed

+88
-5
lines changed

1 file changed

+88
-5
lines changed

json-java21-schema/src/test/java/io/github/simbo1905/json/schema/JsonSchemaCheckIT.java

Lines changed: 88 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.io.File;
1212
import java.nio.file.Files;
1313
import java.nio.file.Path;
14+
import java.time.format.DateTimeFormatter;
1415
import java.util.concurrent.ConcurrentHashMap;
1516
import java.util.concurrent.atomic.LongAdder;
1617
import java.util.stream.Stream;
@@ -165,7 +166,7 @@ static void printAndPersistMetrics() throws Exception {
165166
if (!METRICS_FMT.isEmpty()) {
166167
var outDir = java.nio.file.Path.of("target");
167168
java.nio.file.Files.createDirectories(outDir);
168-
var ts = java.time.OffsetDateTime.now().toString();
169+
var ts = java.time.Instant.now();
169170
if ("json".equalsIgnoreCase(METRICS_FMT)) {
170171
var json = buildJsonSummary(strict, ts);
171172
java.nio.file.Files.writeString(outDir.resolve("json-schema-compat.json"), json);
@@ -176,7 +177,7 @@ static void printAndPersistMetrics() throws Exception {
176177
}
177178
}
178179

179-
private static String buildJsonSummary(boolean strict, String timestamp) {
180+
private static String buildJsonSummary(boolean strict, java.time.Instant timestamp) {
180181
var totals = new StringBuilder();
181182
totals.append("{\n");
182183
totals.append(" \"mode\": \"").append(strict ? "STRICT" : "LENIENT").append("\",\n");
@@ -203,7 +204,7 @@ private static String buildJsonSummary(boolean strict, String timestamp) {
203204
if (!first) totals.append(",\n");
204205
first = false;
205206
totals.append(" {\n");
206-
totals.append(" \"file\": \"").append(file).append("\",\n");
207+
totals.append(" \"file\": ").append(escapeJson(file)).append(",\n");
207208
totals.append(" \"groups\": ").append(counters.groups.sum()).append(",\n");
208209
totals.append(" \"tests\": ").append(counters.tests.sum()).append(",\n");
209210
totals.append(" \"run\": ").append(counters.run.sum()).append(",\n");
@@ -219,7 +220,7 @@ private static String buildJsonSummary(boolean strict, String timestamp) {
219220
return totals.toString();
220221
}
221222

222-
private static String buildCsvSummary(boolean strict, String timestamp) {
223+
private static String buildCsvSummary(boolean strict, java.time.Instant timestamp) {
223224
var csv = new StringBuilder();
224225
csv.append("mode,timestamp,groupsDiscovered,testsDiscovered,validationsRun,passed,failed,skipUnsupportedGroup,skipTestException,skipLenientMismatch\n");
225226
csv.append(strict ? "STRICT" : "LENIENT").append(",");
@@ -240,7 +241,7 @@ private static String buildCsvSummary(boolean strict, String timestamp) {
240241
java.util.Collections.sort(files);
241242
for (String file : files) {
242243
var counters = METRICS.perFile.get(file);
243-
csv.append(file).append(",");
244+
csv.append(escapeCsv(file)).append(",");
244245
csv.append(counters.groups.sum()).append(",");
245246
csv.append(counters.tests.sum()).append(",");
246247
csv.append(counters.run.sum()).append(",");
@@ -252,6 +253,88 @@ private static String buildCsvSummary(boolean strict, String timestamp) {
252253
}
253254
return csv.toString();
254255
}
256+
257+
/**
258+
* Escapes a string for safe inclusion in JSON output.
259+
* Handles quotes, backslashes, and control characters.
260+
*/
261+
private static String escapeJson(String input) {
262+
if (input == null) {
263+
return "null";
264+
}
265+
266+
StringBuilder result = new StringBuilder();
267+
result.append('"');
268+
269+
for (int i = 0; i < input.length(); i++) {
270+
char ch = input.charAt(i);
271+
switch (ch) {
272+
case '"':
273+
result.append("\\\"");
274+
break;
275+
case '\\':
276+
result.append("\\\\");
277+
break;
278+
case '\b':
279+
result.append("\\b");
280+
break;
281+
case '\f':
282+
result.append("\\f");
283+
break;
284+
case '\n':
285+
result.append("\\n");
286+
break;
287+
case '\r':
288+
result.append("\\r");
289+
break;
290+
case '\t':
291+
result.append("\\t");
292+
break;
293+
default:
294+
if (ch < 0x20 || ch > 0x7e) {
295+
// Unicode escape for control characters and non-ASCII
296+
result.append("\\u").append(String.format("%04x", (int) ch));
297+
} else {
298+
result.append(ch);
299+
}
300+
}
301+
}
302+
303+
result.append('"');
304+
return result.toString();
305+
}
306+
307+
/**
308+
* Escapes a string for safe inclusion in CSV output.
309+
* Handles commas, quotes, and newlines by wrapping in quotes if needed.
310+
*/
311+
private static String escapeCsv(String input) {
312+
if (input == null) {
313+
return "";
314+
}
315+
316+
// Check if escaping is needed
317+
boolean needsEscaping = input.contains(",") || input.contains("\"") ||
318+
input.contains("\n") || input.contains("\r");
319+
320+
if (!needsEscaping) {
321+
return input;
322+
}
323+
324+
// Wrap in quotes and escape internal quotes
325+
StringBuilder result = new StringBuilder();
326+
result.append('"');
327+
for (int i = 0; i < input.length(); i++) {
328+
char ch = input.charAt(i);
329+
if (ch == '"') {
330+
result.append("\"\""); // Double quotes to escape
331+
} else {
332+
result.append(ch);
333+
}
334+
}
335+
result.append('"');
336+
return result.toString();
337+
}
255338
}
256339

257340
/**

0 commit comments

Comments
 (0)