diff options
Diffstat (limited to 'goldfishterm/terminfo.cc')
-rw-r--r-- | goldfishterm/terminfo.cc | 325 |
1 files changed, 325 insertions, 0 deletions
diff --git a/goldfishterm/terminfo.cc b/goldfishterm/terminfo.cc new file mode 100644 index 0000000..b58fb5b --- /dev/null +++ b/goldfishterm/terminfo.cc @@ -0,0 +1,325 @@ +// 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 <byteswap.h> +#include <stdint.h> +#include <stdlib.h> + +#include <fstream> +#include <istream> +#include <stdexcept> +#include <string> +#include <vector> + +#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 <typename T> +constexpr absl::underlying_type_t<T> FromEnum(T x) { + return static_cast<absl::underlying_type_t<T>>(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<char*>(&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<char*>(&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<std::string> 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<int8_t> ReadBooleanFlags(int bytes, std::istream& in) { + std::vector<int8_t> booleans(bytes); + in.read(reinterpret_cast<char*>(booleans.data()), booleans.size()); + for (int8_t b : booleans) { + RequireValidNumber("invalid boolean capability", b); + } + return booleans; +} + +std::vector<int32_t> ReadNumbers(bool extended_number_format, int count, + std::istream& in) { + std::vector<int32_t> 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<absl::optional<std::string>> ReadStrings(int offsets_count, + int bytes, + std::istream& in) { + std::vector<int16_t> 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<absl::optional<std::string>> 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<std::string> DatabasePaths() noexcept { + // If TERMINFO is set, only look there. + if (std::string terminfo = GetEnv("TERMINFO"); !terminfo.empty()) { + return {terminfo}; + } + + std::vector<std::string> 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<int> 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<std::string>& TerminfoEntry::get( + StringCapability cap) const noexcept { + try { + return strings_.at(FromEnum(cap)); + } catch (const std::out_of_range&) { + static const absl::optional<std::string> kNone = absl::nullopt; + return kNone; + } +} + +} // namespace goldfishterm |