@@ -14,6 +14,7 @@ use futures::StreamExt;
1414use serde:: { Deserialize , Serialize } ;
1515#[ allow( unused_imports) ]
1616use slog:: { debug, error, info, o, trace, warn, Logger } ;
17+ use std:: borrow:: Cow ;
1718use std:: collections:: { HashMap , VecDeque } ;
1819use std:: sync:: Arc ;
1920use tokio:: io:: { AsyncSeekExt , AsyncWriteExt } ;
@@ -81,7 +82,7 @@ impl JobEventEx for JobEvent {
8182 * Do the HTML escaping of the payload one canonical way, in the
8283 * server:
8384 */
84- html_escape :: encode_safe ( & self . payload) ,
85+ encode_payload ( & self . payload) ,
8586 ) ;
8687
8788 EventRow {
@@ -122,6 +123,37 @@ impl JobEventEx for JobEvent {
122123 }
123124}
124125
126+ fn encode_payload ( payload : & str ) -> Cow < ' _ , str > {
127+ /*
128+ * Apply ANSI formatting to the payload after escaping it (we want to
129+ * transmit the corresponding HTML tags over the wire).
130+ *
131+ * One of the cases this does not handle is multi-line color output split
132+ * across several payloads. Doing so is quite tricky, because buildomat
133+ * works with a single bash script and doesn't know when commands are
134+ * completed. Other systems like GitHub Actions (as checked on 2024-09-03)
135+ * don't handle multiline color either, so it's fine to punt on that.
136+ */
137+ ansi_to_html:: convert_with_opts (
138+ payload,
139+ & ansi_to_html:: Opts :: default ( )
140+ . four_bit_var_prefix ( Some ( "ansi-" . to_string ( ) ) ) ,
141+ )
142+ . map_or_else (
143+ |_| {
144+ /*
145+ * Invalid ANSI code: only escape HTML in case the conversion to
146+ * ANSI fails. To maintain consistency we use the same logic as
147+ * ansi-to-html: do not escape "/". (There are other differences,
148+ * such as ansi-to-html using decimal escapes while html_escape uses
149+ * hex, but those are immaterial.)
150+ */
151+ html_escape:: encode_quoted_attribute ( payload)
152+ } ,
153+ Cow :: Owned ,
154+ )
155+ }
156+
125157#[ derive( Debug , Serialize ) ]
126158struct EventRow {
127159 task : Option < u32 > ,
@@ -573,7 +605,16 @@ pub(crate) async fn run(
573605 */
574606 let mut line =
575607 if console { "|C| " } else { "| " } . to_string ( ) ;
576- let mut chars = ev. payload . chars ( ) ;
608+
609+ /*
610+ * We support ANSI escapes in the log renderer, which means
611+ * that tools will generate ANSI sequences. That doesn't
612+ * work in the GitHub renderer, so we need to strip them out
613+ * entirely.
614+ */
615+ let payload = strip_ansi_escapes:: strip_str ( & ev. payload ) ;
616+ let mut chars = payload. chars ( ) ;
617+
577618 for _ in 0 ..MAX_LINE_LENGTH {
578619 if let Some ( c) = chars. next ( ) {
579620 line. push ( c) ;
@@ -1745,6 +1786,78 @@ pub mod test {
17451786 use super :: * ;
17461787 use buildomat_github_testdata:: * ;
17471788
1789+ #[ test]
1790+ fn test_encode_payload ( ) {
1791+ let data = & [
1792+ ( "Hello, world!" , "Hello, world!" ) ,
1793+ /*
1794+ * HTML escapes:
1795+ */
1796+ (
1797+ "2 & 3 < 4 > 5 / 6 ' 7 \" 8" ,
1798+ "2 & 3 < 4 > 5 / 6 ' 7 " 8" ,
1799+ ) ,
1800+ /*
1801+ * ANSI color codes:
1802+ */
1803+ (
1804+ /*
1805+ * Basic 16-color example; also tests a bright color (96).
1806+ * (ansi-to-html 0.2.1 claims not to support bright colors, but
1807+ * it actually does.)
1808+ */
1809+ "\x1b [31mHello, world!\x1b [0m \x1b [96mAnother message\x1b [0m" ,
1810+ "<span style='color:var(--ansi-red,#a00)'>Hello, world!</span> \
1811+ <span style='color:var(--ansi-bright-cyan,#5ff)'>\
1812+ Another message</span>",
1813+ ) ,
1814+ (
1815+ /*
1816+ * Truecolor, bold, italic, underline, and also with escapes.
1817+ * The second code ("another") does not have a reset, but we
1818+ * want to ensure that we generate closing HTML tags anyway.
1819+ */
1820+ "\x1b [38;2;255;0;0;1;3;4mTest message\x1b [0m and &/' \
1821+ \x1b [38;2;0;255;0;1;3;4manother",
1822+ "<span style='color:#ff0000'><b><i><u>Test message</u></i></b>\
1823+ </span> and &/' <span style='color:#00ff00'><b><i>\
1824+ <u>another</u></i></b></span>",
1825+ ) ,
1826+ (
1827+ /*
1828+ * Invalid ANSI code "xx"; should be HTML-escaped but the
1829+ * invalid ANSI code should remain as-is. (The second ANSI code
1830+ * is valid, and ansi-to-html should handle it.)
1831+ */
1832+ "\x1b [xx;2;255;0;0;1;3;4mTest message\x1b [0m and &/' \
1833+ \x1b [38;2;0;255;0;1;3;4manother",
1834+ "\u{1b} [xx;2;255;0;0;1;3;4mTest message and &/' <span \
1835+ style='color:#00ff00'><b><i><u>another</u></i></b></span>",
1836+ ) ,
1837+ (
1838+ /*
1839+ * Invalid ANSI code "9000"; should be HTML-escaped but the
1840+ * invalid ANSI code should remain as-is. (The second ANSI code
1841+ * is valid, but ansi-to-html's current behavior is to error out
1842+ * in this case. This can probably be improved.)
1843+ */
1844+ "\x1b [9000;2;255;0;0;1;3;4mTest message\x1b [0m and &/' \
1845+ \x1b [38;2;0;255;0;1;3;4manother",
1846+ "\u{1b} [9000;2;255;0;0;1;3;4mTest message\u{1b} [0m and \
1847+ &/' \u{1b} [38;2;0;255;0;1;3;4manother",
1848+ )
1849+ ] ;
1850+
1851+ for ( input, expected) in data {
1852+ let output = encode_payload ( input) ;
1853+ assert_eq ! (
1854+ output, * expected,
1855+ "output != expected: input: {:?}" ,
1856+ input
1857+ ) ;
1858+ }
1859+ }
1860+
17481861 #[ test]
17491862 fn basic_parse_basic ( ) -> Result < ( ) > {
17501863 let ( path, content, _) = real0 ( ) ;
0 commit comments