diff --git a/CHANGELOG.md b/CHANGELOG.md index bce750b5..9d4d5b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - +### Fixed +- Escape json when writing in html ([#312](https://github.com/cucumber/html-formatter/pull/312)) + ## [21.4.0] - 2024-06-21 ### Changed - Upgrade `react-components` to [22.2.0](https://github.com/cucumber/react-components/releases/tag/v22.2.0) diff --git a/java/src/main/java/io/cucumber/htmlformatter/JsonInHtmlWriter.java b/java/src/main/java/io/cucumber/htmlformatter/JsonInHtmlWriter.java new file mode 100644 index 00000000..63a76b0c --- /dev/null +++ b/java/src/main/java/io/cucumber/htmlformatter/JsonInHtmlWriter.java @@ -0,0 +1,62 @@ +package io.cucumber.htmlformatter; + +import java.io.IOException; +import java.io.Writer; + +/** + * Writes json with the forward slash ({@code /}) escaped. Assumes + * JSON has not been escaped yet. + */ +class JsonInHtmlWriter extends Writer { + private static final int BUFFER_SIZE = 1024; + private final Writer delegate; + private char[] escapeBuffer; + + JsonInHtmlWriter(Writer delegate) { + this.delegate = delegate; + } + + @Override + public void write(char[] source, int offset, int length) throws IOException { + char[] destination = prepareBuffer(); + int flushAt = BUFFER_SIZE - 2; + int written = 0; + for (int i = offset; i < offset + length; i++) { + char c = source[i]; + + // Flush buffer if (nearly) full + if (written >= flushAt) { + delegate.write(destination, 0, written); + written = 0; + } + + // Write with escapes + if (c == '/') { + destination[written++] = '\\'; + } + destination[written++] = c; + } + // Flush any remaining + if (written > 0) { + delegate.write(destination, 0, written); + } + } + + private char[] prepareBuffer() { + // Reuse the same buffer, avoids repeated array allocation + if (escapeBuffer == null) { + escapeBuffer = new char[BUFFER_SIZE]; + } + return escapeBuffer; + } + + @Override + public void flush() throws IOException { + delegate.flush(); + } + + @Override + public void close() throws IOException { + delegate.close(); + } +} diff --git a/java/src/main/java/io/cucumber/htmlformatter/MessagesToHtmlWriter.java b/java/src/main/java/io/cucumber/htmlformatter/MessagesToHtmlWriter.java index e46f8562..8165dea5 100644 --- a/java/src/main/java/io/cucumber/htmlformatter/MessagesToHtmlWriter.java +++ b/java/src/main/java/io/cucumber/htmlformatter/MessagesToHtmlWriter.java @@ -22,6 +22,7 @@ public final class MessagesToHtmlWriter implements AutoCloseable { private final String template; private final Writer writer; + private final JsonInHtmlWriter jsonInHtmlWriter; private final Serializer serializer; private boolean preMessageWritten = false; private boolean postMessageWritten = false; @@ -37,8 +38,10 @@ public MessagesToHtmlWriter(OutputStream outputStream, Serializer serializer) th ); } + private MessagesToHtmlWriter(Writer writer, Serializer serializer) throws IOException { this.writer = writer; + this.jsonInHtmlWriter = new JsonInHtmlWriter(writer); this.serializer = serializer; this.template = readResource("index.mustache.html"); } @@ -77,7 +80,7 @@ public void write(Envelope envelope) throws IOException { writer.write(","); } - serializer.writeValue(writer, envelope); + serializer.writeValue(jsonInHtmlWriter, envelope); } /** @@ -135,9 +138,29 @@ private static String readResource(String name) throws IOException { return new String(baos.toByteArray(), UTF_8); } + /** + * Serializes a message to JSON. + */ @FunctionalInterface public interface Serializer { + /** + * Serialize a message to JSON and write it to the given {@code writer}. + * + *