aboutsummaryrefslogtreecommitdiff
path: root/goldfishterm/terminfo.cc
diff options
context:
space:
mode:
Diffstat (limited to 'goldfishterm/terminfo.cc')
-rw-r--r--goldfishterm/terminfo.cc325
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