Repository: Textualize/rich
Pull request #3278 (2024-02-13 00:52:52)

Strip problematic private escape sequences
## Type of changes

- [x] Bug fix
- [ ] New feature
- [ ] Documentation / docstrings
- [ ] Tests
- [ ] Other

## Checklist

- [x] I've run the latest [black](https://github.com/psf/black) with default args on new code.
- [x] I've updated CHANGELOG.md and CONTRIBUTORS.md where appropriate.
- [x] I've added tests for new code.
- [x] I accept that @willmcgugan may be pedantic in the code review.

## Description

["Private escape sequences"](https://en.wikipedia.org/wiki/ANSI_escape_code#Fp_Escape_sequences) are escape sequences that are reserved for private use, though `\x1b7` and `\x1b8` are commonly used for storing and restoring the current cursor position. When those escape codes are not stripped the cursor jumps around and causes Rich to write garbage output. An example of a program that uses this cursor store/restore functionality is the APK package manager in Alpine Linux:

https://gitlab.alpinelinux.org/alpine/apk-tools/-/blob/48d91f482eb48a0a107b714ee183bb7e07782e14/src/print.c#L232-240

This commit updates the ANSI parser to ignore the `\x1b0`-`\x1b?` escape sequences, thus preventing them from being printed and causing havoc.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 087b4b02..e153073c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - `RichHandler` errors and warnings will now use different colors (red and yellow) https://github.com/Textualize/rich/issues/2825
 - Removed the empty line printed in jupyter while using `Progress` https://github.com/Textualize/rich/pull/2616
 - Running tests in environment with `FORCE_COLOR` or `NO_COLOR` environment variables
+- ansi decoder will now strip problematic private escape sequences (like `\x1b7`) https://github.com/Textualize/rich/pull/3278/
 
 ### Added
 
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index a303a124..b107adc8 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -27,6 +27,7 @@ The following people have contributed to the development of Rich:
 - [Kenneth Hoste](https://github.com/boegel)
 - [Lanqing Huang](https://github.com/lqhuang)
 - [Finn Hughes](https://github.com/finnhughes)
+- [Logan Hunt](https://github.com/dosisod)
 - [JP Hutchins](https://github.com/JPhutchins)
 - [Ionite](https://github.com/ionite34)
 - [Josh Karpel](https://github.com/JoshKarpel)
diff --git a/rich/ansi.py b/rich/ansi.py
index 66365e65..7de86ce5 100644
--- a/rich/ansi.py
+++ b/rich/ansi.py
@@ -9,6 +9,7 @@ from .text import Text
 
 re_ansi = re.compile(
     r"""
+(?:\x1b[0-?])|
 (?:\x1b\](.*?)\x1b\\)|
 (?:\x1b([(@-Z\\-_]|\[[0-?]*[ -/]*[@-~]))
 """,


Test patch:
diff --git a/tests/test_ansi.py b/tests/test_ansi.py
index ae87cde1..d81f6459 100644
--- a/tests/test_ansi.py
+++ b/tests/test_ansi.py
@@ -68,3 +68,17 @@ def test_decode_issue_2688(ansi_bytes, expected_text):
     text = Text.from_ansi(ansi_bytes.decode())
 
     assert str(text) == expected_text
+
+
+@pytest.mark.parametrize("code", [*"0123456789:;<=>?"])
+def test_strip_private_escape_sequences(code):
+    text = Text.from_ansi(f"\x1b{code}x")
+
+    console = Console(force_terminal=True)
+
+    with console.capture() as capture:
+        console.print(text)
+
+    expected = "x\n"
+
+    assert capture.get() == expected