Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
62 changes: 62 additions & 0 deletions java/src/main/java/io/cucumber/htmlformatter/JsonInHtmlWriter.java
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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");
}
Expand Down Expand Up @@ -77,7 +80,7 @@ public void write(Envelope envelope) throws IOException {
writer.write(",");
}

serializer.writeValue(writer, envelope);
serializer.writeValue(jsonInHtmlWriter, envelope);
}

/**
Expand Down Expand Up @@ -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}.
*
* <ul>
* <li>Values must be included unless their value is {@code null}
* or an "absent" reference values such as empty optionals.
* <li>Enums must be written as strings.
* <li>The solidus {@code /} may not be escaped. Writing json
* into the html context is handled in this implementation.
* <li>Implementations may not close the {@code writer} after
* writing a {@code value}.
* </ul>
*
* @param writer to write to
* @param value to serialize
* @throws IOException if anything goes wrong
*/
void writeValue(Writer writer, Envelope value) throws IOException;

}
Expand Down
Original file line number Diff line number Diff line change
@@ -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("</script><script></script>");
assertEquals("<\\/script><script><\\/script>", output());
}

@Test
void partial_writes() throws IOException {
char[] buffer = new char[100];
String text = "</script><script></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><script><\\/script>", output());
}

@Test
void large_writes_with_odd_boundaries() throws IOException {
char[] buffer = new char[1024];
// This forces the buffer to flush after every 1023 written characters.
buffer[0] = 'a';
Arrays.fill(buffer, 1, buffer.length, '/');
writer.write(buffer);

StringBuilder expected = new StringBuilder();
expected.append("a");
for (int i = 1; i < buffer.length; i++) {
expected.append("\\/");
}
assertEquals(expected.toString(), output());
}


@Test
void really_large_writes() throws IOException {
char[] buffer = new char[2048];
Arrays.fill(buffer, '/');
writer.write(buffer);

StringBuilder expected = new StringBuilder();
for (int i = 0; i < buffer.length; i++) {
expected.append("\\/");
}
assertEquals(expected.toString(), output());
}

@Test
void empty_write() throws IOException {
char[] buffer = new char[0];
writer.write(buffer);
assertEquals("", output());
}

private String output() throws IOException {
writer.flush();
return new String(out.toByteArray(), UTF_8);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,23 @@

import io.cucumber.htmlformatter.MessagesToHtmlWriter.Serializer;
import io.cucumber.messages.Convertor;
import io.cucumber.messages.types.Comment;
import io.cucumber.messages.types.Envelope;
import io.cucumber.messages.types.Feature;
import io.cucumber.messages.types.GherkinDocument;
import io.cucumber.messages.types.Location;
import io.cucumber.messages.types.TestRunFinished;
import io.cucumber.messages.types.TestRunStarted;
import org.junit.jupiter.api.Test;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;

import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonList;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
Expand Down Expand Up @@ -82,6 +89,22 @@ void it_writes_two_messages_separated_by_a_comma() throws IOException {
"window.CUCUMBER_MESSAGES = [{\"testRunStarted\":{\"timestamp\":{\"seconds\":10,\"nanos\":0}}},{\"testRunFinished\":{\"success\":true,\"timestamp\":{\"seconds\":15,\"nanos\":0}}}];"));
}


@Test
void it_escapes_forward_slashes() throws IOException {
Envelope envelope = Envelope.of(new GherkinDocument(
null,
null,
singletonList(new Comment(
new Location(0L, 0L),
"</script><script>alert('Hello')</script>"
))
));
String html = renderAsHtml(envelope);
assertThat(html, containsString(
"window.CUCUMBER_MESSAGES = [{\"gherkinDocument\":{\"comments\":[{\"location\":{\"line\":0,\"column\":0},\"text\":\"<\\/script><script>alert('Hello')<\\/script>\"}]}}];"));
}

private static String renderAsHtml(Envelope... messages) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (MessagesToHtmlWriter messagesToHtmlWriter = new MessagesToHtmlWriter(bytes, serializer)) {
Expand Down
Loading