This teaches `filterANSIEscapes()` how to find the end of an OSC sequence. It also keeps OSC 8 (hyperlinks) when not instructed to filter out all escapes, just as it keeps colors. This also relaxes the parsing of CSI escapes to find the end of the sequence for invalid sequences, and handles better escapes that don't start CSI or OSC. This fixes the repl output for `:doc builtins.fetchGit`. Fixes: https://git.lix.systems/lix-project/lix/issues/160 Change-Id: Id0000000f2a6956c042c883a4545edf347fa1799
142 lines
7.9 KiB
C++
142 lines
7.9 KiB
C++
#include "lix/libutil/terminal.hh"
|
||
#include <gtest/gtest.h>
|
||
|
||
namespace nix {
|
||
|
||
TEST(filterANSIEscapes, stripCSI) {
|
||
EXPECT_EQ(filterANSIEscapes("a\e[1;2;3pb\e[qc"), "abc");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e[0123456789:;<=>? !\"#$%&'()*+,-./~bar\e[@baz"), "foobarbaz");
|
||
// strip malformed sequences too, with parameter bytes after intermediate bytes
|
||
EXPECT_EQ(filterANSIEscapes("foo\e['-';;^bar"), "foobar");
|
||
// strip unfinished sequences
|
||
EXPECT_EQ(filterANSIEscapes("foo\e[123"), "foo");
|
||
// allow colors when !filterAll
|
||
EXPECT_EQ(filterANSIEscapes("foo\e[31;44mbar\e[0m"), "foo\e[31;44mbar\e[0m");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e[31;44mbar\e[0m", true), "foobar");
|
||
}
|
||
|
||
TEST(filterANSIEscapes, undefinedCSI) {
|
||
// if we get an undefined character (outside 0x20–0x7e) behavior is undefined.
|
||
// our current impl will abort the CSI sequence, so this tests for that.
|
||
// it's fine to change that behavior though, and we might want to see what terminals do!
|
||
EXPECT_EQ(filterANSIEscapes("foo\e[123\nbar"), "foo\nbar");
|
||
// if we terminate with \e, ensure we process it for another code
|
||
EXPECT_EQ(filterANSIEscapes("foo\e[123\e[123qbar"), "foobar");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e[123\e[31;44mbar"), "foo\e[31;44mbar");
|
||
}
|
||
|
||
TEST(filterANSIEscapes, stripOSC) {
|
||
// OSC ends with ST (ESC \) or BEL
|
||
EXPECT_EQ(filterANSIEscapes("a\e]0;this is a window title\ab"), "ab");
|
||
EXPECT_EQ(filterANSIEscapes("a\e]0;this is a window title\e\\b"), "ab");
|
||
EXPECT_EQ(filterANSIEscapes("a\e]\ab\e]\e\\c"), "abc");
|
||
// embedding a CSI in an OSC doesn't confuse things
|
||
EXPECT_EQ(filterANSIEscapes("a\e]\ab\e]\e[31;44m\e\\c"), "abc");
|
||
// parsing ST should not be confused by leading escapes
|
||
EXPECT_EQ(filterANSIEscapes("a\e]0;title\e\e\\b"), "ab");
|
||
EXPECT_EQ(filterANSIEscapes("a\e]0;title\e\ab"), "ab");
|
||
// OSC 8 is kept when !filterAll
|
||
EXPECT_EQ(filterANSIEscapes("a \e]8;;http://example.com\e\\link\e]8;;\e\\."), "a \e]8;;http://example.com\e\\link\e]8;;\e\\.");
|
||
EXPECT_EQ(filterANSIEscapes("a \e]8;;http://example.com\e\\link\e]8;;\e\\.", true), "a link.");
|
||
EXPECT_EQ(filterANSIEscapes("a \e]8;id=foo;http://example.com\e\\link\e]8;;\e\\."), "a \e]8;id=foo;http://example.com\e\\link\e]8;;\e\\.");
|
||
// OSC 88 is not OSC 8
|
||
EXPECT_EQ(filterANSIEscapes("a\e]88;;foo\e\\b"), "ab");
|
||
// nor are these variants
|
||
EXPECT_EQ(filterANSIEscapes("a\e];8;foo\e\\b"), "ab");
|
||
EXPECT_EQ(filterANSIEscapes("a\e] 8;foo\e\\b"), "ab");
|
||
EXPECT_EQ(filterANSIEscapes("a\e]08;foo\e\\b"), "ab");
|
||
EXPECT_EQ(filterANSIEscapes("a\e]]8;foo\e\\b"), "ab");
|
||
// strip unfinished sequences
|
||
EXPECT_EQ(filterANSIEscapes("a\e]0;foo"), "a");
|
||
EXPECT_EQ(filterANSIEscapes("a\e]8;;url"), "a");
|
||
}
|
||
|
||
TEST(filterANSIEscapes, stripCRBEL) {
|
||
using namespace std::string_view_literals;
|
||
// we strip CR and BEL, but not other control characters (besides \e processing and \t
|
||
// expansion). we should probably change this!
|
||
EXPECT_EQ(
|
||
filterANSIEscapes(
|
||
// all control codes except \t and \e
|
||
"a\0\1\2\3\4\5\6\a\b\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1c\x1d\x1e\x1f\x7f b"sv
|
||
),
|
||
"a\0\1\2\3\4\5\6\b\n\v\f\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1c\x1d\x1e\x1f\x7f b"sv
|
||
);
|
||
}
|
||
|
||
TEST(filterANSIEscapes, otherEscapes) {
|
||
// an \e that's not a CSI or OSC eats any number of 0x20–0x2f, plus one more printable
|
||
EXPECT_EQ(filterANSIEscapes("foo\ebar"), "fooar");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e@bar"), "foobar");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e\177bar"), "foobar");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e(Bbar"), "foobar");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e !\"#$%&'()*+,-./qbar"), "foobar");
|
||
// getting control chars in this sequence is undefined, but we process them (except \t)
|
||
EXPECT_EQ(filterANSIEscapes("foo\e\a\r\f\nbar"), "foo\f\nar");
|
||
// this eating aborts on another \e or a \t
|
||
EXPECT_EQ(filterANSIEscapes("foo\e\e[31mbar"), "foo\e[31mbar");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e\tbar", false, std::numeric_limits<unsigned int>::max(), false), "foo\tbar");
|
||
// it also aborts on a utf8 char for simplicity
|
||
EXPECT_EQ(filterANSIEscapes("foo\eƒbar"), "fooƒbar");
|
||
}
|
||
|
||
TEST(filterANSIEscapes, tabs) {
|
||
// eatTabs converts tabs into spaces until tabstop
|
||
EXPECT_EQ(filterANSIEscapes("foo\tbar"), "foo bar");
|
||
EXPECT_EQ(filterANSIEscapes("\tfoo"), " foo");
|
||
EXPECT_EQ(filterANSIEscapes("1234567\t"), "1234567 ");
|
||
EXPECT_EQ(filterANSIEscapes("12345678\t"), "12345678 ");
|
||
// filtered escapes don't affect the tabstop
|
||
EXPECT_EQ(filterANSIEscapes("foo\e@\tbar"), "foo bar");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e[3q\t\e[4pbar"), "foo bar");
|
||
EXPECT_EQ(filterANSIEscapes("foo\a\r\tbar"), "foo bar");
|
||
// color/OSC 8 don't either
|
||
EXPECT_EQ(filterANSIEscapes("foo\e[31m\tbar"), "foo\e[31m bar");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\a\tbar\e]8;;\a"), "foo\e]8;;url\a bar\e]8;;\a");
|
||
// \e\t still processes the tab
|
||
EXPECT_EQ(filterANSIEscapes("foo\e\tbar"), "foo bar");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e\tbar", false, std::numeric_limits<unsigned int>::max(), false), "foo\tbar");
|
||
// aborting a CSI with a \t still processes the tab
|
||
EXPECT_EQ(filterANSIEscapes("foo\e[3\tbar"), "foo bar");
|
||
}
|
||
|
||
TEST(filterANSIEscapes, width) {
|
||
// truncate the string at the given width, ignoring escapes
|
||
EXPECT_EQ(filterANSIEscapes("foo", false, 0), "");
|
||
EXPECT_EQ(filterANSIEscapes("\e[31mfoo", false, 0), "");
|
||
EXPECT_EQ(filterANSIEscapes("foo", false, 1), "f");
|
||
EXPECT_EQ(filterANSIEscapes("\e[31mfoo", false, 1), "\e[31mf");
|
||
EXPECT_EQ(filterANSIEscapes("\a\r\emfoo", false, 1), "f");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\e\\bar\e]8;;\e\\baz", false, 8), "foo\e]8;;url\e\\bar\e]8;;\e\\ba");
|
||
// arguably we should allow kept escapes while we're at the limit, but for now we stop processing
|
||
EXPECT_EQ(filterANSIEscapes("foo\e[31mbar\e][0mbaz", false, 6), "foo\e[31mbar");
|
||
// expanding tabs respects the width
|
||
EXPECT_EQ(filterANSIEscapes("foo\t", false, 4), "foo ");
|
||
EXPECT_EQ(filterANSIEscapes("foo\t", false, 6), "foo ");
|
||
// truncating with an open OSC 8 closes it if we cut off any OSC 8 codes
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\abar", false, 4), "foo\e]8;;url\ab");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\abar\e]8;;\a", false, 4), "foo\e]8;;url\ab\e]8;;\e\\");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\e\\bar\e]8;;other-url\a", false, 4), "foo\e]8;;url\e\\b\e]8;;\e\\");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;id=one;url\abar\e]8;id=two;\a", false, 4), "foo\e]8;id=one;url\ab\e]8;;\e\\");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\abar\e]8;;\a", false, 3), "foo");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\abar\e]8;;\a", false, 6), "foo\e]8;;url\abar\e]8;;\e\\");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\abar\e]8;;\a", false, 7), "foo\e]8;;url\abar\e]8;;\a");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\abar\e]8;;\abaz", false, 7), "foo\e]8;;url\abar\e]8;;\ab");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;\abar", false, 4), "foo\e]8;;\ab");
|
||
// an OSC 8 with params but no URL we still consider open
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;id=one;\abar\e]8;;\a", false, 4), "foo\e]8;id=one;\ab\e]8;;\e\\");
|
||
// we aren't tricked by not-quite-8s
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\abar\e]88;;\a", false, 4), "foo\e]8;;url\ab");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\abar\e] 8;;\a", false, 4), "foo\e]8;;url\ab");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\abar\e]08;;\a", false, 4), "foo\e]8;;url\ab");
|
||
EXPECT_EQ(filterANSIEscapes("foo\e]8;;url\abar\e]]8;;\a", false, 4), "foo\e]8;;url\ab");
|
||
}
|
||
|
||
TEST(filterANSIEscapes, controlChars) {
|
||
// right now, we keep most control chars, and count them towards width.
|
||
// we should probably change this! but this test shows current behavior.
|
||
EXPECT_EQ(filterANSIEscapes("foo\v\n\fbar", false, 8), "foo\v\n\fba");
|
||
}
|
||
|
||
}
|