diff --git a/packages/syft-job/src/syft_job/job_repr.py b/packages/syft-job/src/syft_job/job_repr.py index accd0e90eeb..d6787e7e9fb 100644 --- a/packages/syft-job/src/syft_job/job_repr.py +++ b/packages/syft-job/src/syft_job/job_repr.py @@ -121,12 +121,17 @@ def _repr_html_(self) -> str: """ - html_content = self._convert_ansi_to_html(raw_content) - html_content = ( - html_content.replace("&", "&") - .replace('"', """) - .replace("'", "'") - ) + # Fix 1: HTML-escape the raw text *first* so that characters like + # `<`, `>`, `&` and `'` in the job's own output cannot be + # interpreted as HTML markup. `quote=False` keeps `\""` as `\""` + # rather than `"` -- the ANSI->HTML step that follows will insert + # double-quoted `style` attributes that must remain unescaped. + escaped_content = escape(raw_content, quote=False) + + # Fix 2: Convert ANSI codes to HTML spans *after* escaping, so the + # span tags we emit are real markup. The style attributes use plain + # double-quotes that were preserved by `quote=False` above. + html_content = self._convert_ansi_to_html(escaped_content) clean_content = self._strip_ansi_codes(raw_content) lines = clean_content.split("\n") diff --git a/packages/syft-job/src/syft_job/job_stdout.py b/packages/syft-job/src/syft_job/job_stdout.py index 64ca7bffbf8..6565665d92d 100644 --- a/packages/syft-job/src/syft_job/job_stdout.py +++ b/packages/syft-job/src/syft_job/job_stdout.py @@ -1,6 +1,7 @@ from __future__ import annotations import re +from html import escape from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -20,7 +21,14 @@ def _strip_ansi_codes(self, text: str) -> str: return ansi_escape.sub("", text) def _convert_ansi_to_html(self, text: str) -> str: - """Convert ANSI color codes to HTML spans.""" + """Convert ANSI color codes to HTML spans. + + The *text* argument must already be HTML-escaped (``<``, ``>``, ``&`` + and ``'`` replaced with entity references, ``"`` kept as-is via + ``quote=False``) so that only the ANSI escape sequences remain + unprocessed. The ```` tags emitted here use plain double-quoted + ``style`` attributes that must never be entity-escaped afterwards. + """ # Basic ANSI color code mapping ansi_colors = { "30": "color: #000000;", # black @@ -129,17 +137,17 @@ def _repr_html_(self) -> str: """ - # Convert ANSI codes to HTML for display - html_content = self._convert_ansi_to_html(raw_content) - - # Escape any remaining HTML characters that aren't our color spans - # We need to be careful not to escape our intentional HTML - html_content = ( - html_content.replace("&", "&") - .replace('"', """) - .replace("'", "'") - ) - # Don't escape < and > since we want our HTML spans to work + # Fix 1: HTML-escape the raw text *first* so that characters like + # `<`, `>`, `&` and `'` in the job's own output cannot be + # interpreted as HTML markup. `quote=False` keeps `\""` as `\""` + # rather than `"` -- the ANSI->HTML step that follows will insert + # double-quoted `style` attributes that must remain unescaped. + escaped_content = escape(raw_content, quote=False) + + # Fix 2: Convert ANSI codes to HTML spans *after* escaping, so the + # span tags we emit are real markup. The style attributes use plain + # double-quotes that were preserved by `quote=False` above. + html_content = self._convert_ansi_to_html(escaped_content) # Count lines and characters (use clean content for stats) clean_content = self._strip_ansi_codes(raw_content) diff --git a/packages/syft-job/tests/test_issue_9586_html_viewer.py b/packages/syft-job/tests/test_issue_9586_html_viewer.py new file mode 100644 index 00000000000..fa627e47ace --- /dev/null +++ b/packages/syft-job/tests/test_issue_9586_html_viewer.py @@ -0,0 +1,233 @@ +"""Tests for issue #9586: job stdout/stderr HTML views drop `<...>` text and show no ANSI colors. + +Two bugs were present in StdoutViewer._repr_html_ (job_stdout.py) and +StderrViewer._repr_html_ (job_repr.py): + + Bug 1 – Wrong escaping order: ANSI codes were converted to + ```` first, and then ``"`` was + replaced with ``"``. Every resulting span became + ````, so the browser + ignored the style and colored output showed up without colours. + + Bug 2 – Missing HTML escaping: ``<`` and ``>`` were never escaped. + Any output text that looks like an HTML tag (e.g. + ```` or tracebacks containing ``in ``) + was parsed as HTML and disappeared from the view. Worse, + output containing ``" + viewer = _make_stdout_viewer(tmp_path, payload) + html = viewer._repr_html_() + + assert "" + viewer = _make_stderr_viewer(tmp_path, payload) + html = viewer._repr_html_() + + assert "