1111import java .io .File ;
1212import java .nio .file .Files ;
1313import java .nio .file .Path ;
14+ import java .time .format .DateTimeFormatter ;
1415import java .util .concurrent .ConcurrentHashMap ;
1516import java .util .concurrent .atomic .LongAdder ;
1617import 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