Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
71 changes: 71 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,71 @@
package io.cucumber.htmlformatter;

import java.io.IOException;
import java.io.Writer;

/**
* Writes json with the forward slash ({@code /}) escaped.
*/
class JsonInHtmlWriter extends Writer {
private static final int WRITE_BUFFER_SIZE = 1024;
private final Writer delegate;
private char[] writeBuffer;

JsonInHtmlWriter(Writer delegate) {
this.delegate = delegate;
}

@Override
public void write(char[] buffer, int offset, int length) throws IOException {
int escapes = countEscapes(buffer, offset, length);
if (escapes == 0) {
delegate.write(buffer, offset, length);
return;
}
int escapedLength = length + escapes;
char[] escapedBuffer = prepareWriteBuffer(escapedLength);
writeEscapeTo(buffer, offset, length, escapedBuffer);
delegate.write(escapedBuffer, 0, escapedLength);
}

private static int countEscapes(char[] source, int startAt, int length) {
int count = 0;
for (int i = startAt; i < startAt + length; i++) {
if (source[i] == '/') {
count++;
}
}
return count;
}

private char[] prepareWriteBuffer(int length) {
if (length > WRITE_BUFFER_SIZE) {
return new char[length];
}
// Reuse the same write buffer, avoid array allocations
if (writeBuffer == null) {
writeBuffer = new char[WRITE_BUFFER_SIZE];
}
return writeBuffer;
}

private static void writeEscapeTo(char[] source, int startAt, int length, char[] destination) {
for (int i = startAt, j = 0; i < startAt + length; i++) {
char c = source[i];
if (c == '/') {
destination[j++] = '\\';
}
destination[j++] = c;
}
}

@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
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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("<script>");
assertEquals("<script>", 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() throws IOException {
char[] buffer = new char[1024];
Arrays.fill(buffer, '/');
writer.write(buffer);

StringBuilder expected = new StringBuilder();
for (int i = 0; i < 1024; i++) {
expected.append("\\/");
}
assertEquals(expected.toString(), 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