Skip to content

Commit e9f040a

Browse files
committed
Escape json when writing in html
We're writing the json messages inside a `<script>` element. This means that the `</script>` element must be escaped. Or more generally, any `/`.
1 parent e170ca1 commit e9f040a

File tree

9 files changed

+467
-264
lines changed

9 files changed

+467
-264
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.cucumber.htmlformatter;
2+
3+
import java.io.IOException;
4+
import java.io.Writer;
5+
6+
/**
7+
* Writes json in HTML. So with the backslash {@code \} escaped.
8+
*/
9+
class JsonInHtmlWriter extends Writer {
10+
private static final int WRITE_BUFFER_SIZE = 1024;
11+
private final Writer delegate;
12+
private char[] writeBuffer;
13+
14+
JsonInHtmlWriter(Writer delegate) {
15+
this.delegate = delegate;
16+
}
17+
18+
@Override
19+
public void write(char[] buffer, int offset, int length) throws IOException {
20+
int escapes = countEscapes(buffer, offset, length);
21+
if (escapes == 0) {
22+
delegate.write(buffer, offset, length);
23+
return;
24+
}
25+
int escapedLength = length + escapes;
26+
char[] escaped = prepareBuffer(escapedLength);
27+
escape(buffer, offset, length, escaped);
28+
delegate.write(escaped, 0, escapedLength);
29+
}
30+
31+
private static int countEscapes(char[] source, int startAt, int length) {
32+
int count = 0;
33+
for (int i = startAt; i < startAt + length; i++) {
34+
if (source[i] == '/') {
35+
count++;
36+
}
37+
}
38+
return count;
39+
}
40+
41+
private char[] prepareBuffer(int length) {
42+
if (length > WRITE_BUFFER_SIZE) {
43+
return new char[length];
44+
}
45+
// Reuse the same write buffer, avoid array allocations
46+
if (writeBuffer == null) {
47+
writeBuffer = new char[WRITE_BUFFER_SIZE];
48+
}
49+
return writeBuffer;
50+
}
51+
52+
private static void escape(char[] source, int startAt, int length, char[] destination) {
53+
for (int i = startAt, j = 0; i < startAt + length; i++) {
54+
char c = source[i];
55+
if (c == '/') {
56+
destination[j++] = '\\';
57+
}
58+
destination[j++] = c;
59+
}
60+
}
61+
62+
@Override
63+
public void flush() throws IOException {
64+
delegate.flush();
65+
}
66+
67+
@Override
68+
public void close() throws IOException {
69+
delegate.close();
70+
}
71+
}

java/src/main/java/io/cucumber/htmlformatter/MessagesToHtmlWriter.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
public final class MessagesToHtmlWriter implements AutoCloseable {
2323
private final String template;
2424
private final Writer writer;
25+
private final JsonInHtmlWriter jsonInHtmlWriter;
2526
private final Serializer serializer;
2627
private boolean preMessageWritten = false;
2728
private boolean postMessageWritten = false;
@@ -37,8 +38,10 @@ public MessagesToHtmlWriter(OutputStream outputStream, Serializer serializer) th
3738
);
3839
}
3940

41+
4042
private MessagesToHtmlWriter(Writer writer, Serializer serializer) throws IOException {
4143
this.writer = writer;
44+
this.jsonInHtmlWriter = new JsonInHtmlWriter(writer);
4245
this.serializer = serializer;
4346
this.template = readResource("index.mustache.html");
4447
}
@@ -77,7 +80,7 @@ public void write(Envelope envelope) throws IOException {
7780
writer.write(",");
7881
}
7982

80-
serializer.writeValue(writer, envelope);
83+
serializer.writeValue(jsonInHtmlWriter, envelope);
8184
}
8285

8386
/**
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package io.cucumber.htmlformatter;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.IOException;
7+
import java.io.OutputStreamWriter;
8+
import java.util.Arrays;
9+
10+
import static java.nio.charset.StandardCharsets.UTF_8;
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
13+
class JsonInHtmlWriterTest {
14+
15+
private final ByteArrayOutputStream out = new ByteArrayOutputStream();
16+
private final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(out, UTF_8);
17+
private final JsonInHtmlWriter writer = new JsonInHtmlWriter(outputStreamWriter);
18+
19+
@Test
20+
void writes() throws IOException {
21+
writer.write("<script>");
22+
assertEquals("<script>", output());
23+
}
24+
25+
@Test
26+
void escapes_single() throws IOException {
27+
writer.write("/");
28+
assertEquals("\\/", output());
29+
}
30+
31+
@Test
32+
void escapes_multiple() throws IOException {
33+
writer.write("</script><script></script>");
34+
assertEquals("<\\/script><script><\\/script>", output());
35+
}
36+
37+
@Test
38+
void partial_writes() throws IOException {
39+
char[] buffer = new char[100];
40+
String text = "</script><script></script>";
41+
42+
text.getChars(0, 9, buffer, 0);
43+
writer.write(buffer, 0, 9);
44+
45+
text.getChars(9, 17, buffer, 2);
46+
writer.write(buffer, 2, 8);
47+
48+
text.getChars(17, 26, buffer, 4);
49+
writer.write(buffer, 4, 9);
50+
51+
assertEquals("<\\/script><script><\\/script>", output());
52+
}
53+
54+
@Test
55+
void large_writes() throws IOException {
56+
char[] buffer = new char[1024];
57+
Arrays.fill(buffer, '/');
58+
writer.write(buffer);
59+
60+
StringBuilder expected = new StringBuilder();
61+
for (int i = 0; i < 1024; i++) {
62+
expected.append("\\/");
63+
}
64+
assertEquals(expected.toString(), output());
65+
}
66+
67+
private String output() throws IOException {
68+
writer.flush();
69+
return new String(out.toByteArray(), UTF_8);
70+
}
71+
}

java/src/test/java/io/cucumber/htmlformatter/MessagesToHtmlWriterTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,23 @@
22

33
import io.cucumber.htmlformatter.MessagesToHtmlWriter.Serializer;
44
import io.cucumber.messages.Convertor;
5+
import io.cucumber.messages.types.Comment;
56
import io.cucumber.messages.types.Envelope;
7+
import io.cucumber.messages.types.Feature;
8+
import io.cucumber.messages.types.GherkinDocument;
9+
import io.cucumber.messages.types.Location;
610
import io.cucumber.messages.types.TestRunFinished;
711
import io.cucumber.messages.types.TestRunStarted;
812
import org.junit.jupiter.api.Test;
913

1014
import java.io.ByteArrayOutputStream;
1115
import java.io.IOException;
1216
import java.time.Instant;
17+
import java.util.Arrays;
18+
import java.util.Collections;
1319

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

92+
93+
@Test
94+
void it_escapes_forward_slashes() throws IOException {
95+
Envelope envelope = Envelope.of(new GherkinDocument(
96+
null,
97+
null,
98+
singletonList(new Comment(
99+
new Location(0L, 0L),
100+
"</script><script>alert('Hello')</script>"
101+
))
102+
));
103+
String html = renderAsHtml(envelope);
104+
assertThat(html, containsString(
105+
"window.CUCUMBER_MESSAGES = [{\"gherkinDocument\":{\"comments\":[{\"location\":{\"line\":0,\"column\":0},\"text\":\"<\\/script><script>alert('Hello')<\\/script>\"}]}}];"));
106+
}
107+
85108
private static String renderAsHtml(Envelope... messages) throws IOException {
86109
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
87110
try (MessagesToHtmlWriter messagesToHtmlWriter = new MessagesToHtmlWriter(bytes, serializer)) {

0 commit comments

Comments
 (0)