1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
#include "lix/libutil/terminal.hh"
#include <gtest/gtest.h>
#include <regex>

namespace nix {

TEST(filterANSIEscapes, emptyString) {
    auto s = "";
    auto expected = "";

    ASSERT_EQ(filterANSIEscapes(s), expected);
}

TEST(filterANSIEscapes, doesntChangePrintableChars) {
    auto s = "09 2q304ruyhr slk2-19024 kjsadh sar f";

    ASSERT_EQ(filterANSIEscapes(s), s);
}

TEST(filterANSIEscapes, filtersColorCodes) {
    auto s = "\u001b[30m A \u001b[31m B \u001b[32m C \u001b[33m D \u001b[0m";

    ASSERT_EQ(filterANSIEscapes(s, true, 2), " A" );
    ASSERT_EQ(filterANSIEscapes(s, true, 3), " A " );
    ASSERT_EQ(filterANSIEscapes(s, true, 4), " A  " );
    ASSERT_EQ(filterANSIEscapes(s, true, 5), " A  B" );
    ASSERT_EQ(filterANSIEscapes(s, true, 8), " A  B  C" );
}

TEST(filterANSIEscapes, expandsTabs) {
    auto s = "foo\tbar\tbaz";

    ASSERT_EQ(filterANSIEscapes(s, true), "foo     bar     baz" );
}

TEST(filterANSIEscapes, utf8) {
    ASSERT_EQ(filterANSIEscapes("foobar", true, 5), "fooba");
    ASSERT_EQ(filterANSIEscapes("fóóbär", true, 6), "fóóbär");
    ASSERT_EQ(filterANSIEscapes("fóóbär", true, 5), "fóóbä");
    ASSERT_EQ(filterANSIEscapes("fóóbär", true, 3), "fóó");
    ASSERT_EQ(filterANSIEscapes("f€€bär", true, 4), "f€€b");
    ASSERT_EQ(filterANSIEscapes("f𐍈𐍈bär", true, 4), "f𐍈𐍈b");
}

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");
}

TEST(makeHyperlink, works)
{
    auto big = std::string(701, 'A');
    EXPECT_EQ(makeHyperlink(big, "meow"), "\e]8;;meow\e\\" + big + "\e]8;;\e\\");
    EXPECT_EQ(makeHyperlink("meow", big), "meow");
}

TEST(makeHyperlinkLocalPath, works)
{
    // NOLINTNEXTLINE(lix-foreign-exceptions): its a test lol
    auto regex = std::regex{R""(^file://([^/]+)/(.*)$)""};
    std::smatch match;
    auto output = makeHyperlinkLocalPath("/a/b/ c", 4);

    ASSERT_TRUE(std::regex_match(output, match, regex));
    // Hostname has a value
    ASSERT_GT(match[1].length(), 0);
    ASSERT_EQ(match[2].str(), "a/b/%20c#4");
}
}