Skip to content

Commit 76f368b

Browse files
Remove empty-string terminfo capabilities that cause input to be swallowed (#1861)
Programs like less (v691) and bat fail to accept keyboard input (e.g. '/' for search) when the terminfo database defines string capabilities as empty strings. The empty strings get loaded into the command parser's table and incorrectly match any input, silently consuming it. Remove the six capabilities that were defined with empty values (ka1, ka3, kc1, kc3, khlp, kund) from the StringCaps array, and add a defensive filter in terminfo() to skip any remaining empty-valued string capabilities. Signed-off-by: Christian Parpart <christian@parpart.family>
1 parent c3108bf commit 76f368b

File tree

3 files changed

+85
-9
lines changed

3 files changed

+85
-9
lines changed

metainfo.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
<release version="0.6.3" urgency="medium" type="development">
108108
<description>
109109
<ul>
110+
<li>Fixes keyboard input being swallowed in programs like less and bat due to empty-string terminfo capabilities (#1861)</li>
110111
<li>Fixes F3 key not reaching PTY applications when a keybinding matches but its action does not execute, by falling through to the terminal instead of silently consuming the key event</li>
111112
<li>Fixes F3/Shift+F3 search navigation keybindings firing in any mode instead of only when Search mode is active</li>
112113
<li>Fixes build failure on Alpine Linux (musl libc) due to missing close_range() function (#1879)</li>

src/vtbackend/Capabilities.cpp

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -274,10 +274,6 @@ namespace
274274
String { Undefined, "kUP5"sv, "\033[1;5A"sv },
275275
String { Undefined, "kUP6"sv, "\033[1;6A"sv },
276276
String { Undefined, "kUP7"sv, "\033[1;7A"sv },
277-
String { "K1"_tcap, "ka1"sv, ""sv }, // upper left of keypad
278-
String { "K3"_tcap, "ka3"sv, ""sv }, // upper right of keypad
279-
String { "K4"_tcap, "kc1"sv, ""sv }, // center of keypad
280-
String { "K5"_tcap, "kc3"sv, ""sv }, // lower right of keypad
281277
String { "kl"_tcap, "kcub1"sv, "\033OD"sv }, // app: cursor left
282278
String { "kd"_tcap, "kcud1"sv, "\033OB"sv }, // app: cursor left
283279
String { "kr"_tcap, "kcuf1"sv, "\033OC"sv }, // app: cursor right
@@ -347,14 +343,11 @@ namespace
347343
String { "k7"_tcap, "kf7"sv, "\033[18~"sv },
348344
String { "k8"_tcap, "kf8"sv, "\033[19~"sv },
349345
String { "k9"_tcap, "kf9"sv, "\033[20~"sv },
350-
String { "%1"_tcap, "khlp"sv, ""sv },
351346
String { "kh"_tcap, "khome"sv, "\033OH"sv },
352347
String { "kI"_tcap, "kich1"sv, "\033[2~"sv },
353348
String { "Km"_tcap, "kmous"sv, "\033[M"sv },
354349
String { "kN"_tcap, "knp"sv, "\033[6~"sv },
355350
String { "kP"_tcap, "kpp"sv, "\033[5~"sv },
356-
String { "&8"_tcap, "kund"sv, ""sv },
357-
358351
// {{{ Extensions originally introduced by tmux.
359352
//
360353
String { "Ss"_tcap, "Ss"sv, "\033[%p1%d q" }, // Set cursor style.
@@ -492,7 +485,7 @@ string StaticDatabase::terminfo() const
492485
output << " " << cap.name << "#" << cap.value << ",\n";
493486

494487
for (auto const& cap: strings)
495-
if (!cap.name.empty())
488+
if (!cap.name.empty() && !cap.value.empty())
496489
output << " " << cap.name << "=" << crispy::escape(cap.value, crispy::numeric_escape::Octal)
497490
<< ",\n";
498491

src/vtbackend/Capabilities_test.cpp

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
#include <catch2/catch_test_macros.hpp>
77

8-
#include <format>
8+
#include <regex>
9+
#include <string>
910

1011
using namespace std::string_view_literals;
1112
using crispy::fromHexString;
@@ -32,3 +33,84 @@ TEST_CASE("Capabilities.get")
3233
auto const bce = tcap.numericCapability("bce");
3334
REQUIRE(bce);
3435
}
36+
37+
// Issue #1861: Empty string terminfo capabilities cause input to be swallowed in programs
38+
// like less and bat. When capabilities are defined as empty strings (e.g., "ka1=,"),
39+
// buggy parsers match any input against them.
40+
TEST_CASE("Capabilities.terminfo_no_empty_string_values", "[issue-1861]")
41+
{
42+
vtbackend::capabilities::StaticDatabase const tcap;
43+
auto const terminfo = tcap.terminfo();
44+
45+
// The regex matches lines like " name=," where the value after '=' is empty.
46+
// In terminfo format, "name=value," defines a string capability.
47+
// "name=," means the value is an empty string — this must never appear.
48+
auto const emptyValuePattern = std::regex(R"(^\s+(\w+)=,$)", std::regex::multiline);
49+
50+
auto begin = std::sregex_iterator(terminfo.begin(), terminfo.end(), emptyValuePattern);
51+
auto end = std::sregex_iterator();
52+
53+
std::string emptyCapNames;
54+
for (auto it = begin; it != end; ++it)
55+
{
56+
if (!emptyCapNames.empty())
57+
emptyCapNames += ", ";
58+
emptyCapNames += (*it)[1].str();
59+
}
60+
61+
INFO("Capabilities with empty string values: " << emptyCapNames);
62+
CHECK(emptyCapNames.empty());
63+
}
64+
65+
// Verify that previously-empty keypad capabilities (ka1, ka3, kc1, kc3) are no longer
66+
// present in the terminfo output at all — they should be omitted, not set to empty.
67+
TEST_CASE("Capabilities.keypad_caps_not_in_terminfo", "[issue-1861]")
68+
{
69+
vtbackend::capabilities::StaticDatabase const tcap;
70+
auto const terminfo = tcap.terminfo();
71+
72+
CHECK(terminfo.find("ka1=") == std::string::npos);
73+
CHECK(terminfo.find("ka3=") == std::string::npos);
74+
CHECK(terminfo.find("kc1=") == std::string::npos);
75+
CHECK(terminfo.find("kc3=") == std::string::npos);
76+
}
77+
78+
// Verify that khlp and kund (which were also empty) are omitted.
79+
TEST_CASE("Capabilities.help_undo_caps_not_in_terminfo", "[issue-1861]")
80+
{
81+
vtbackend::capabilities::StaticDatabase const tcap;
82+
auto const terminfo = tcap.terminfo();
83+
84+
CHECK(terminfo.find("khlp=") == std::string::npos);
85+
CHECK(terminfo.find("kund=") == std::string::npos);
86+
}
87+
88+
// Verify that non-empty string capabilities are still present in terminfo output.
89+
TEST_CASE("Capabilities.non_empty_caps_still_present", "[issue-1861]")
90+
{
91+
vtbackend::capabilities::StaticDatabase const tcap;
92+
auto const terminfo = tcap.terminfo();
93+
94+
// These are well-known capabilities that must still be present.
95+
CHECK(terminfo.find("bold=") != std::string::npos);
96+
CHECK(terminfo.find("clear=") != std::string::npos);
97+
CHECK(terminfo.find("kcub1=") != std::string::npos);
98+
CHECK(terminfo.find("kcud1=") != std::string::npos);
99+
CHECK(terminfo.find("kf1=") != std::string::npos);
100+
CHECK(terminfo.find("smkx=") != std::string::npos);
101+
CHECK(terminfo.find("rmkx=") != std::string::npos);
102+
}
103+
104+
// Verify that the stringCapability() API returns an empty view for removed capabilities,
105+
// confirming they are no longer part of the static database.
106+
TEST_CASE("Capabilities.removed_caps_return_empty_from_api", "[issue-1861]")
107+
{
108+
vtbackend::capabilities::StaticDatabase const tcap;
109+
110+
CHECK(tcap.stringCapability("ka1").empty());
111+
CHECK(tcap.stringCapability("ka3").empty());
112+
CHECK(tcap.stringCapability("kc1").empty());
113+
CHECK(tcap.stringCapability("kc3").empty());
114+
CHECK(tcap.stringCapability("khlp").empty());
115+
CHECK(tcap.stringCapability("kund").empty());
116+
}

0 commit comments

Comments
 (0)