From ba4320f2581e7c79072c2a192a6a062052cf0f09 Mon Sep 17 00:00:00 2001 From: Marco Fontani Date: Tue, 14 Oct 2025 09:35:17 +0200 Subject: [PATCH 1/2] main.c: no-op for CSI N J, = ... h, l and "?" --- src/main.c | 95 +++++++++++++++++++++++++++++++++++++++++++ tests/csi-equals.sh | 21 ++++++++++ tests/csi-j.sh | 29 +++++++++++++ tests/csi-k.sh | 12 ++++++ tests/csi-question.sh | 21 ++++++++++ 5 files changed, 178 insertions(+) create mode 100755 tests/csi-equals.sh create mode 100755 tests/csi-j.sh create mode 100755 tests/csi-question.sh diff --git a/src/main.c b/src/main.c index b6f8881..ff65e44 100644 --- a/src/main.c +++ b/src/main.c @@ -138,6 +138,8 @@ just_strip_it(bool ignore_sgr_errors) STATE_TEXT, STATE_GOT_ESC, STATE_GOT_ESC_BRACKET, + STATE_GOT_ESC_BRACKET_EQUALS, + STATE_GOT_ESC_BRACKET_QUESTION, } state = STATE_TEXT; char buffer[INPUT_BUFFER_SIZE]; @@ -269,6 +271,19 @@ just_strip_it(bool ignore_sgr_errors) // Ignore "Erase in Line" sequence. state = STATE_TEXT; } + else if (c == 'J') + { + // Ignore "Erase in Display" sequence. + state = STATE_TEXT; + } + else if (c == '=') + { + state = STATE_GOT_ESC_BRACKET_EQUALS; + } + else if (c == '?') + { + state = STATE_GOT_ESC_BRACKET_QUESTION; + } else { if (ignore_sgr_errors) @@ -282,6 +297,42 @@ just_strip_it(bool ignore_sgr_errors) ); } } + else if (state == STATE_GOT_ESC_BRACKET_EQUALS) + { + // Read numbers until "h" or "l", ignore everything in between. + if (c == 'h' || c == 'l') + state = STATE_TEXT; + else if (c < '0' || c > '9') + { + if (ignore_sgr_errors) + state = STATE_TEXT; + else + ERROR( + "SGR ]= sequence contains invalid character " + "'%c' at %zu characters read / %zu in SGR sequence " + "which begun at %zu characters read.\n", + c, read, sgr_chars_len, begun_at + ); + } + } + else if (state == STATE_GOT_ESC_BRACKET_QUESTION) + { + // Read numbers until "h" or "l", ignore everything in between. + if (c == 'h' || c == 'l') + state = STATE_TEXT; + else if (c < '0' || c > '9') + { + if (ignore_sgr_errors) + state = STATE_TEXT; + else + ERROR( + "SGR ]? sequence contains invalid character " + "'%c' at %zu characters read / %zu in SGR sequence " + "which begun at %zu characters read.\n", + c, read, sgr_chars_len, begun_at + ); + } + } else { ERROR("Invalid state %d.\n", (int)state); @@ -328,6 +379,8 @@ static inline __attribute__((always_inline)) void ansi2html( STATE_TEXT, STATE_GOT_ESC, STATE_GOT_ESC_BRACKET, + STATE_GOT_ESC_BRACKET_EQUALS, + STATE_GOT_ESC_BRACKET_QUESTION, } state = STATE_TEXT; char buffer[INPUT_BUFFER_SIZE]; @@ -534,6 +587,24 @@ static inline __attribute__((always_inline)) void ansi2html( span_outputted = true; } } + else if (c == 'J') + { + // Ignore "Erase in Display" sequence. + state = STATE_TEXT; + if (span && !span_outputted) + { + append_to_buffer(span); + span_outputted = true; + } + } + else if (c == '=') + { + state = STATE_GOT_ESC_BRACKET_EQUALS; + } + else if (c == '?') + { + state = STATE_GOT_ESC_BRACKET_QUESTION; + } else { // The character should be a digit, or semicolon. @@ -588,6 +659,30 @@ static inline __attribute__((always_inline)) void ansi2html( ); } break; + case STATE_GOT_ESC_BRACKET_EQUALS: + // Read numbers until "h" or "l", ignore everything in between. + if (c == 'h' || c == 'l') + state = STATE_TEXT; + else if (c < '0' || c > '9') + ERROR( + "SGR ]= sequence contains invalid character " + "'%c' at %zu characters read / %zu in SGR sequence " + "which begun at %zu characters read.\n", + c, read, sgr_chars_len, begun_at + ); + break; + case STATE_GOT_ESC_BRACKET_QUESTION: + // Read numbers until "h" or "l", ignore everything in between. + if (c == 'h' || c == 'l') + state = STATE_TEXT; + else if (c < '0' || c > '9') + ERROR( + "SGR ]? sequence contains invalid character " + "'%c' at %zu characters read / %zu in SGR sequence " + "which begun at %zu characters read.\n", + c, read, sgr_chars_len, begun_at + ); + break; } } } while (1); diff --git a/tests/csi-equals.sh b/tests/csi-equals.sh new file mode 100755 index 0000000..4ffdb5d --- /dev/null +++ b/tests/csi-equals.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +. "$(dirname "$0")/".functions.sh + +# CSI = 0 ... 19 h and CSI = ... l are no-op +for l in h l; do + for k in $(seq 0 19); do + str=$(printf 'Hello, \e[0;1mBold\e[=%s%s\e[0m world!' "$k" "$l") + want=$'Hello, Bold world!' + got=$(printf '%s' "$str" | ./ansi2html -p vga) + str_eq_html "$str" "$want" "$got" + + want=$'Hello, Bold world!' + got=$(printf '%s' "$str" | ./ansi2html -S) + str_eq_html "$str" "$want" "$got" + done +done + +done_testing diff --git a/tests/csi-j.sh b/tests/csi-j.sh new file mode 100755 index 0000000..722b78c --- /dev/null +++ b/tests/csi-j.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +set -e + +. "$(dirname "$0")/".functions.sh + +# CSI J is a no-op when stripping OR converting to HTML +str=$'Hello, \e[0;1mBold\e[J\e[0m world!' +want=$'Hello, Bold world!' +got=$(printf '%s' "$str" | ./ansi2html -p vga) +str_eq_html "$str" "$want" "$got" + +want=$'Hello, Bold world!' +got=$(printf '%s' "$str" | ./ansi2html -S) +str_eq_html "$str" "$want" "$got" + +# CSI 0 J is also a no-op, same for CSI 1 J, CSI 2 J and CSI 3 J +for k in 0 1 2 3; do + str=$(printf 'Hello, \e[0;1mBold\e[%sJ\e[0m world!' "$k") + want=$'Hello, Bold world!' + got=$(printf '%s' "$str" | ./ansi2html -p vga) + str_eq_html "$str" "$want" "$got" + + want=$'Hello, Bold world!' + got=$(printf '%s' "$str" | ./ansi2html -S) + str_eq_html "$str" "$want" "$got" +done + +done_testing diff --git a/tests/csi-k.sh b/tests/csi-k.sh index f7fb9c0..08a6215 100755 --- a/tests/csi-k.sh +++ b/tests/csi-k.sh @@ -14,4 +14,16 @@ want=$'Hello, Bold world!' got=$(printf '%s' "$str" | ./ansi2html -S) str_eq_html "$str" "$want" "$got" +# CSI 0 K is also a no-op, same for CSI 1 K and CSI 2 K +for k in 0 1 2; do + str=$(printf 'Hello, \e[0;1mBold\e[%sK\e[0m world!' "$k") + want=$'Hello, Bold world!' + got=$(printf '%s' "$str" | ./ansi2html -p vga) + str_eq_html "$str" "$want" "$got" + + want=$'Hello, Bold world!' + got=$(printf '%s' "$str" | ./ansi2html -S) + str_eq_html "$str" "$want" "$got" +done + done_testing diff --git a/tests/csi-question.sh b/tests/csi-question.sh new file mode 100755 index 0000000..fdd0eb5 --- /dev/null +++ b/tests/csi-question.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +. "$(dirname "$0")/".functions.sh + +# CSI ? ... h and CSI ? ... l are no-op +for l in h l; do + for k in 25 47 1049; do + str=$(printf 'Hello, \e[0;1mBold\e[?%s%s\e[0m world!' "$k" "$l") + want=$'Hello, Bold world!' + got=$(printf '%s' "$str" | ./ansi2html -p vga) + str_eq_html "$str" "$want" "$got" + + want=$'Hello, Bold world!' + got=$(printf '%s' "$str" | ./ansi2html -S) + str_eq_html "$str" "$want" "$got" + done +done + +done_testing From d471e9722d6b3f0a00277f25d1690139c76905dd Mon Sep 17 00:00:00 2001 From: Marco Fontani Date: Tue, 14 Oct 2025 09:36:40 +0200 Subject: [PATCH 2/2] workflows: parallel execution --- .github/workflows/test-build.yml | 6 +++--- Makefile | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index de77e41..88ce941 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -46,7 +46,7 @@ jobs: - name: run tests timeout-minutes: 2 run: | - make prove + make prove-j2 build-static: runs-on: ubuntu-latest container: alpine:latest @@ -89,7 +89,7 @@ jobs: - name: run tests [with iTerm2 palettes] timeout-minutes: 1 run: | - prove -v tests/*.sh + prove -v -j 2 tests/*.sh - name: static compilation [no iTerm2 palettes] with mingw32-w64 timeout-minutes: 2 run: | @@ -154,7 +154,7 @@ jobs: - name: run tests timeout-minutes: 2 run: | - make prove + make prove-j2 - name: "Compress for ${{matrix.arch}} [with iTerm2 palettes]" timeout-minutes: 1 run: | diff --git a/Makefile b/Makefile index cb83c68..32c7d97 100644 --- a/Makefile +++ b/Makefile @@ -93,3 +93,7 @@ run-tests: .PHONY: prove prove: @prove tests/*.sh + +.PHONY: prove-j2 +prove-j2: + @prove -j 2 tests/*.sh