diff --git a/CHANGELOG.md b/CHANGELOG.md index a94fa9b9..406ccec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Changed - Upgrade `react-components` to [23.2.0](https://github.com/cucumber/react-components/releases/tag/v23.2.0) +### Fixed +- Escape html comments in json ([#408](https://github.com/cucumber/html-formatter/pull/408)) + ## [21.13.0] - 2025-07-07 ### Changed - Upgrade `cucumber-messages` to [28.0.0](https://github.com/cucumber/messages/releases/tag/v28.0.0) diff --git a/dotnet/Cucumber.HtmlFormatter/JsonInHtmlWriter.cs b/dotnet/Cucumber.HtmlFormatter/JsonInHtmlWriter.cs index 407c9178..71958a38 100644 --- a/dotnet/Cucumber.HtmlFormatter/JsonInHtmlWriter.cs +++ b/dotnet/Cucumber.HtmlFormatter/JsonInHtmlWriter.cs @@ -33,7 +33,8 @@ public override void Write(char[] source, int offset, int length) throw new ArgumentException("Cannot read past the end of the input source char array."); var destination = PrepareBuffer(); - var flushAt = BUFFER_SIZE - 2; + // Largest write without boundary check is 4 bytes + var flushAt = BUFFER_SIZE - 4; var written = 0; for (var i = offset; i < offset + length; i++) { @@ -46,12 +47,19 @@ public override void Write(char[] source, int offset, int length) written = 0; } - // Write with escapes - if (c == '/') + // Replace < with \x3C + // https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements + if (c == '<') { destination[written++] = '\\'; + destination[written++] = 'x'; + destination[written++] = '3'; + destination[written++] = 'C'; + } + else + { + destination[written++] = c; } - destination[written++] = c; } // Flush any remaining if (written > 0) @@ -66,7 +74,8 @@ public override async Task WriteAsync(char[] source, int offset, int length) throw new ArgumentException("Cannot read past the end of the input source char array."); var destination = PrepareBuffer(); - var flushAt = BUFFER_SIZE - 2; + // Largest write without boundary check is 4 bytes + var flushAt = BUFFER_SIZE - 4; var written = 0; for (var i = offset; i < offset + length; i++) { @@ -79,12 +88,19 @@ public override async Task WriteAsync(char[] source, int offset, int length) written = 0; } - // Write with escapes - if (c == '/') + // Replace < with \x3C + // https://html.spec.whatwg.org/multipage/scripting.html#restrictions-for-contents-of-script-elements + if (c == '<') { destination[written++] = '\\'; + destination[written++] = 'x'; + destination[written++] = '3'; + destination[written++] = 'C'; + } + else + { + destination[written++] = c; } - destination[written++] = c; } // Flush any remaining if (written > 0) diff --git a/dotnet/Cucumber.HtmlFormatterTest/JsonInHtmlWriterTests.cs b/dotnet/Cucumber.HtmlFormatterTest/JsonInHtmlWriterTests.cs index 16a78de0..09a048f7 100644 --- a/dotnet/Cucumber.HtmlFormatterTest/JsonInHtmlWriterTests.cs +++ b/dotnet/Cucumber.HtmlFormatterTest/JsonInHtmlWriterTests.cs @@ -32,29 +32,29 @@ public async Task WritesAsync() [TestMethod] public void EscapesSingle() { - _writer.Write("/"); - Assert.AreEqual("\\/", Output()); + _writer.Write("<"); + Assert.AreEqual("\\x3C", Output()); } [TestMethod] public async Task EscapesSingleAsync() { - await _writer.WriteAsync("/"); - Assert.AreEqual("\\/", await OutputAsync()); + await _writer.WriteAsync("<"); + Assert.AreEqual("\\x3C", await OutputAsync()); } [TestMethod] public void EscapesMultiple() { _writer.Write(""); - Assert.AreEqual("<\\/script>"); - Assert.AreEqual("<\\/script>")] )); string html = RenderAsHtml(envelope); - Assert.IsTrue(html.Contains("window.CUCUMBER_MESSAGES = [{\"gherkinDocument\":{\"comments\":[{\"location\":{\"line\":0,\"column\":0},\"text\":\"<\\/script>")] )); string html = await RenderAsHtmlAsync(envelope); - Assert.IsTrue(html.Contains("window.CUCUMBER_MESSAGES = [{\"gherkinDocument\":{\"comments\":[{\"location\":{\"line\":0,\"column\":0},\"text\":\"<\\/script>"); - assertEquals("<\\/script>