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}. + * + * + * + * @param writer to write to + * @param value to serialize + * @throws IOException if anything goes wrong + */ void writeValue(Writer writer, Envelope value) throws IOException; } diff --git a/java/src/test/java/io/cucumber/htmlformatter/JsonInHtmlWriterTest.java b/java/src/test/java/io/cucumber/htmlformatter/JsonInHtmlWriterTest.java new file mode 100644 index 00000000..bf2afa40 --- /dev/null +++ b/java/src/test/java/io/cucumber/htmlformatter/JsonInHtmlWriterTest.java @@ -0,0 +1,95 @@ +package io.cucumber.htmlformatter; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.Arrays; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.assertEquals; + +class JsonInHtmlWriterTest { + + private final ByteArrayOutputStream out = new ByteArrayOutputStream(); + private final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(out, UTF_8); + private final JsonInHtmlWriter writer = new JsonInHtmlWriter(outputStreamWriter); + + @Test + void writes() throws IOException { + writer.write("{\"hello\": \"world\"}"); + assertEquals("{\"hello\": \"world\"}", output()); + } + + @Test + void escapes_single() throws IOException { + writer.write("/"); + assertEquals("\\/", output()); + } + + @Test + void escapes_multiple() throws IOException { + writer.write(""); + assertEquals("<\\/script>"; + + text.getChars(0, 9, buffer, 0); + writer.write(buffer, 0, 9); + + text.getChars(9, 17, buffer, 2); + writer.write(buffer, 2, 8); + + text.getChars(17, 26, buffer, 4); + writer.write(buffer, 4, 9); + + assertEquals("<\\/script>" + )) + )); + String html = renderAsHtml(envelope); + assertThat(html, containsString( + "window.CUCUMBER_MESSAGES = [{\"gherkinDocument\":{\"comments\":[{\"location\":{\"line\":0,\"column\":0},\"text\":\"<\\/script>`, + }, + ], + }, + } + const html = await renderAsHtml(e1) + assert( + html.indexOf( + `window.CUCUMBER_MESSAGES = [{"gherkinDocument":{"comments":[{"location":{"line":0,"column":0},"text":"<\\/script>' + )] + ) + ) + end it 'appends the message to out' do formatter.write_message(message) @@ -75,6 +88,15 @@ def script expect(out.string).to eq("#{message.to_json},\n#{message.to_json}") end + + it 'escapes forward slashes' do + + formatter.write_message(message_with_slashes) + + expect(out.string).to eq('{"gherkinDocument":{"comments":[{"location":{"line":0,"column":0},"text":"<\/script>