// Copyright 2021 Benjamin Barenblat // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy of // the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. #include "goldfishterm/terminfo.h" #include #include #include #include #include #include #include #include #include "third_party/abseil/absl/base/config.h" #include "third_party/abseil/absl/meta/type_traits.h" #include "third_party/abseil/absl/strings/str_cat.h" #include "third_party/abseil/absl/strings/str_format.h" #include "third_party/abseil/absl/strings/str_split.h" #include "third_party/abseil/absl/types/optional.h" namespace goldfishterm { namespace { constexpr int16_t kTerminfoMagic = 0432; constexpr int16_t kTerminfoExtendedNumberFormatMagic = 01036; constexpr char kSystemTerminfoDirectory[] = "/etc/terminfo"; template constexpr absl::underlying_type_t FromEnum(T x) { return static_cast>(x); } std::string GetEnv(const char* var) noexcept { const char* val = getenv(var); if (val == nullptr) { return ""; } return val; } // Reads a potentially unaligned little-endian int16_t. int16_t ReadShort(std::istream& in) { // TODO(bbarenblat@gmail.com): Do we need to support anything else here (maybe // PDP-11-endian)? #if !defined(ABSL_IS_LITTLE_ENDIAN) && !defined(ABSL_IS_BIG_ENDIAN) #error Neither ABSL_IS_LITTLE_ENDIAN nor ABSL_IS_BIG_ENDIAN is defined #endif int16_t n; in.read(reinterpret_cast(&n), sizeof(int16_t)); #ifdef ABSL_IS_BIG_ENDIAN n = bswap_16(n); #endif return n; } // Reads a potentially unaligned little-endian int32_t. int32_t ReadInt(std::istream& in) { // TODO(bbarenblat@gmail.com): Do we need to support anything else here (maybe // PDP-11-endian)? #if !defined(ABSL_IS_LITTLE_ENDIAN) && !defined(ABSL_IS_BIG_ENDIAN) #error Neither ABSL_IS_LITTLE_ENDIAN nor ABSL_IS_BIG_ENDIAN is defined #endif int32_t n; in.read(reinterpret_cast(&n), sizeof(int32_t)); #ifdef ABSL_IS_BIG_ENDIAN n = bswap_32(n); #endif return n; } int16_t ReadNonnegativeShort(const char* error_message, std::istream& in) { int16_t n = ReadShort(in); if (n < 0) { throw std::runtime_error(error_message); } return n; } void RequireValidNumber(const char* error_message, int n) { // -1 and -2 are reserved to mean "not present" and "canceled", respectively. if (n < -2) { throw std::runtime_error(absl::StrCat(error_message, " ", n)); } } struct Header final { bool extended_number_format; int names_bytes; int booleans_bytes; int numbers_count; int strings_offsets_count; int strings_bytes; Header(std::istream& description) { switch (ReadShort(description)) { case kTerminfoMagic: extended_number_format = false; break; case kTerminfoExtendedNumberFormatMagic: extended_number_format = true; break; default: throw std::runtime_error("bad magic"); } names_bytes = ReadNonnegativeShort( "terminal names section cannot have negative length", description); booleans_bytes = ReadNonnegativeShort( "boolean flags section cannot have negative length", description); numbers_count = ReadNonnegativeShort( "numbers section cannot have negative length", description); strings_offsets_count = ReadNonnegativeShort( "strings section cannot have negative numbers of entries", description); strings_bytes = ReadNonnegativeShort( "strings table cannot have negative length", description); } }; std::vector ReadTerminalNames(int bytes, std::istream& in) { assert(bytes > 0); std::string names(bytes - 1, '\0'); in.read(names.data(), names.size()); // Discard the null at the end of the names list. if (in.get() != '\0') { throw std::runtime_error("terminal names were not null-terminated"); } return absl::StrSplit(names, '|'); } std::vector ReadBooleanFlags(int bytes, std::istream& in) { std::vector booleans(bytes); in.read(reinterpret_cast(booleans.data()), booleans.size()); for (int8_t b : booleans) { RequireValidNumber("invalid boolean capability", b); } return booleans; } std::vector ReadNumbers(bool extended_number_format, int count, std::istream& in) { std::vector numbers; numbers.reserve(count); for (int i = 0; i < count; ++i) { int32_t n; if (extended_number_format) { n = ReadInt(in); } else { n = ReadShort(in); } RequireValidNumber("invalid numeric capability", n); numbers.push_back(n); } return numbers; } std::vector> ReadStrings(int offsets_count, int bytes, std::istream& in) { std::vector offsets; offsets.reserve(offsets_count); for (int i = 0; i < offsets_count; ++i) { int16_t n = ReadShort(in); RequireValidNumber("invalid string offset", n); offsets.push_back(n); } // Read the string table, including the trailing null (so we can easily find // the end of each capability). std::string data(bytes, '\0'); in.read(data.data(), data.size()); std::vector> result; for (int16_t offset : offsets) { if (offset < 0) { result.push_back(absl::nullopt); } else { result.push_back(std::string(data.data() + offset)); } } return result; } std::vector DatabasePaths() noexcept { // If TERMINFO is set, only look there. if (std::string terminfo = GetEnv("TERMINFO"); !terminfo.empty()) { return {terminfo}; } std::vector paths; // Start by looking in HOME. if (std::string home = GetEnv("HOME"); !home.empty()) { paths.push_back(absl::StrCat(home, "/.terminfo")); } // Next, look in TERMINFO_DIRS. if (std::string terminfo_dirs = GetEnv("TERMINFO_DIRS"); !terminfo_dirs.empty()) { for (absl::string_view dir : absl::StrSplit(terminfo_dirs, ':')) { if (dir.empty()) { paths.push_back(kSystemTerminfoDirectory); } else { paths.push_back(std::string(dir)); } } } // Finally, look in system locations. paths.push_back(kSystemTerminfoDirectory); paths.push_back("/lib/terminfo"); paths.push_back("/usr/share/terminfo"); return paths; } std::ifstream OpenDescriptionForTerminal(absl::string_view db_path, absl::string_view name) { // The path to the terminal entry inside the database takes the general form // `c/name`, where c is the first character of the name (on file systems that // are case-sensitive) and the hex encoding of the name (on file systems that // are not). if (name.empty()) { throw std::invalid_argument("empty terminal name"); } std::ifstream entry(absl::StrFormat("%s/%02x/%s", db_path, name[0], name)); if (!entry.fail()) { return entry; } entry = std::ifstream(absl::StrFormat("%s/%c/%s", db_path, name[0], name)); if (!entry.fail()) { return entry; } throw std::runtime_error("terminfo entry not found"); } } // namespace TerminfoEntry TerminfoEntry::FromSystemDatabase( absl::string_view terminal_name) { for (const auto& path : DatabasePaths()) { try { std::ifstream binary = OpenDescriptionForTerminal(path, terminal_name); return TerminfoEntry(binary); } catch (const std::runtime_error&) { continue; } } throw std::runtime_error("no valid system entry by that name"); } TerminfoEntry::TerminfoEntry(std::istream& description) { description.exceptions(std::istream::badbit | std::istream::failbit); Header header(description); if (header.names_bytes > 0) { names_ = ReadTerminalNames(header.names_bytes, description); } booleans_ = ReadBooleanFlags(header.booleans_bytes, description); // Discard the padding byte between the boolean flags section and the number // section, if it exists. if ((header.names_bytes + header.booleans_bytes) % 2 == 1 && (header.numbers_count != 0 || header.strings_offsets_count != 0 || header.strings_bytes != 0) && description.get() != '\0') { throw std::runtime_error("padding byte was nonnull"); } numbers_ = ReadNumbers(header.extended_number_format, header.numbers_count, description); strings_ = ReadStrings(header.strings_offsets_count, header.strings_bytes, description); } bool TerminfoEntry::get(BooleanCapability cap) const noexcept { try { return booleans_.at(FromEnum(cap)) > 0; } catch (const std::out_of_range&) { return false; } } absl::optional TerminfoEntry::get(NumericCapability cap) const noexcept { try { int r = numbers_.at(FromEnum(cap)); if (r >= 0) { return r; } else { return absl::nullopt; } } catch (const std::out_of_range&) { return absl::nullopt; } } const absl::optional& TerminfoEntry::get( StringCapability cap) const noexcept { try { return strings_.at(FromEnum(cap)); } catch (const std::out_of_range&) { static const absl::optional kNone = absl::nullopt; return kNone; } } } // namespace goldfishterm