From 62e4c68907d8d33709c2c1f92a161dff00b3d5f2 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 22:01:36 +0200 Subject: Adding upstream version 0.11.2. Signed-off-by: Daniel Baumann --- src/base/CMakeLists.txt | 83 +++ src/base/Makefile.am | 110 ++++ src/base/README.md | 1 + src/base/ansi_scrubber.cc | 388 +++++++++++++ src/base/ansi_scrubber.hh | 75 +++ src/base/attr_line.builder.cc | 30 + src/base/attr_line.builder.hh | 119 ++++ src/base/attr_line.cc | 537 ++++++++++++++++++ src/base/attr_line.hh | 758 +++++++++++++++++++++++++ src/base/attr_line.tests.cc | 91 +++ src/base/auto_fd.hh | 318 +++++++++++ src/base/auto_mem.hh | 402 +++++++++++++ src/base/auto_pid.cc | 63 +++ src/base/auto_pid.hh | 175 ++++++ src/base/bus.hh | 68 +++ src/base/date_time_scanner.cc | 308 ++++++++++ src/base/date_time_scanner.hh | 137 +++++ src/base/enum_util.hh | 48 ++ src/base/file_range.hh | 78 +++ src/base/fs_util.cc | 183 ++++++ src/base/fs_util.hh | 122 ++++ src/base/fs_util.tests.cc | 56 ++ src/base/func_util.hh | 159 ++++++ src/base/future_util.hh | 111 ++++ src/base/humanize.cc | 110 ++++ src/base/humanize.file_size.tests.cc | 51 ++ src/base/humanize.hh | 58 ++ src/base/humanize.network.cc | 76 +++ src/base/humanize.network.hh | 109 ++++ src/base/humanize.network.tests.cc | 118 ++++ src/base/humanize.time.cc | 208 +++++++ src/base/humanize.time.hh | 88 +++ src/base/humanize.time.tests.cc | 125 ++++ src/base/injector.bind.hh | 173 ++++++ src/base/injector.hh | 230 ++++++++ src/base/intern_string.cc | 303 ++++++++++ src/base/intern_string.hh | 865 ++++++++++++++++++++++++++++ src/base/intern_string.tests.cc | 152 +++++ src/base/is_utf8.cc | 313 +++++++++++ src/base/is_utf8.hh | 59 ++ src/base/isc.cc | 147 +++++ src/base/isc.hh | 225 ++++++++ src/base/itertools.hh | 785 ++++++++++++++++++++++++++ src/base/lnav.console.cc | 494 ++++++++++++++++ src/base/lnav.console.hh | 176 ++++++ src/base/lnav.console.into.hh | 51 ++ src/base/lnav.gzip.cc | 135 +++++ src/base/lnav.gzip.hh | 54 ++ src/base/lnav.gzip.tests.cc | 67 +++ src/base/lnav_log.cc | 686 ++++++++++++++++++++++ src/base/lnav_log.hh | 188 +++++++ src/base/log_level_enum.hh | 65 +++ src/base/lrucache.hpp | 83 +++ src/base/math_util.hh | 73 +++ src/base/network.tcp.cc | 75 +++ src/base/network.tcp.hh | 80 +++ src/base/network.tcp.tests.cc | 50 ++ src/base/opt_util.hh | 105 ++++ src/base/paths.cc | 130 +++++ src/base/paths.hh | 58 ++ src/base/result.h | 1032 ++++++++++++++++++++++++++++++++++ src/base/snippet_highlighters.cc | 344 ++++++++++++ src/base/snippet_highlighters.hh | 43 ++ src/base/string_attr_type.cc | 51 ++ src/base/string_attr_type.hh | 679 ++++++++++++++++++++++ src/base/string_util.cc | 304 ++++++++++ src/base/string_util.hh | 232 ++++++++ src/base/string_util.tests.cc | 90 +++ src/base/strnatcmp.c | 302 ++++++++++ src/base/strnatcmp.h | 41 ++ src/base/test_base.cc | 33 ++ src/base/time_util.cc | 239 ++++++++ src/base/time_util.hh | 206 +++++++ 73 files changed, 14781 insertions(+) create mode 100644 src/base/CMakeLists.txt create mode 100644 src/base/Makefile.am create mode 100644 src/base/README.md create mode 100644 src/base/ansi_scrubber.cc create mode 100644 src/base/ansi_scrubber.hh create mode 100644 src/base/attr_line.builder.cc create mode 100644 src/base/attr_line.builder.hh create mode 100644 src/base/attr_line.cc create mode 100644 src/base/attr_line.hh create mode 100644 src/base/attr_line.tests.cc create mode 100644 src/base/auto_fd.hh create mode 100644 src/base/auto_mem.hh create mode 100644 src/base/auto_pid.cc create mode 100644 src/base/auto_pid.hh create mode 100644 src/base/bus.hh create mode 100644 src/base/date_time_scanner.cc create mode 100644 src/base/date_time_scanner.hh create mode 100644 src/base/enum_util.hh create mode 100644 src/base/file_range.hh create mode 100644 src/base/fs_util.cc create mode 100644 src/base/fs_util.hh create mode 100644 src/base/fs_util.tests.cc create mode 100644 src/base/func_util.hh create mode 100644 src/base/future_util.hh create mode 100644 src/base/humanize.cc create mode 100644 src/base/humanize.file_size.tests.cc create mode 100644 src/base/humanize.hh create mode 100644 src/base/humanize.network.cc create mode 100644 src/base/humanize.network.hh create mode 100644 src/base/humanize.network.tests.cc create mode 100644 src/base/humanize.time.cc create mode 100644 src/base/humanize.time.hh create mode 100644 src/base/humanize.time.tests.cc create mode 100644 src/base/injector.bind.hh create mode 100644 src/base/injector.hh create mode 100644 src/base/intern_string.cc create mode 100644 src/base/intern_string.hh create mode 100644 src/base/intern_string.tests.cc create mode 100644 src/base/is_utf8.cc create mode 100644 src/base/is_utf8.hh create mode 100644 src/base/isc.cc create mode 100644 src/base/isc.hh create mode 100644 src/base/itertools.hh create mode 100644 src/base/lnav.console.cc create mode 100644 src/base/lnav.console.hh create mode 100644 src/base/lnav.console.into.hh create mode 100644 src/base/lnav.gzip.cc create mode 100644 src/base/lnav.gzip.hh create mode 100644 src/base/lnav.gzip.tests.cc create mode 100644 src/base/lnav_log.cc create mode 100644 src/base/lnav_log.hh create mode 100644 src/base/log_level_enum.hh create mode 100644 src/base/lrucache.hpp create mode 100644 src/base/math_util.hh create mode 100644 src/base/network.tcp.cc create mode 100644 src/base/network.tcp.hh create mode 100644 src/base/network.tcp.tests.cc create mode 100644 src/base/opt_util.hh create mode 100644 src/base/paths.cc create mode 100644 src/base/paths.hh create mode 100644 src/base/result.h create mode 100644 src/base/snippet_highlighters.cc create mode 100644 src/base/snippet_highlighters.hh create mode 100644 src/base/string_attr_type.cc create mode 100644 src/base/string_attr_type.hh create mode 100644 src/base/string_util.cc create mode 100644 src/base/string_util.hh create mode 100644 src/base/string_util.tests.cc create mode 100644 src/base/strnatcmp.c create mode 100644 src/base/strnatcmp.h create mode 100644 src/base/test_base.cc create mode 100644 src/base/time_util.cc create mode 100644 src/base/time_util.hh (limited to 'src/base') diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt new file mode 100644 index 0000000..aa4143f --- /dev/null +++ b/src/base/CMakeLists.txt @@ -0,0 +1,83 @@ +add_library( + base STATIC + ../config.h.in + ansi_scrubber.cc + attr_line.cc + attr_line.builder.cc + auto_pid.cc + date_time_scanner.cc + fs_util.cc + humanize.cc + humanize.network.cc + humanize.time.cc + intern_string.cc + is_utf8.cc + isc.cc + lnav.console.cc + lnav.gzip.cc + lnav_log.cc + network.tcp.cc + paths.cc + snippet_highlighters.cc + string_attr_type.cc + string_util.cc + strnatcmp.c + time_util.cc + + ansi_scrubber.hh + attr_line.hh + attr_line.builder.hh + auto_fd.hh + auto_mem.hh + auto_pid.hh + bus.hh + date_time_scanner.hh + enum_util.hh + fs_util.hh + func_util.hh + future_util.hh + humanize.hh + humanize.network.hh + humanize.time.hh + injector.hh + injector.bind.hh + intern_string.hh + is_utf8.hh + isc.hh + itertools.hh + lnav.console.hh + lnav.console.into.hh + log_level_enum.hh + lrucache.hpp + math_util.hh + network.tcp.hh + paths.hh + result.h + snippet_highlighters.hh + string_attr_type.hh + strnatcmp.h + time_util.hh + + ../third-party/xxHash/xxhash.h + ../third-party/xxHash/xxhash.c +) + +target_include_directories(base PUBLIC . .. ../third-party + ${CMAKE_CURRENT_BINARY_DIR}/..) +target_link_libraries(base cppfmt cppscnlib pcrepp ncurses::libcurses pthread) + +add_executable( + test_base + attr_line.tests.cc + fs_util.tests.cc + humanize.file_size.tests.cc + humanize.network.tests.cc + humanize.time.tests.cc + intern_string.tests.cc + lnav.gzip.tests.cc + string_util.tests.cc + network.tcp.tests.cc + test_base.cc) +target_include_directories(test_base PUBLIC ../third-party/doctest-root) +target_link_libraries(test_base base pcrepp ZLIB::ZLIB) +add_test(NAME test_base COMMAND test_base) diff --git a/src/base/Makefile.am b/src/base/Makefile.am new file mode 100644 index 0000000..4a459a6 --- /dev/null +++ b/src/base/Makefile.am @@ -0,0 +1,110 @@ + +include $(top_srcdir)/aminclude_static.am + +AM_CPPFLAGS = \ + $(CODE_COVERAGE_CPPFLAGS) \ + -Wall \ + -I$(top_srcdir)/src/ \ + -I$(top_srcdir)/src/third-party \ + -I$(top_srcdir)/src/fmtlib \ + -I$(top_srcdir)/src/third-party/scnlib/include \ + $(LIBARCHIVE_CFLAGS) \ + $(READLINE_CFLAGS) \ + $(SQLITE3_CFLAGS) \ + $(PCRE_CFLAGS) \ + $(LIBCURL_CPPFLAGS) + +AM_LIBS = $(CODE_COVERAGE_LIBS) +AM_CFLAGS = $(CODE_COVERAGE_CFLAGS) +AM_CXXFLAGS = $(CODE_COVERAGE_CXXFLAGS) + +noinst_LIBRARIES = libbase.a + +noinst_HEADERS = \ + ansi_scrubber.hh \ + attr_line.hh \ + attr_line.builder.hh \ + auto_fd.hh \ + auto_mem.hh \ + auto_pid.hh \ + bus.hh \ + date_time_scanner.hh \ + enum_util.hh \ + file_range.hh \ + fs_util.hh \ + func_util.hh \ + future_util.hh \ + humanize.hh \ + humanize.network.hh \ + humanize.time.hh \ + injector.hh \ + injector.bind.hh \ + intern_string.hh \ + is_utf8.hh \ + isc.hh \ + itertools.hh \ + lnav_log.hh \ + lnav.console.hh \ + lnav.console.into.hh \ + lnav.gzip.hh \ + log_level_enum.hh \ + lrucache.hpp \ + math_util.hh \ + network.tcp.hh \ + opt_util.hh \ + paths.hh \ + result.h \ + snippet_highlighters.hh \ + string_attr_type.hh \ + string_util.hh \ + strnatcmp.h \ + time_util.hh + +libbase_a_SOURCES = \ + ansi_scrubber.cc \ + attr_line.cc \ + attr_line.builder.cc \ + auto_pid.cc \ + date_time_scanner.cc \ + fs_util.cc \ + humanize.cc \ + humanize.network.cc \ + humanize.time.cc \ + intern_string.cc \ + is_utf8.cc \ + isc.cc \ + lnav.console.cc \ + lnav.gzip.cc \ + lnav_log.cc \ + network.tcp.cc \ + paths.cc \ + snippet_highlighters.cc \ + string_attr_type.cc \ + string_util.cc \ + strnatcmp.c \ + time_util.cc \ + ../third-party/xxHash/xxhash.h \ + ../third-party/xxHash/xxhash.c + +check_PROGRAMS = \ + test_base + +test_base_SOURCES = \ + attr_line.tests.cc \ + fs_util.tests.cc \ + humanize.file_size.tests.cc \ + humanize.network.tests.cc \ + humanize.time.tests.cc \ + intern_string.tests.cc \ + lnav.gzip.tests.cc \ + string_util.tests.cc \ + test_base.cc + +test_base_LDADD = \ + libbase.a \ + ../fmtlib/libcppfmt.a \ + ../third-party/scnlib/src/libscnlib.a \ + ../pcrepp/libpcrepp.a + +TESTS = \ + test_base diff --git a/src/base/README.md b/src/base/README.md new file mode 100644 index 0000000..944dde8 --- /dev/null +++ b/src/base/README.md @@ -0,0 +1 @@ +# libbase -- Library of utility functions diff --git a/src/base/ansi_scrubber.cc b/src/base/ansi_scrubber.cc new file mode 100644 index 0000000..98f6c96 --- /dev/null +++ b/src/base/ansi_scrubber.cc @@ -0,0 +1,388 @@ +/** + * Copyright (c) 2013, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file ansi_scrubber.cc + */ + +#include + +#include "ansi_scrubber.hh" + +#include "base/opt_util.hh" +#include "config.h" +#include "pcrepp/pcre2pp.hh" +#include "scn/scn.h" +#include "view_curses.hh" + +static const lnav::pcre2pp::code& +ansi_regex() +{ + static const auto retval = lnav::pcre2pp::code::from_const( + "\x1b\\[([\\d=;\\?]*)([a-zA-Z])|(?:\\X\x08\\X)+"); + + return retval; +} + +size_t +erase_ansi_escapes(string_fragment input) +{ + static thread_local auto md = lnav::pcre2pp::match_data::unitialized(); + + const auto& regex = ansi_regex(); + nonstd::optional move_start; + size_t fill_index = 0; + + auto matcher = regex.capture_from(input).into(md); + while (true) { + auto match_res = matcher.matches(PCRE2_NO_UTF_CHECK); + + if (match_res.is()) { + break; + } + if (match_res.is()) { + log_error("ansi scrub regex failure"); + break; + } + + auto sf = md[0].value(); + auto bs_index_res = sf.codepoint_to_byte_index(1); + + if (move_start) { + auto move_len = sf.sf_begin - move_start.value(); + memmove(input.writable_data(fill_index), + input.data() + move_start.value(), + move_len); + fill_index += move_len; + } else { + fill_index = sf.sf_begin; + } + + if (sf.length() >= 3 && bs_index_res.isOk() + && sf[bs_index_res.unwrap()] == '\b') + { + static const auto OVERSTRIKE_RE + = lnav::pcre2pp::code::from_const(R"((\X)\x08(\X))"); + + auto loop_res = OVERSTRIKE_RE.capture_from(sf).for_each( + [&fill_index, &input](lnav::pcre2pp::match_data& over_md) { + auto lhs = over_md[1].value(); + if (lhs == "_") { + auto rhs = over_md[2].value(); + memmove(input.writable_data(fill_index), + rhs.data(), + rhs.length()); + fill_index += rhs.length(); + } else { + memmove(input.writable_data(fill_index), + lhs.data(), + lhs.length()); + fill_index += lhs.length(); + } + }); + } + move_start = md.remaining().sf_begin; + } + + memmove(input.writable_data(fill_index), + md.remaining().data(), + md.remaining().length()); + fill_index += md.remaining().length(); + + return fill_index; +} + +void +scrub_ansi_string(std::string& str, string_attrs_t* sa) +{ + static thread_local auto md = lnav::pcre2pp::match_data::unitialized(); + const auto& regex = ansi_regex(); + int64_t origin_offset = 0; + int last_origin_offset_end = 0; + + replace(str.begin(), str.end(), '\0', ' '); + auto matcher = regex.capture_from(str).into(md); + while (true) { + auto match_res = matcher.matches(PCRE2_NO_UTF_CHECK); + + if (match_res.is()) { + break; + } + if (match_res.is()) { + log_error("ansi scrub regex failure"); + break; + } + + const auto sf = md[0].value(); + auto bs_index_res = sf.codepoint_to_byte_index(1); + + if (sf.length() >= 3 && bs_index_res.isOk() + && sf[bs_index_res.unwrap()] == '\b') + { + ssize_t fill_index = sf.sf_begin; + line_range bold_range; + line_range ul_range; + auto sub_sf = sf; + + while (!sub_sf.empty()) { + auto lhs_opt = sub_sf.consume_codepoint(); + if (!lhs_opt) { + return; + } + auto lhs_pair = lhs_opt.value(); + auto mid_opt = lhs_pair.second.consume_codepoint(); + if (!mid_opt) { + return; + } + auto mid_pair = mid_opt.value(); + auto rhs_opt = mid_pair.second.consume_codepoint(); + if (!rhs_opt) { + return; + } + auto rhs_pair = rhs_opt.value(); + sub_sf = rhs_pair.second; + + if (lhs_pair.first == '_' || rhs_pair.first == '_') { + if (sa != nullptr && bold_range.is_valid()) { + sa->emplace_back(bold_range, + VC_STYLE.value(text_attrs{A_BOLD})); + bold_range.clear(); + } + if (ul_range.is_valid()) { + ul_range.lr_end += 1; + } else { + ul_range.lr_start = fill_index; + ul_range.lr_end = fill_index + 1; + } + auto cp = lhs_pair.first == '_' ? rhs_pair.first + : lhs_pair.first; + ww898::utf::utf8::write(cp, [&str, &fill_index](auto ch) { + str[fill_index++] = ch; + }); + } else { + if (sa != nullptr && ul_range.is_valid()) { + sa->emplace_back( + ul_range, VC_STYLE.value(text_attrs{A_UNDERLINE})); + ul_range.clear(); + } + if (bold_range.is_valid()) { + bold_range.lr_end += 1; + } else { + bold_range.lr_start = fill_index; + bold_range.lr_end = fill_index + 1; + } + try { + ww898::utf::utf8::write(lhs_pair.first, + [&str, &fill_index](auto ch) { + str[fill_index++] = ch; + }); + } catch (const std::runtime_error& e) { + log_error("invalid UTF-8 at %d", sf.sf_begin); + return; + } + } + } + + auto output_size = fill_index - sf.sf_begin; + auto erased_size = sf.length() - output_size; + + if (sa != nullptr) { +#if 0 + shift_string_attrs( + *sa, caps->c_begin + sf.length() / 3, -erased_size); +#endif + sa->emplace_back(line_range{last_origin_offset_end, + sf.sf_begin + (int) output_size}, + SA_ORIGIN_OFFSET.value(origin_offset)); + } + + if (sa != nullptr && ul_range.is_valid()) { + sa->emplace_back(ul_range, + VC_STYLE.value(text_attrs{A_UNDERLINE})); + ul_range.clear(); + } + if (sa != nullptr && bold_range.is_valid()) { + sa->emplace_back(bold_range, + VC_STYLE.value(text_attrs{A_BOLD})); + bold_range.clear(); + } + + str.erase(str.begin() + fill_index, str.begin() + sf.sf_end); + last_origin_offset_end = sf.sf_begin + output_size; + origin_offset += erased_size; + matcher.reload_input(str, last_origin_offset_end); + continue; + } + + auto seq = md[1].value(); + auto terminator = md[2].value(); + struct line_range lr; + bool has_attrs = false; + text_attrs attrs; + nonstd::optional role; + size_t lpc; + + switch (terminator[0]) { + case 'm': + for (lpc = seq.sf_begin; + lpc != std::string::npos && lpc < (size_t) seq.sf_end;) + { + auto ansi_code_res = scn::scan_value( + scn::string_view{&str[lpc], &str[seq.sf_end]}); + + if (ansi_code_res) { + auto ansi_code = ansi_code_res.value(); + if (90 <= ansi_code && ansi_code <= 97) { + ansi_code -= 60; + attrs.ta_attrs |= A_STANDOUT; + } + if (30 <= ansi_code && ansi_code <= 37) { + attrs.ta_fg_color = ansi_code - 30; + } + if (40 <= ansi_code && ansi_code <= 47) { + attrs.ta_bg_color = ansi_code - 40; + } + switch (ansi_code) { + case 1: + attrs.ta_attrs |= A_BOLD; + break; + + case 2: + attrs.ta_attrs |= A_DIM; + break; + + case 4: + attrs.ta_attrs |= A_UNDERLINE; + break; + + case 7: + attrs.ta_attrs |= A_REVERSE; + break; + } + } + lpc = str.find(';', lpc); + if (lpc != std::string::npos) { + lpc += 1; + } + } + has_attrs = true; + break; + + case 'C': { + auto spaces_res + = scn::scan_value(seq.to_string_view()); + + if (spaces_res && spaces_res.value() > 0) { + str.insert((std::string::size_type) sf.sf_end, + spaces_res.value(), + ' '); + } + break; + } + + case 'H': { + unsigned int row = 0, spaces = 0; + + if (scn::scan(seq.to_string_view(), "{};{}", row, spaces) + && spaces > 1) + { + int ispaces = spaces - 1; + if (ispaces > sf.sf_begin) { + str.insert((unsigned long) sf.sf_end, + ispaces - sf.sf_begin, + ' '); + } + } + break; + } + + case 'O': { + auto role_res = scn::scan_value(seq.to_string_view()); + + if (role_res) { + role_t role_tmp = (role_t) role_res.value(); + if (role_tmp > role_t::VCR_NONE + && role_tmp < role_t::VCR__MAX) + { + role = role_tmp; + has_attrs = true; + } + } + break; + } + } + str.erase(str.begin() + sf.sf_begin, str.begin() + sf.sf_end); + if (sa != nullptr) { + shift_string_attrs(*sa, sf.sf_begin, -sf.length()); + + if (has_attrs) { + for (auto rit = sa->rbegin(); rit != sa->rend(); rit++) { + if (rit->sa_range.lr_end != -1) { + continue; + } + rit->sa_range.lr_end = sf.sf_begin; + } + lr.lr_start = sf.sf_begin; + lr.lr_end = -1; + if (attrs.ta_attrs || attrs.ta_fg_color || attrs.ta_bg_color) { + sa->emplace_back(lr, VC_STYLE.value(attrs)); + } + role | [&lr, &sa](role_t r) { + sa->emplace_back(lr, VC_ROLE.value(r)); + }; + } + sa->emplace_back(line_range{last_origin_offset_end, sf.sf_begin}, + SA_ORIGIN_OFFSET.value(origin_offset)); + last_origin_offset_end = sf.sf_begin; + origin_offset += sf.length(); + } + + matcher.reload_input(str, sf.sf_begin); + } + + if (sa != nullptr && last_origin_offset_end > 0) { + sa->emplace_back(line_range{last_origin_offset_end, (int) str.size()}, + SA_ORIGIN_OFFSET.value(origin_offset)); + } +} + +void +add_ansi_vars(std::map& vars) +{ + vars["ansi_csi"] = ANSI_CSI; + vars["ansi_norm"] = ANSI_NORM; + vars["ansi_bold"] = ANSI_BOLD_START; + vars["ansi_underline"] = ANSI_UNDERLINE_START; + vars["ansi_black"] = ANSI_COLOR(COLOR_BLACK); + vars["ansi_red"] = ANSI_COLOR(COLOR_RED); + vars["ansi_green"] = ANSI_COLOR(COLOR_GREEN); + vars["ansi_yellow"] = ANSI_COLOR(COLOR_YELLOW); + vars["ansi_blue"] = ANSI_COLOR(COLOR_BLUE); + vars["ansi_magenta"] = ANSI_COLOR(COLOR_MAGENTA); + vars["ansi_cyan"] = ANSI_COLOR(COLOR_CYAN); + vars["ansi_white"] = ANSI_COLOR(COLOR_WHITE); +} diff --git a/src/base/ansi_scrubber.hh b/src/base/ansi_scrubber.hh new file mode 100644 index 0000000..b832e17 --- /dev/null +++ b/src/base/ansi_scrubber.hh @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2013, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file ansi_scrubber.hh + */ + +#ifndef lnav_ansi_scrubber_hh +#define lnav_ansi_scrubber_hh + +#include +#include + +#include "attr_line.hh" +#include "shlex.resolver.hh" + +#define ANSI_CSI "\x1b[" +#define ANSI_CHAR_ATTR "m" +#define ANSI_BOLD_PARAM "1" +#define ANSI_BOLD_START ANSI_CSI ANSI_BOLD_PARAM ANSI_CHAR_ATTR +#define ANSI_UNDERLINE_START ANSI_CSI "4m" +#define ANSI_NORM ANSI_CSI "0m" +#define ANSI_STRIKE_PARAM "9" +#define ANSI_STRIKE_START ANSI_CSI ANSI_STRIKE_PARAM ANSI_CHAR_ATTR + +#define ANSI_BOLD(msg) ANSI_BOLD_START msg ANSI_NORM +#define ANSI_UNDERLINE(msg) ANSI_UNDERLINE_START msg ANSI_NORM + +#define ANSI_ROLE(msg) ANSI_CSI "%dO" msg ANSI_NORM +#define XANSI_COLOR(col) "3" #col +#define ANSI_COLOR_PARAM(col) XANSI_COLOR(col) +#define ANSI_COLOR(col) ANSI_CSI XANSI_COLOR(col) "m" + +/** + * Check a string for ANSI escape sequences, process them, remove them, and add + * any style attributes to the given attribute container. + * + * @param str The string to check for ANSI escape sequences. + * @param sa The container for any style attributes. + */ +void scrub_ansi_string(std::string& str, string_attrs_t* sa); + +size_t erase_ansi_escapes(string_fragment input); + +/** + * Populate a variable map with strings that contain escape sequences that + * might be useful to script writers. + */ +void add_ansi_vars(std::map& vars); + +#endif diff --git a/src/base/attr_line.builder.cc b/src/base/attr_line.builder.cc new file mode 100644 index 0000000..95416dc --- /dev/null +++ b/src/base/attr_line.builder.cc @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "attr_line.builder.hh" diff --git a/src/base/attr_line.builder.hh b/src/base/attr_line.builder.hh new file mode 100644 index 0000000..1e62532 --- /dev/null +++ b/src/base/attr_line.builder.hh @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_attr_line_builder_hh +#define lnav_attr_line_builder_hh + +#include + +#include "attr_line.hh" + +class attr_line_builder { +public: + explicit attr_line_builder(attr_line_t& al) : alb_line(al) {} + + class attr_guard { + public: + attr_guard(attr_line_t& al, string_attr_pair sap) + : ag_line(al), ag_start(al.get_string().length()), + ag_attr(std::move(sap)) + { + } + + attr_guard(const attr_guard&) = delete; + + attr_guard& operator=(const attr_guard&) = delete; + + attr_guard(attr_guard&& other) noexcept + : ag_line(other.ag_line), ag_start(other.ag_start), + ag_attr(std::move(other.ag_attr)) + { + other.ag_start = nonstd::nullopt; + } + + ~attr_guard() + { + if (this->ag_start) { + this->ag_line.al_attrs.emplace_back( + line_range{ + this->ag_start.value(), + (int) this->ag_line.get_string().length(), + }, + this->ag_attr); + } + } + + private: + attr_line_t& ag_line; + nonstd::optional ag_start; + string_attr_pair ag_attr; + }; + + attr_guard with_attr(string_attr_pair sap) + { + return {this->alb_line, std::move(sap)}; + } + + template + attr_line_builder& overlay_attr(Args... args) + { + this->alb_line.al_attrs.template emplace_back(args...); + return *this; + } + + template + attr_line_builder& overlay_attr_for_char(int index, Args... args) + { + this->alb_line.al_attrs.template emplace_back( + line_range{index, index + 1}, args...); + return *this; + } + + template + attr_line_builder& append(Args... args) + { + this->alb_line.append(args...); + + return *this; + } + + attr_line_builder& indent(size_t amount) + { + auto pre = this->with_attr(SA_PREFORMATTED.value()); + + this->append(amount, ' '); + + return *this; + } + +private: + attr_line_t& alb_line; +}; + +#endif diff --git a/src/base/attr_line.cc b/src/base/attr_line.cc new file mode 100644 index 0000000..b65f4ba --- /dev/null +++ b/src/base/attr_line.cc @@ -0,0 +1,537 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "attr_line.hh" + +#include + +#include "ansi_scrubber.hh" +#include "auto_mem.hh" +#include "config.h" +#include "lnav_log.hh" +#include "pcrepp/pcre2pp.hh" + +attr_line_t& +attr_line_t::with_ansi_string(const char* str, ...) +{ + auto_mem formatted_str; + va_list args; + + va_start(args, str); + auto ret = vasprintf(formatted_str.out(), str, args); + va_end(args); + + if (ret >= 0 && formatted_str != nullptr) { + this->al_string = formatted_str; + scrub_ansi_string(this->al_string, &this->al_attrs); + } + + return *this; +} + +attr_line_t& +attr_line_t::with_ansi_string(const std::string& str) +{ + this->al_string = str; + scrub_ansi_string(this->al_string, &this->al_attrs); + + return *this; +} + +namespace text_stream { +struct word { + string_fragment w_word; + string_fragment w_remaining; +}; + +struct space { + string_fragment s_value; + string_fragment s_remaining; +}; + +struct corrupt { + string_fragment c_value; + string_fragment c_remaining; +}; + +struct eof { + string_fragment e_remaining; +}; + +using chunk = mapbox::util::variant; + +chunk +consume(const string_fragment text) +{ + static const auto WORD_RE + = lnav::pcre2pp::code::from_const(R"((*UTF)^[^\p{Z}\p{So}\p{C}]+)"); + static const auto SPACE_RE + = lnav::pcre2pp::code::from_const(R"((*UTF)^\s)"); + + if (text.empty()) { + return eof{text}; + } + + auto word_find_res + = WORD_RE.find_in(text, PCRE2_NO_UTF_CHECK).ignore_error(); + if (word_find_res) { + auto split_res = text.split_n(word_find_res->f_all.length()).value(); + + return word{split_res.first, split_res.second}; + } + + if (isspace(text.front())) { + auto split_res = text.split_n(1).value(); + + return space{split_res.first, split_res.second}; + } + + auto space_find_res + = SPACE_RE.find_in(text, PCRE2_NO_UTF_CHECK).ignore_error(); + if (space_find_res) { + auto split_res = text.split_n(space_find_res->f_all.length()).value(); + + return space{split_res.first, split_res.second}; + } + + auto csize_res = ww898::utf::utf8::char_size( + [&text]() { return std::make_pair(text.front(), text.length()); }); + + if (csize_res.isErr()) { + auto split_res = text.split_n(1); + + return corrupt{split_res->first, split_res->second}; + } + + auto split_res = text.split_n(csize_res.unwrap()); + + return word{split_res->first, split_res->second}; +} + +} // namespace text_stream + +static void +split_attrs(attr_line_t& al, const line_range& lr) +{ + if (lr.empty()) { + return; + } + + string_attrs_t new_attrs; + + for (auto& attr : al.al_attrs) { + if (!lr.intersects(attr.sa_range)) { + continue; + } + + new_attrs.emplace_back(line_range{lr.lr_end, attr.sa_range.lr_end}, + std::make_pair(attr.sa_type, attr.sa_value)); + attr.sa_range.lr_end = lr.lr_start; + } + for (auto& new_attr : new_attrs) { + al.al_attrs.emplace_back(std::move(new_attr)); + } +} + +attr_line_t& +attr_line_t::insert(size_t index, + const attr_line_t& al, + text_wrap_settings* tws) +{ + if (index < this->al_string.length()) { + shift_string_attrs(this->al_attrs, index, al.al_string.length()); + } + + this->al_string.insert(index, al.al_string); + + for (const auto& sa : al.al_attrs) { + this->al_attrs.emplace_back(sa); + + line_range& lr = this->al_attrs.back().sa_range; + + lr.shift(0, index); + if (lr.lr_end == -1) { + lr.lr_end = index + al.al_string.length(); + } + } + + if (tws == nullptr) { + return *this; + } + + auto starting_line_index = this->al_string.rfind('\n', index); + if (starting_line_index == std::string::npos) { + starting_line_index = 0; + } else { + starting_line_index += 1; + } + + const ssize_t usable_width = tws->tws_width - tws->tws_indent; + + auto text_to_wrap = string_fragment::from_str_range( + this->al_string, starting_line_index, this->al_string.length()); + string_fragment last_word; + ssize_t line_ch_count = 0; + auto needs_indent = false; + + while (!text_to_wrap.empty()) { + if (needs_indent) { + this->insert(text_to_wrap.sf_begin, + tws->tws_indent + tws->tws_padding_indent, + ' '); + auto indent_lr = line_range{ + text_to_wrap.sf_begin, + text_to_wrap.sf_begin + tws->tws_indent, + }; + split_attrs(*this, indent_lr); + indent_lr.lr_end += tws->tws_padding_indent; + line_ch_count += tws->tws_padding_indent; + if (!indent_lr.empty()) { + this->al_attrs.emplace_back(indent_lr, SA_PREFORMATTED.value()); + } + text_to_wrap = text_to_wrap.prepend( + this->al_string.data(), + tws->tws_indent + tws->tws_padding_indent); + needs_indent = false; + } + auto chunk = text_stream::consume(text_to_wrap); + + text_to_wrap = chunk.match( + [&](text_stream::word word) { + auto ch_count + = word.w_word.utf8_length().unwrapOr(word.w_word.length()); + + if ((line_ch_count + ch_count) > usable_width + && find_string_attr_containing(this->al_attrs, + &SA_PREFORMATTED, + text_to_wrap.sf_begin) + == this->al_attrs.end()) + { + this->insert(word.w_word.sf_begin, 1, '\n'); + this->insert(word.w_word.sf_begin + 1, + tws->tws_indent + tws->tws_padding_indent, + ' '); + auto indent_lr = line_range{ + word.w_word.sf_begin + 1, + word.w_word.sf_begin + 1 + tws->tws_indent, + }; + split_attrs(*this, indent_lr); + indent_lr.lr_end += tws->tws_padding_indent; + if (!indent_lr.empty()) { + this->al_attrs.emplace_back(indent_lr, + SA_PREFORMATTED.value()); + } + line_ch_count = tws->tws_padding_indent + ch_count; + auto trailing_space_count = 0; + if (!last_word.empty()) { + trailing_space_count + = word.w_word.sf_begin - last_word.sf_begin; + this->erase(last_word.sf_begin, trailing_space_count); + } + return word.w_remaining + .erase_before(this->al_string.data(), + trailing_space_count) + .prepend(this->al_string.data(), + 1 + tws->tws_indent + tws->tws_padding_indent); + } + line_ch_count += ch_count; + + return word.w_remaining; + }, + [&](text_stream::space space) { + if (space.s_value == "\n") { + line_ch_count = 0; + needs_indent = true; + return space.s_remaining; + } + + if (line_ch_count > 0) { + auto ch_count = space.s_value.utf8_length().unwrapOr( + space.s_value.length()); + + if ((line_ch_count + ch_count) > usable_width + && find_string_attr_containing(this->al_attrs, + &SA_PREFORMATTED, + text_to_wrap.sf_begin) + == this->al_attrs.end()) + { + this->erase(space.s_value.sf_begin, + space.s_value.length()); + this->insert(space.s_value.sf_begin, "\n"); + line_ch_count = 0; + needs_indent = true; + + auto trailing_space_count = 0; + if (!last_word.empty()) { + trailing_space_count + = space.s_value.sf_begin - last_word.sf_begin; + this->erase(last_word.sf_end, trailing_space_count); + } + + return space.s_remaining + .erase_before( + this->al_string.data(), + space.s_value.length() + trailing_space_count) + .prepend(this->al_string.data(), 1); + } + line_ch_count += ch_count; + } else if (find_string_attr_containing(this->al_attrs, + &SA_PREFORMATTED, + text_to_wrap.sf_begin) + == this->al_attrs.end()) + { + this->erase(space.s_value.sf_begin, space.s_value.length()); + return space.s_remaining.erase_before( + this->al_string.data(), space.s_value.length()); + } + + return space.s_remaining; + }, + [](text_stream::corrupt corrupt) { return corrupt.c_remaining; }, + [](text_stream::eof eof) { return eof.e_remaining; }); + + if (chunk.is()) { + last_word = text_to_wrap; + } + + ensure(this->al_string.data() == text_to_wrap.sf_string); + ensure(text_to_wrap.sf_begin <= text_to_wrap.sf_end); + } + return *this; +} + +attr_line_t +attr_line_t::subline(size_t start, size_t len) const +{ + if (len == std::string::npos) { + len = this->al_string.length() - start; + } + + line_range lr{(int) start, (int) (start + len)}; + attr_line_t retval; + + retval.al_string = this->al_string.substr(start, len); + for (const auto& sa : this->al_attrs) { + if (!lr.intersects(sa.sa_range)) { + continue; + } + + auto ilr = lr.intersection(sa.sa_range).shift(0, -lr.lr_start); + retval.al_attrs.emplace_back(ilr, + std::make_pair(sa.sa_type, sa.sa_value)); + const auto& last_lr = retval.al_attrs.back().sa_range; + + ensure(last_lr.lr_end <= (int) retval.al_string.length()); + } + + return retval; +} + +void +attr_line_t::split_lines(std::vector& lines) const +{ + size_t pos = 0, next_line; + + while ((next_line = this->al_string.find('\n', pos)) != std::string::npos) { + lines.emplace_back(this->subline(pos, next_line - pos)); + pos = next_line + 1; + } + lines.emplace_back(this->subline(pos)); +} + +attr_line_t& +attr_line_t::right_justify(unsigned long width) +{ + long padding = width - this->length(); + if (padding > 0) { + this->al_string.insert(0, padding, ' '); + for (auto& al_attr : this->al_attrs) { + if (al_attr.sa_range.lr_start > 0) { + al_attr.sa_range.lr_start += padding; + } + if (al_attr.sa_range.lr_end != -1) { + al_attr.sa_range.lr_end += padding; + } + } + } + + return *this; +} + +size_t +attr_line_t::nearest_text(size_t x) const +{ + if (x > 0 && x >= (size_t) this->length()) { + if (this->empty()) { + x = 0; + } else { + x = this->length() - 1; + } + } + + while (x > 0 && isspace(this->al_string[x])) { + x -= 1; + } + + return x; +} + +void +attr_line_t::apply_hide() +{ + auto& sa = this->al_attrs; + + for (auto& sattr : sa) { + if (sattr.sa_type == &SA_HIDDEN && sattr.sa_range.length() > 3) { + auto& lr = sattr.sa_range; + + std::for_each(sa.begin(), sa.end(), [&](string_attr& attr) { + if (attr.sa_type == &VC_STYLE && lr.contains(attr.sa_range)) { + attr.sa_type = &SA_REMOVED; + } + }); + + this->al_string.replace(lr.lr_start, lr.length(), "\xE2\x8B\xAE"); + shift_string_attrs(sa, lr.lr_start + 1, -(lr.length() - 3)); + sattr.sa_type = &VC_ROLE; + sattr.sa_value = role_t::VCR_HIDDEN; + lr.lr_end = lr.lr_start + 3; + } + } +} + +attr_line_t& +attr_line_t::rtrim() +{ + auto index = this->al_string.length(); + + for (; index > 0; index--) { + if (find_string_attr_containing( + this->al_attrs, &SA_PREFORMATTED, index - 1) + != this->al_attrs.end()) + { + break; + } + if (!isspace(this->al_string[index - 1])) { + break; + } + } + if (index > 0 && index < this->al_string.length()) { + this->erase(index); + } + return *this; +} + +attr_line_t& +attr_line_t::erase(size_t pos, size_t len) +{ + if (len == std::string::npos) { + len = this->al_string.size() - pos; + } + if (len == 0) { + return *this; + } + + this->al_string.erase(pos, len); + + shift_string_attrs(this->al_attrs, pos, -((int32_t) len)); + auto new_end = std::remove_if( + this->al_attrs.begin(), this->al_attrs.end(), [](const auto& attr) { + return attr.sa_range.empty(); + }); + this->al_attrs.erase(new_end, this->al_attrs.end()); + + return *this; +} + +attr_line_t& +attr_line_t::pad_to(ssize_t size) +{ + const auto curr_len = this->utf8_length_or_length(); + + if (curr_len < size) { + this->append((size - curr_len), ' '); + for (auto& attr : this->al_attrs) { + if (attr.sa_range.lr_start == 0 && attr.sa_range.lr_end == curr_len) + { + attr.sa_range.lr_end = this->al_string.length(); + } + } + } + + return *this; +} + +line_range +line_range::intersection(const line_range& other) const +{ + int actual_end; + + if (this->lr_end == -1) { + actual_end = other.lr_end; + } else if (other.lr_end == -1) { + actual_end = this->lr_end; + } else { + actual_end = std::min(this->lr_end, other.lr_end); + } + return line_range{std::max(this->lr_start, other.lr_start), actual_end}; +} + +line_range& +line_range::shift(int32_t start, int32_t amount) +{ + if (start == this->lr_start) { + if (amount > 0) { + this->lr_start += amount; + } + if (this->lr_end != -1) { + this->lr_end += amount; + if (this->lr_end < this->lr_start) { + this->lr_end = this->lr_start; + } + } + } else if (start < this->lr_start) { + this->lr_start = std::max(0, this->lr_start + amount); + if (this->lr_end != -1) { + this->lr_end = std::max(0, this->lr_end + amount); + } + } else if (this->lr_end != -1) { + if (start < this->lr_end) { + if (amount < 0 && amount < (start - this->lr_end)) { + this->lr_end = start; + } else { + this->lr_end = std::max(this->lr_start, this->lr_end + amount); + } + } + } + + return *this; +} diff --git a/src/base/attr_line.hh b/src/base/attr_line.hh new file mode 100644 index 0000000..c9cb6a8 --- /dev/null +++ b/src/base/attr_line.hh @@ -0,0 +1,758 @@ +/** + * Copyright (c) 2017, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file attr_line.hh + */ + +#ifndef attr_line_hh +#define attr_line_hh + +#include +#include +#include + +#include + +#include "fmt/format.h" +#include "intern_string.hh" +#include "string_attr_type.hh" +#include "string_util.hh" + +/** + * Encapsulates a range in a string. + */ +struct line_range { + enum class unit { + bytes, + codepoint, + }; + + int lr_start; + int lr_end; + unit lr_unit; + + explicit line_range(int start = -1, int end = -1, unit u = unit::bytes) + : lr_start(start), lr_end(end), lr_unit(u) + { + } + + bool is_valid() const { return this->lr_start != -1; } + + int length() const + { + return this->lr_end == -1 ? INT_MAX : this->lr_end - this->lr_start; + } + + bool empty() const { return this->length() == 0; } + + void clear() + { + this->lr_start = -1; + this->lr_end = -1; + } + + int end_for_string(const std::string& str) const + { + return this->lr_end == -1 ? str.length() : this->lr_end; + } + + bool contains(int pos) const + { + return this->lr_start <= pos + && (this->lr_end == -1 || pos < this->lr_end); + } + + bool contains(const struct line_range& other) const + { + return this->contains(other.lr_start) + && (this->lr_end == -1 || other.lr_end <= this->lr_end); + } + + bool intersects(const struct line_range& other) const + { + if (this->contains(other.lr_start)) { + return true; + } + if (other.lr_end > 0 && this->contains(other.lr_end - 1)) { + return true; + } + if (other.contains(this->lr_start)) { + return true; + } + + return false; + } + + line_range intersection(const struct line_range& other) const; + + line_range& shift(int32_t start, int32_t amount); + + void ltrim(const char* str) + { + while (this->lr_start < this->lr_end && isspace(str[this->lr_start])) { + this->lr_start += 1; + } + } + + bool operator<(const struct line_range& rhs) const + { + if (this->lr_start < rhs.lr_start) { + return true; + } + if (this->lr_start > rhs.lr_start) { + return false; + } + + // this->lr_start == rhs.lr_start + if (this->lr_end == rhs.lr_end) { + return false; + } + + if (this->lr_end < rhs.lr_end) { + return false; + } + return true; + } + + bool operator==(const struct line_range& rhs) const + { + return (this->lr_start == rhs.lr_start && this->lr_end == rhs.lr_end); + } + + const char* substr(const std::string& str) const + { + if (this->lr_start == -1) { + return str.c_str(); + } + return &(str.c_str()[this->lr_start]); + } + + size_t sublen(const std::string& str) const + { + if (this->lr_start == -1) { + return str.length(); + } + if (this->lr_end == -1) { + return str.length() - this->lr_start; + } + return this->length(); + } +}; + +inline line_range +to_line_range(const string_fragment& frag) +{ + return line_range{frag.sf_begin, frag.sf_end}; +} + +struct string_attr { + string_attr(const struct line_range& lr, const string_attr_pair& value) + : sa_range(lr), sa_type(value.first), sa_value(value.second) + { + } + + string_attr() = default; + + bool operator<(const struct string_attr& rhs) const + { + if (this->sa_range < rhs.sa_range) { + return true; + } + if (this->sa_range == rhs.sa_range && this->sa_type == rhs.sa_type + && this->sa_type == &VC_ROLE + && this->sa_value.get() < rhs.sa_value.get()) + { + return true; + } + + return false; + } + + struct line_range sa_range; + const string_attr_type_base* sa_type{nullptr}; + string_attr_value sa_value; +}; + +template +struct string_attr_wrapper { + explicit string_attr_wrapper(const string_attr* sa) : saw_string_attr(sa) {} + + template + std::enable_if_t::value, const U&> get() const + { + return this->saw_string_attr->sa_value.template get(); + } + + const string_attr* saw_string_attr; +}; + +/** A map of line ranges to attributes for that range. */ +using string_attrs_t = std::vector; + +inline string_attrs_t::const_iterator +find_string_attr(const string_attrs_t& sa, + const string_attr_type_base* type, + int start = 0) +{ + string_attrs_t::const_iterator iter; + + for (iter = sa.begin(); iter != sa.end(); ++iter) { + if (iter->sa_type == type && iter->sa_range.lr_start >= start) { + break; + } + } + + return iter; +} + +inline nonstd::optional +get_string_attr(const string_attrs_t& sa, + const string_attr_type_base* type, + int start = 0) +{ + auto iter = find_string_attr(sa, type, start); + + if (iter == sa.end()) { + return nonstd::nullopt; + } + + return nonstd::make_optional(&(*iter)); +} + +template +inline nonstd::optional> +get_string_attr(const string_attrs_t& sa, + const string_attr_type& type, + int start = 0) +{ + auto iter = find_string_attr(sa, &type, start); + + if (iter == sa.end()) { + return nonstd::nullopt; + } + + return nonstd::make_optional(string_attr_wrapper(&(*iter))); +} + +template +inline string_attrs_t::const_iterator +find_string_attr_containing(const string_attrs_t& sa, + const string_attr_type_base* type, + T x) +{ + string_attrs_t::const_iterator iter; + + for (iter = sa.begin(); iter != sa.end(); ++iter) { + if (iter->sa_type == type && iter->sa_range.contains(x)) { + break; + } + } + + return iter; +} + +inline string_attrs_t::iterator +find_string_attr(string_attrs_t& sa, const struct line_range& lr) +{ + string_attrs_t::iterator iter; + + for (iter = sa.begin(); iter != sa.end(); ++iter) { + if (lr.contains(iter->sa_range)) { + break; + } + } + + return iter; +} + +inline string_attrs_t::const_iterator +find_string_attr(const string_attrs_t& sa, size_t near) +{ + auto nearest = sa.end(); + ssize_t last_diff = INT_MAX; + + for (auto iter = sa.begin(); iter != sa.end(); ++iter) { + const auto& lr = iter->sa_range; + + if (!lr.is_valid() || !lr.contains(near)) { + continue; + } + + ssize_t diff = near - lr.lr_start; + if (diff < last_diff) { + last_diff = diff; + nearest = iter; + } + } + + return nearest; +} + +template +inline string_attrs_t::const_iterator +rfind_string_attr_if(const string_attrs_t& sa, ssize_t near, T predicate) +{ + auto nearest = sa.end(); + ssize_t last_diff = INT_MAX; + + for (auto iter = sa.begin(); iter != sa.end(); ++iter) { + const auto& lr = iter->sa_range; + + if (lr.lr_start > near) { + continue; + } + + if (!predicate(*iter)) { + continue; + } + + ssize_t diff = near - lr.lr_start; + if (diff < last_diff) { + last_diff = diff; + nearest = iter; + } + } + + return nearest; +} + +inline struct line_range +find_string_attr_range(const string_attrs_t& sa, string_attr_type_base* type) +{ + auto iter = find_string_attr(sa, type); + + if (iter != sa.end()) { + return iter->sa_range; + } + + return line_range(); +} + +inline void +remove_string_attr(string_attrs_t& sa, const struct line_range& lr) +{ + string_attrs_t::iterator iter; + + while ((iter = find_string_attr(sa, lr)) != sa.end()) { + sa.erase(iter); + } +} + +inline void +remove_string_attr(string_attrs_t& sa, string_attr_type_base* type) +{ + for (auto iter = sa.begin(); iter != sa.end();) { + if (iter->sa_type == type) { + iter = sa.erase(iter); + } else { + ++iter; + } + } +} + +inline void +shift_string_attrs(string_attrs_t& sa, int32_t start, int32_t amount) +{ + for (auto& iter : sa) { + iter.sa_range.shift(start, amount); + } +} + +struct text_wrap_settings { + text_wrap_settings& with_indent(int indent) + { + this->tws_indent = indent; + return *this; + } + + text_wrap_settings& with_padding_indent(int indent) + { + this->tws_padding_indent = indent; + return *this; + } + + text_wrap_settings& with_width(int width) + { + this->tws_width = width; + return *this; + } + + int tws_indent{2}; + int tws_width{80}; + int tws_padding_indent{0}; +}; + +/** + * A line that has attributes. + */ +class attr_line_t { +public: + attr_line_t() = default; + + attr_line_t(std::string str) : al_string(std::move(str)) {} + + attr_line_t(const char* str) : al_string(str) {} + + static inline attr_line_t from_ansi_str(const char* str) + { + attr_line_t retval; + + return retval.with_ansi_string("%s", str); + } + + /** @return The string itself. */ + std::string& get_string() { return this->al_string; } + + const std::string& get_string() const { return this->al_string; } + + /** @return The attributes for the string. */ + string_attrs_t& get_attrs() { return this->al_attrs; } + + const string_attrs_t& get_attrs() const { return this->al_attrs; } + + attr_line_t& with_string(const std::string& str) + { + this->al_string = str; + return *this; + } + + attr_line_t& with_ansi_string(const char* str, ...); + + attr_line_t& with_ansi_string(const std::string& str); + + attr_line_t& with_attr(const string_attr& sa) + { + this->al_attrs.push_back(sa); + return *this; + } + + attr_line_t& ensure_space() + { + if (!this->al_string.empty() && this->al_string.back() != ' ' + && this->al_string.back() != '[') + { + this->append(1, ' '); + } + + return *this; + } + + template + attr_line_t& append(S str, const string_attr_pair& value) + { + size_t start_len = this->al_string.length(); + + this->al_string.append(str); + + line_range lr{(int) start_len, (int) this->al_string.length()}; + + this->al_attrs.emplace_back(lr, value); + + return *this; + } + + template + attr_line_t& append(const std::pair& value) + { + size_t start_len = this->al_string.length(); + + this->al_string.append(std::move(value.first)); + + line_range lr{(int) start_len, (int) this->al_string.length()}; + + this->al_attrs.emplace_back(lr, value.second); + + return *this; + } + + template + attr_line_t& append_quoted(const std::pair& value) + { + this->al_string.append("\u201c"); + + size_t start_len = this->al_string.length(); + + this->al_string.append(std::move(value.first)); + + line_range lr{(int) start_len, (int) this->al_string.length()}; + + this->al_attrs.emplace_back(lr, value.second); + + this->al_string.append("\u201d"); + + return *this; + } + + attr_line_t& append_quoted(const intern_string_t str) + { + this->al_string.append("\u201c"); + this->al_string.append(str.get(), str.size()); + this->al_string.append("\u201d"); + + return *this; + } + + attr_line_t& append_quoted(const attr_line_t& al) + { + this->al_string.append("\u201c"); + this->append(al); + this->al_string.append("\u201d"); + + return *this; + } + + template + attr_line_t& append_quoted(S s) + { + this->al_string.append("\u201c"); + this->append(std::move(s)); + this->al_string.append("\u201d"); + + return *this; + } + + attr_line_t& append(const intern_string_t str) + { + this->al_string.append(str.get(), str.size()); + return *this; + } + + attr_line_t& append(const string_fragment& str) + { + this->al_string.append(str.data(), str.length()); + return *this; + } + + template + attr_line_t& append(S str) + { + this->al_string.append(str); + return *this; + } + + template + attr_line_t& appendf(fmt::format_string fstr, Args&&... args) + { + this->template append( + fmt::vformat(fstr, fmt::make_format_args(args...))); + return *this; + } + + attr_line_t& with_attr_for_all(const string_attr_pair& sap) + { + this->al_attrs.emplace_back(line_range{0, -1}, sap); + return *this; + } + + template + attr_line_t& join(const C& container, + const string_attr_pair& sap, + const F& fill) + { + bool init = true; + for (const auto& elem : container) { + if (!init) { + this->append(fill); + } + this->append(std::make_pair(elem, sap)); + init = false; + } + + return *this; + } + + template + attr_line_t& join(const C& container, const F& fill) + { + bool init = true; + for (const auto& elem : container) { + if (!init) { + this->append(fill); + } + this->append(elem); + init = false; + } + + return *this; + } + + attr_line_t& insert(size_t index, + const attr_line_t& al, + text_wrap_settings* tws = nullptr); + + attr_line_t& append(const attr_line_t& al, + text_wrap_settings* tws = nullptr) + { + return this->insert(this->al_string.length(), al, tws); + } + + attr_line_t& append(size_t len, char c) + { + this->al_string.append(len, c); + return *this; + } + + attr_line_t& insert(size_t index, size_t len, char c) + { + this->al_string.insert(index, len, c); + + shift_string_attrs(this->al_attrs, index, len); + + return *this; + } + + attr_line_t& insert(size_t index, const char* str) + { + this->al_string.insert(index, str); + + shift_string_attrs(this->al_attrs, index, strlen(str)); + + return *this; + } + + template + attr_line_t& add_header(Args... args) + { + if (!this->blank()) { + this->insert(0, args...); + } + return *this; + } + + template + attr_line_t& with_default(Args... args) + { + if (this->blank()) { + this->clear(); + this->append(args...); + } + + return *this; + } + + attr_line_t& erase(size_t pos, size_t len = std::string::npos); + + attr_line_t& rtrim(); + + attr_line_t& erase_utf8_chars(size_t start) + { + auto byte_index = utf8_char_to_byte_index(this->al_string, start); + this->erase(byte_index); + + return *this; + } + + attr_line_t& right_justify(unsigned long width); + + attr_line_t& pad_to(ssize_t size); + + ssize_t length() const + { + size_t retval = this->al_string.length(); + + for (const auto& al_attr : this->al_attrs) { + retval = std::max(retval, (size_t) al_attr.sa_range.lr_start); + if (al_attr.sa_range.lr_end != -1) { + retval = std::max(retval, (size_t) al_attr.sa_range.lr_end); + } + } + + return retval; + } + + Result utf8_length() const + { + return utf8_string_length(this->al_string); + } + + ssize_t utf8_length_or_length() const + { + return utf8_string_length(this->al_string).unwrapOr(this->length()); + } + + std::string get_substring(const line_range& lr) const + { + if (!lr.is_valid()) { + return ""; + } + return this->al_string.substr(lr.lr_start, lr.sublen(this->al_string)); + } + + string_fragment to_string_fragment( + string_attrs_t::const_iterator iter) const + { + return string_fragment(this->al_string.c_str(), + iter->sa_range.lr_start, + iter->sa_range.end_for_string(this->al_string)); + } + + string_attrs_t::const_iterator find_attr(size_t near) const + { + near = std::min(near, this->al_string.length() - 1); + + while (near > 0 && isspace(this->al_string[near])) { + near -= 1; + } + + return find_string_attr(this->al_attrs, near); + } + + bool empty() const { return this->length() == 0; } + + bool blank() const { return is_blank(this->al_string); } + + /** Clear the string and the attributes for the string. */ + attr_line_t& clear() + { + this->al_string.clear(); + this->al_attrs.clear(); + + return *this; + } + + attr_line_t subline(size_t start, size_t len = std::string::npos) const; + + void split_lines(std::vector& lines) const; + + std::vector split_lines() const + { + std::vector retval; + + this->split_lines(retval); + return retval; + } + + size_t nearest_text(size_t x) const; + + void apply_hide(); + + std::string al_string; + string_attrs_t al_attrs; +}; + +#endif diff --git a/src/base/attr_line.tests.cc b/src/base/attr_line.tests.cc new file mode 100644 index 0000000..53b338e --- /dev/null +++ b/src/base/attr_line.tests.cc @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "attr_line.hh" + +#include "config.h" +#include "doctest/doctest.h" + +using namespace lnav::roles::literals; + +TEST_CASE("attr_line_t::basic-wrapping") +{ + text_wrap_settings tws = {3, 21}; + attr_line_t to_be_wrapped{"This line, right here, needs to be wrapped."}; + attr_line_t dst; + + to_be_wrapped.al_attrs.emplace_back( + line_range{0, (int) to_be_wrapped.al_string.length()}, + VC_ROLE.value(role_t::VCR_ERROR)); + dst.append(to_be_wrapped, &tws); + + CHECK(dst.get_string() == + "This line, right\n" + " here, needs to be\n" + " wrapped."); + + for (const auto& attr : dst.al_attrs) { + printf("attr %d:%d %s\n", + attr.sa_range.lr_start, + attr.sa_range.lr_end, + attr.sa_type->sat_name); + } +} + +TEST_CASE("attr_line_t::unicode-wrap") +{ + text_wrap_settings tws = {3, 21}; + attr_line_t prefix; + + prefix.append(" ") + .append("\u2022"_list_glyph) + .append(" ") + .with_attr_for_all(SA_PREFORMATTED.value()); + + attr_line_t body; + body.append("This is a long line that needs to be wrapped and indented"); + + attr_line_t li; + + li.append(prefix) + .append(body, &tws) + .with_attr_for_all(SA_PREFORMATTED.value()); + + attr_line_t dst; + + dst.append(li); + + CHECK(dst.get_string() + == " \u2022 This is a long\n" + " line that needs to\n" + " be wrapped and\n" + " indented"); +} diff --git a/src/base/auto_fd.hh b/src/base/auto_fd.hh new file mode 100644 index 0000000..da4a582 --- /dev/null +++ b/src/base/auto_fd.hh @@ -0,0 +1,318 @@ +/** + * Copyright (c) 2007-2012, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file auto_fd.hh + */ + +#ifndef auto_fd_hh +#define auto_fd_hh + +#include +#include +#include + +#include +#include +#include +#include + +#include "base/lnav_log.hh" +#include "base/result.h" + +/** + * Resource management class for file descriptors. + * + * @see auto_ptr + */ +class auto_fd { +public: + /** + * Wrapper for the posix pipe(2) function that stores the file descriptor + * results in an auto_fd array. + * + * @param af An array of at least two auto_fd elements, where the first + * contains the reader end of the pipe and the second contains the writer. + * @return The result of the pipe(2) function. + */ + static int pipe(auto_fd* af) + { + int retval, fd[2]; + + require(af != nullptr); + + if ((retval = ::pipe(fd)) == 0) { + af[0] = fd[0]; + af[1] = fd[1]; + } + + return retval; + } + + /** + * dup(2) the given file descriptor and wrap it in an auto_fd. + * + * @param fd The file descriptor to duplicate. + * @return A new auto_fd that contains the duplicated file descriptor. + */ + static auto_fd dup_of(int fd) + { + if (fd == -1) { + return auto_fd{}; + } + + auto new_fd = ::dup(fd); + + if (new_fd == -1) { + throw std::bad_alloc(); + } + + return auto_fd(new_fd); + } + + /** + * Construct an auto_fd to manage the given file descriptor. + * + * @param fd The file descriptor to be managed. + */ + explicit auto_fd(int fd = -1) : af_fd(fd) { require(fd >= -1); } + + /** + * Non-const copy constructor. Management of the file descriptor will be + * transferred from the source to this object and the source will be + * cleared. + * + * @param af The source of the file descriptor. + */ + auto_fd(auto_fd&& af) noexcept : af_fd(af.release()) {} + + /** + * Const copy constructor. The file descriptor from the source will be + * dup(2)'d and the new descriptor stored in this object. + * + * @param af The source of the file descriptor. + */ + auto_fd(const auto_fd& af) = delete; + + auto_fd dup() const + { + int new_fd; + + if (this->af_fd == -1 || (new_fd = ::dup(this->af_fd)) == -1) { + throw std::bad_alloc(); + } + + return auto_fd{new_fd}; + } + + /** + * Destructor that will close the file descriptor managed by this object. + */ + ~auto_fd() { this->reset(); } + + /** @return The file descriptor as a plain integer. */ + operator int() const { return this->af_fd; } + + /** + * Replace the current descriptor with the given one. The current + * descriptor will be closed. + * + * @param fd The file descriptor to store in this object. + * @return *this + */ + auto_fd& operator=(int fd) + { + require(fd >= -1); + + this->reset(fd); + return *this; + } + + /** + * Transfer management of the given file descriptor to this object. + * + * @param af The old manager of the file descriptor. + * @return *this + */ + auto_fd& operator=(auto_fd&& af) noexcept + { + this->reset(af.release()); + return *this; + } + + /** + * Return a pointer that can be passed to functions that require an out + * parameter for file descriptors (e.g. openpty). + * + * @return A pointer to the internal integer. + */ + int* out() + { + this->reset(); + return &this->af_fd; + } + + /** + * Stop managing the file descriptor in this object and return its value. + * + * @return The file descriptor. + */ + int release() + { + int retval = this->af_fd; + + this->af_fd = -1; + return retval; + } + + /** + * @return The file descriptor. + */ + int get() const { return this->af_fd; } + + /** + * Closes the current file descriptor and replaces its value with the given + * one. + * + * @param fd The new file descriptor to be managed. + */ + void reset(int fd = -1) + { + require(fd >= -1); + + if (this->af_fd != fd) { + if (this->af_fd != -1) { + switch (this->af_fd) { + case STDIN_FILENO: + case STDOUT_FILENO: + case STDERR_FILENO: + break; + default: + close(this->af_fd); + break; + } + } + this->af_fd = fd; + } + } + + void close_on_exec() const + { + if (this->af_fd == -1) { + return; + } + log_perror(fcntl(this->af_fd, F_SETFD, FD_CLOEXEC)); + } + +private: + int af_fd; /*< The managed file descriptor. */ +}; + +class auto_pipe { +public: + static Result for_child_fd(int child_fd) + { + auto_pipe retval(child_fd); + + if (retval.open() == -1) { + return Err(std::string(strerror(errno))); + } + + return Ok(std::move(retval)); + } + + explicit auto_pipe(int child_fd = -1, int child_flags = O_RDONLY) + : ap_child_flags(child_flags), ap_child_fd(child_fd) + { + switch (child_fd) { + case STDIN_FILENO: + this->ap_child_flags = O_RDONLY; + break; + case STDOUT_FILENO: + case STDERR_FILENO: + this->ap_child_flags = O_WRONLY; + break; + } + } + + int open() { return auto_fd::pipe(this->ap_fd); } + + void close() + { + this->ap_fd[0].reset(); + this->ap_fd[1].reset(); + } + + auto_fd& read_end() { return this->ap_fd[0]; } + + auto_fd& write_end() { return this->ap_fd[1]; } + + void after_fork(pid_t child_pid) + { + int new_fd; + + switch (child_pid) { + case -1: + this->close(); + break; + case 0: + if (this->ap_child_flags == O_RDONLY) { + this->write_end().reset(); + if (this->read_end().get() == -1) { + this->read_end() = ::open("/dev/null", O_RDONLY); + } + new_fd = this->read_end().get(); + } else { + this->read_end().reset(); + if (this->write_end().get() == -1) { + this->write_end() = ::open("/dev/null", O_WRONLY); + } + new_fd = this->write_end().get(); + } + if (this->ap_child_fd != -1) { + if (new_fd != this->ap_child_fd) { + dup2(new_fd, this->ap_child_fd); + this->close(); + } + } + break; + default: + if (this->ap_child_flags == O_RDONLY) { + this->read_end().reset(); + } else { + this->write_end().reset(); + } + break; + } + } + + int ap_child_flags; + int ap_child_fd; + auto_fd ap_fd[2]; +}; + +#endif diff --git a/src/base/auto_mem.hh b/src/base/auto_mem.hh new file mode 100644 index 0000000..e6b456c --- /dev/null +++ b/src/base/auto_mem.hh @@ -0,0 +1,402 @@ +/** + * Copyright (c) 2007-2019, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file auto_mem.hh + */ + +#ifndef lnav_auto_mem_hh +#define lnav_auto_mem_hh + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "base/result.h" + +using free_func_t = void (*)(void*); + +/** + * Resource management class for memory allocated by a custom allocator. + * + * @param T The object type. + * @param auto_free The function to call to free the managed object. + */ +template +class auto_mem { +public: + static void noop_free(void*) {} + + static auto_mem leak(T* ptr) + { + auto_mem retval(noop_free); + + retval = ptr; + + return retval; + } + + explicit auto_mem(T* ptr = nullptr) + : am_ptr(ptr), am_free_func(default_free) + { + } + + auto_mem(const auto_mem& am) = delete; + + template + explicit auto_mem(F free_func) noexcept + : am_ptr(nullptr), am_free_func((free_func_t) free_func) + { + } + + auto_mem(auto_mem&& other) noexcept + : am_ptr(other.release()), am_free_func(other.am_free_func) + { + } + + ~auto_mem() { this->reset(); } + + bool empty() const { return this->am_ptr == nullptr; } + + operator T*() const { return this->am_ptr; } + + T* operator->() { return this->am_ptr; } + + auto_mem& operator=(T* ptr) + { + this->reset(ptr); + return *this; + } + + auto_mem& operator=(auto_mem&) = delete; + + auto_mem& operator=(auto_mem&& am) noexcept + { + this->reset(am.release()); + this->am_free_func = am.am_free_func; + return *this; + } + + T* release() + { + T* retval = this->am_ptr; + + this->am_ptr = nullptr; + return retval; + } + + T* in() const { return this->am_ptr; } + + T** out() + { + this->reset(); + return &this->am_ptr; + } + + template + F get_free_func() const + { + return (F) this->am_free_func; + } + + void reset(T* ptr = nullptr) + { + if (this->am_ptr != ptr) { + if (this->am_ptr != nullptr) { + this->am_free_func((void*) this->am_ptr); + } + this->am_ptr = ptr; + } + } + +private: + T* am_ptr; + void (*am_free_func)(void*); +}; + +template +class static_root_mem { +public: + static_root_mem() { memset(&this->srm_value, 0, sizeof(T)); } + + ~static_root_mem() { free_func(&this->srm_value); } + + const T* operator->() const { return &this->srm_value; } + + const T& in() const { return this->srm_value; } + + T* inout() + { + free_func(&this->srm_value); + memset(&this->srm_value, 0, sizeof(T)); + return &this->srm_value; + } + +private: + static_root_mem& operator=(T&) { return *this; } + + static_root_mem& operator=(static_root_mem&) { return *this; } + + T srm_value; +}; + +class auto_buffer { +public: + using value_type = char; + + static auto_buffer alloc(size_t capacity) + { + return auto_buffer{capacity == 0 ? nullptr : (char*) malloc(capacity), + capacity}; + } + + static auto_buffer alloc_bitmap(size_t capacity_in_bits) + { + return alloc((capacity_in_bits + 7) / 8); + } + + static auto_buffer from(const char* mem, size_t size) + { + auto retval = alloc(size); + + retval.resize(size); + memcpy(retval.in(), mem, size); + return retval; + } + + auto_buffer(const auto_buffer&) = delete; + + auto_buffer(auto_buffer&& other) noexcept + : ab_buffer(other.ab_buffer), ab_size(other.ab_size), + ab_capacity(other.ab_capacity) + { + other.ab_buffer = nullptr; + other.ab_size = 0; + other.ab_capacity = 0; + } + + ~auto_buffer() + { + free(this->ab_buffer); + this->ab_buffer = nullptr; + this->ab_size = 0; + this->ab_capacity = 0; + } + + auto_buffer& operator=(auto_buffer&) = delete; + + auto_buffer& operator=(auto_buffer&& other) noexcept + { + free(this->ab_buffer); + this->ab_buffer = std::exchange(other.ab_buffer, nullptr); + this->ab_size = std::exchange(other.ab_size, 0); + this->ab_capacity = std::exchange(other.ab_capacity, 0); + return *this; + } + + void swap(auto_buffer& other) + { + std::swap(this->ab_buffer, other.ab_buffer); + std::swap(this->ab_size, other.ab_size); + std::swap(this->ab_capacity, other.ab_capacity); + } + + char* in() { return this->ab_buffer; } + + char* at(size_t offset) { return &this->ab_buffer[offset]; } + + const char* at(size_t offset) const { return &this->ab_buffer[offset]; } + + char* begin() { return this->ab_buffer; } + + const char* begin() const { return this->ab_buffer; } + + auto_buffer& push_back(char ch) + { + if (this->ab_size == this->ab_capacity) { + this->expand_by(256); + } + this->ab_buffer[this->ab_size] = ch; + this->ab_size += 1; + + return *this; + } + + void pop_back() { this->ab_size -= 1; } + + bool is_bit_set(size_t bit_offset) const + { + size_t byte_offset = bit_offset / 8; + auto bitmask = 1UL << (bit_offset % 8); + + return this->ab_buffer[byte_offset] & bitmask; + } + + void set_bit(size_t bit_offset) + { + size_t byte_offset = bit_offset / 8; + auto bitmask = 1UL << (bit_offset % 8); + + this->ab_buffer[byte_offset] |= bitmask; + } + + void clear_bit(size_t bit_offset) + { + size_t byte_offset = bit_offset / 8; + auto bitmask = 1UL << (bit_offset % 8); + + this->ab_buffer[byte_offset] &= ~bitmask; + } + + std::reverse_iterator rbegin() + { + return std::reverse_iterator(this->end()); + } + + std::reverse_iterator rbegin() const + { + return std::reverse_iterator(this->end()); + } + + char* end() { return &this->ab_buffer[this->ab_size]; } + + const char* end() const { return &this->ab_buffer[this->ab_size]; } + + std::reverse_iterator rend() + { + return std::reverse_iterator(this->begin()); + } + + std::reverse_iterator rend() const + { + return std::reverse_iterator(this->begin()); + } + + std::pair release() + { + auto retval = std::make_pair(this->ab_buffer, this->ab_size); + + this->ab_buffer = nullptr; + this->ab_size = 0; + this->ab_capacity = 0; + return retval; + } + + size_t size() const { return this->ab_size; } + + size_t bitmap_size() const { return this->ab_size * 8; } + + bool empty() const { return this->ab_size == 0; } + + bool full() const { return this->ab_size == this->ab_capacity; } + + size_t capacity() const { return this->ab_capacity; } + + size_t available() const { return this->ab_capacity - this->ab_size; } + + void clear() { this->resize(0); } + + auto_buffer& resize(size_t new_size) + { + assert(new_size <= this->ab_capacity); + + this->ab_size = new_size; + return *this; + } + + auto_buffer& resize_bitmap(size_t new_size_in_bits, int fill = 0) + { + auto new_size = (new_size_in_bits + 7) / 8; + assert(new_size <= this->ab_capacity); + + auto old_size = std::exchange(this->ab_size, new_size); + memset(this->at(old_size), 0, this->ab_size - old_size); + return *this; + } + + auto_buffer& resize_by(ssize_t amount) + { + return this->resize(this->ab_size + amount); + } + + void expand_to(size_t new_capacity) + { + if (new_capacity <= this->ab_capacity) { + return; + } + auto* new_buffer = (char*) realloc(this->ab_buffer, new_capacity); + + if (new_buffer == nullptr) { + throw std::bad_alloc(); + } + + this->ab_buffer = new_buffer; + this->ab_capacity = new_capacity; + } + + void expand_bitmap_to(size_t new_capacity_in_bits) + { + this->expand_to((new_capacity_in_bits + 7) / 8); + } + + void expand_by(size_t amount) + { + if (amount == 0) { + return; + } + + this->expand_to(this->ab_capacity + amount); + } + + std::string to_string() const { return {this->ab_buffer, this->ab_size}; } + +private: + auto_buffer(char* buffer, size_t capacity) + : ab_buffer(buffer), ab_capacity(capacity) + { + } + + char* ab_buffer; + size_t ab_size{0}; + size_t ab_capacity; +}; + +struct text_auto_buffer { + auto_buffer inner; +}; + +struct blob_auto_buffer { + auto_buffer inner; +}; + +#endif diff --git a/src/base/auto_pid.cc b/src/base/auto_pid.cc new file mode 100644 index 0000000..a662988 --- /dev/null +++ b/src/base/auto_pid.cc @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "auto_pid.hh" + +#include + +#include "config.h" +#include "fmt/format.h" +#include "lnav_log.hh" + +namespace lnav { +namespace pid { + +bool in_child = false; + +Result, std::string> +from_fork() +{ + auto pid = ::fork(); + + if (pid == -1) { + return Err( + fmt::format(FMT_STRING("fork() failed: {}"), strerror(errno))); + } + + if (pid != 0) { + log_debug("started child: %d", pid); + } else { + in_child = true; + } + + return Ok(auto_pid(pid)); +} + +} // namespace pid +} // namespace lnav diff --git a/src/base/auto_pid.hh b/src/base/auto_pid.hh new file mode 100644 index 0000000..702af1e --- /dev/null +++ b/src/base/auto_pid.hh @@ -0,0 +1,175 @@ +/** + * Copyright (c) 2013, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file auto_pid.hh + */ + +#ifndef auto_pid_hh +#define auto_pid_hh + +#include +#include + +#include +#include + +#include "base/lnav_log.hh" +#include "base/result.h" +#include "mapbox/variant.hpp" + +enum class process_state { + running, + finished, +}; + +template +class auto_pid { +public: + explicit auto_pid(pid_t child, int status = 0) + : ap_status(status), ap_child(child) + { + } + + auto_pid(const auto_pid& other) = delete; + + auto_pid(auto_pid&& other) noexcept + : ap_status(other.ap_status), ap_child(std::move(other).release()) + { + } + + ~auto_pid() noexcept + { + this->reset(); + } + + auto_pid& operator=(auto_pid&& other) noexcept + { + auto other_status = other.ap_status; + this->reset(std::move(other).release()); + this->ap_status = other_status; + return *this; + } + + auto_pid& operator=(const auto_pid& other) = delete; + + pid_t in() const + { + return this->ap_child; + } + + bool in_child() const + { + static_assert(ProcState == process_state::running, + "this method is only available in the RUNNING state"); + return this->ap_child == 0; + } + + pid_t release() && + { + return std::exchange(this->ap_child, -1); } + + int status() const + { + static_assert(ProcState == process_state::finished, + "wait_for_child() must be called first"); + return this->ap_status; + } + + bool was_normal_exit() const + { + static_assert(ProcState == process_state::finished, + "wait_for_child() must be called first"); + return WIFEXITED(this->ap_status); + } + + int exit_status() const + { + static_assert(ProcState == process_state::finished, + "wait_for_child() must be called first"); + return WEXITSTATUS(this->ap_status); + } + + using poll_result + = mapbox::util::variant, + auto_pid>; + + poll_result poll() && + { + if (this->ap_child != -1) { + auto rc = waitpid(this->ap_child, &this->ap_status, WNOHANG); + + if (rc <= 0) { + return std::move(*this); + } + } + + return auto_pid( + std::exchange(this->ap_child, -1), this->ap_status); + } + + auto_pid wait_for_child(int options = 0) && + { + if (this->ap_child != -1) { + while ((waitpid(this->ap_child, &this->ap_status, options)) < 0 + && (errno == EINTR)) + { + ; + } + } + + return auto_pid( + std::exchange(this->ap_child, -1), this->ap_status); + } + + void reset(pid_t child = -1) noexcept + { + if (this->ap_child != child) { + this->ap_status = 0; + if (ProcState == process_state::running && this->ap_child != -1) { + log_debug("sending SIGTERM to child: %d", this->ap_child); + kill(this->ap_child, SIGTERM); + } + this->ap_child = child; + } + } + +private: + int ap_status{0}; + pid_t ap_child; +}; + +namespace lnav { +namespace pid { + +extern bool in_child; + +Result, std::string> from_fork(); +} // namespace pid +} // namespace lnav + +#endif diff --git a/src/base/bus.hh b/src/base/bus.hh new file mode 100644 index 0000000..dea23ac --- /dev/null +++ b/src/base/bus.hh @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_bus_hh +#define lnav_bus_hh + +#include + +#include "lnav_log.hh" + +template +class bus { +public: + bus() = default; + + virtual ~bus() { require(this->b_components.empty()); } + + bus(const bus&) = delete; + + void attach(T* component) + { + this->b_components.template emplace_back(component); + } + + void detach(T* component) + { + auto iter = this->b_components.begin(); + for (; iter != this->b_components.end(); ++iter) { + if (*iter == component) { + break; + } + } + require(iter != this->b_components.end()); + + this->b_components.erase(iter); + } + +protected: + std::vector b_components; +}; + +#endif diff --git a/src/base/date_time_scanner.cc b/src/base/date_time_scanner.cc new file mode 100644 index 0000000..72b7e5d --- /dev/null +++ b/src/base/date_time_scanner.cc @@ -0,0 +1,308 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file date_time_scanner.cc + */ + +#include + +#include "date_time_scanner.hh" + +#include "config.h" +#include "ptimec.hh" +#include "scn/scn.h" + +size_t +date_time_scanner::ftime(char* dst, + size_t len, + const char* const time_fmt[], + const exttm& tm) const +{ + off_t off = 0; + + if (time_fmt == nullptr) { + PTIMEC_FORMATS[this->dts_fmt_lock].pf_ffunc(dst, off, len, tm); + if (tm.et_flags & ETF_MILLIS_SET) { + dst[off++] = '.'; + ftime_L(dst, off, len, tm); + } else if (tm.et_flags & ETF_MICROS_SET) { + dst[off++] = '.'; + ftime_f(dst, off, len, tm); + } else if (tm.et_flags & ETF_NANOS_SET) { + dst[off++] = '.'; + ftime_N(dst, off, len, tm); + } + dst[off] = '\0'; + } else { + off = ftime_fmt(dst, len, time_fmt[this->dts_fmt_lock], tm); + } + + return (size_t) off; +} + +bool +next_format(const char* const fmt[], int& index, int& locked_index) +{ + bool retval = true; + + if (locked_index == -1) { + index += 1; + if (fmt[index] == nullptr) { + retval = false; + } + } else if (index == locked_index) { + retval = false; + } else { + index = locked_index; + } + + return retval; +} + +const char* +date_time_scanner::scan(const char* time_dest, + size_t time_len, + const char* const time_fmt[], + struct exttm* tm_out, + struct timeval& tv_out, + bool convert_local) +{ + int curr_time_fmt = -1; + bool found = false; + const char* retval = nullptr; + + if (!time_fmt) { + time_fmt = PTIMEC_FORMAT_STR; + } + + while (next_format(time_fmt, curr_time_fmt, this->dts_fmt_lock)) { + *tm_out = this->dts_base_tm; + tm_out->et_flags = 0; + if (time_len > 1 && time_dest[0] == '+' && isdigit(time_dest[1])) { + retval = nullptr; + auto epoch_scan_res = scn::scan_value( + scn::string_view{time_dest, time_len}); + if (epoch_scan_res) { + time_t gmt = epoch_scan_res.value(); + + if (convert_local && this->dts_local_time) { + localtime_r(&gmt, &tm_out->et_tm); +#ifdef HAVE_STRUCT_TM_TM_ZONE + tm_out->et_tm.tm_zone = nullptr; +#endif + tm_out->et_tm.tm_isdst = 0; + gmt = tm2sec(&tm_out->et_tm); + } + tv_out.tv_sec = gmt; + tv_out.tv_usec = 0; + tm_out->et_flags = ETF_DAY_SET | ETF_MONTH_SET | ETF_YEAR_SET + | ETF_MACHINE_ORIENTED | ETF_EPOCH_TIME; + + this->dts_fmt_lock = curr_time_fmt; + this->dts_fmt_len = std::distance(epoch_scan_res.begin(), + epoch_scan_res.end()); + retval = time_dest + this->dts_fmt_len; + found = true; + break; + } + } else if (time_fmt == PTIMEC_FORMAT_STR) { + ptime_func func = PTIMEC_FORMATS[curr_time_fmt].pf_func; + off_t off = 0; + +#ifdef HAVE_STRUCT_TM_TM_ZONE + if (!this->dts_keep_base_tz) { + tm_out->et_tm.tm_zone = nullptr; + } +#endif + if (func(tm_out, time_dest, off, time_len)) { + retval = &time_dest[off]; + + if (tm_out->et_tm.tm_year < 70) { + tm_out->et_tm.tm_year = 80; + } + if (convert_local + && (this->dts_local_time + || tm_out->et_flags & ETF_EPOCH_TIME)) + { + time_t gmt = tm2sec(&tm_out->et_tm); + + this->to_localtime(gmt, *tm_out); + } + const auto& last_tm = this->dts_last_tm.et_tm; + if (last_tm.tm_year == tm_out->et_tm.tm_year + && last_tm.tm_mon == tm_out->et_tm.tm_mon + && last_tm.tm_mday == tm_out->et_tm.tm_mday + && last_tm.tm_hour == tm_out->et_tm.tm_hour + && last_tm.tm_min == tm_out->et_tm.tm_min) + { + const auto sec_diff = tm_out->et_tm.tm_sec - last_tm.tm_sec; + + // log_debug("diff %d", sec_diff); + tv_out = this->dts_last_tv; + tv_out.tv_sec += sec_diff; + tm_out->et_tm.tm_wday = last_tm.tm_wday; + } else { + // log_debug("doing tm2sec"); + tv_out.tv_sec = tm2sec(&tm_out->et_tm); + secs2wday(tv_out, &tm_out->et_tm); + } + tv_out.tv_usec = tm_out->et_nsec / 1000; + + this->dts_fmt_lock = curr_time_fmt; + this->dts_fmt_len = retval - time_dest; + + found = true; + break; + } + } else { + off_t off = 0; + +#ifdef HAVE_STRUCT_TM_TM_ZONE + if (!this->dts_keep_base_tz) { + tm_out->et_tm.tm_zone = nullptr; + } +#endif + if (ptime_fmt( + time_fmt[curr_time_fmt], tm_out, time_dest, off, time_len) + && (time_dest[off] == '.' || time_dest[off] == ',' + || off == (off_t) time_len)) + { + retval = &time_dest[off]; + if (tm_out->et_tm.tm_year < 70) { + tm_out->et_tm.tm_year = 80; + } + if (convert_local + && (this->dts_local_time + || tm_out->et_flags & ETF_EPOCH_TIME)) + { + time_t gmt = tm2sec(&tm_out->et_tm); + + this->to_localtime(gmt, *tm_out); +#ifdef HAVE_STRUCT_TM_TM_ZONE + tm_out->et_tm.tm_zone = nullptr; +#endif + tm_out->et_tm.tm_isdst = 0; + } + + tv_out.tv_sec = tm2sec(&tm_out->et_tm); + tv_out.tv_usec = tm_out->et_nsec / 1000; + secs2wday(tv_out, &tm_out->et_tm); + + this->dts_fmt_lock = curr_time_fmt; + this->dts_fmt_len = retval - time_dest; + + found = true; + break; + } + } + } + + if (!found) { + retval = nullptr; + } + + if (retval != nullptr) { + this->dts_last_tm = *tm_out; + this->dts_last_tv = tv_out; + } + + if (retval != nullptr && static_cast(retval - time_dest) < time_len) + { + /* Try to pull out the milli/micro-second value. */ + if (retval[0] == '.' || retval[0] == ',') { + off_t off = (retval - time_dest) + 1; + + if (ptime_N(tm_out, time_dest, off, time_len)) { + tv_out.tv_usec + = std::chrono::duration_cast( + std::chrono::nanoseconds{tm_out->et_nsec}) + .count(); + this->dts_fmt_len += 10; + tm_out->et_flags |= ETF_NANOS_SET; + retval += 10; + } else if (ptime_f(tm_out, time_dest, off, time_len)) { + tv_out.tv_usec + = std::chrono::duration_cast( + std::chrono::nanoseconds{tm_out->et_nsec}) + .count(); + this->dts_fmt_len += 7; + tm_out->et_flags |= ETF_MICROS_SET; + retval += 7; + } else if (ptime_L(tm_out, time_dest, off, time_len)) { + tv_out.tv_usec + = std::chrono::duration_cast( + std::chrono::nanoseconds{tm_out->et_nsec}) + .count(); + this->dts_fmt_len += 4; + tm_out->et_flags |= ETF_MILLIS_SET; + retval += 4; + } + } + } + + return retval; +} + +void +date_time_scanner::set_base_time(time_t base_time, const tm& local_tm) +{ + this->dts_base_time = base_time; + this->dts_base_tm.et_tm = local_tm; + this->dts_last_tm = exttm{}; + this->dts_last_tv = timeval{}; +} + +void +date_time_scanner::to_localtime(time_t t, exttm& tm_out) +{ + if (t < (24 * 60 * 60)) { + // Don't convert and risk going past the epoch. + return; + } + + if (t < this->dts_local_offset_valid || t >= this->dts_local_offset_expiry) + { + time_t new_gmt; + + localtime_r(&t, &tm_out.et_tm); +#ifdef HAVE_STRUCT_TM_TM_ZONE + tm_out.et_tm.tm_zone = nullptr; +#endif + tm_out.et_tm.tm_isdst = 0; + + new_gmt = tm2sec(&tm_out.et_tm); + this->dts_local_offset_cache = t - new_gmt; + this->dts_local_offset_valid = t; + this->dts_local_offset_expiry = t + (EXPIRE_TIME - 1); + this->dts_local_offset_expiry + -= this->dts_local_offset_expiry % EXPIRE_TIME; + } else { + time_t adjust_gmt = t - this->dts_local_offset_cache; + gmtime_r(&adjust_gmt, &tm_out.et_tm); + } +} diff --git a/src/base/date_time_scanner.hh b/src/base/date_time_scanner.hh new file mode 100644 index 0000000..90eaffa --- /dev/null +++ b/src/base/date_time_scanner.hh @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file date_time_scanner.hh + */ + +#ifndef lnav_date_time_scanner_hh +#define lnav_date_time_scanner_hh + +#include +#include + +#include + +#include "time_util.hh" + +/** + * Scans a timestamp string to discover the date-time format using the custom + * ptimec parser. Once a format is found, it is locked in so that the next + * time a timestamp needs to be scanned, the format does not have to be + * rediscovered. The discovered date-time format can also be used to convert + * an exttm struct to a string using the ftime() method. + */ +struct date_time_scanner { + date_time_scanner() { this->clear(); } + + void clear() + { + this->dts_base_time = 0; + this->dts_base_tm = exttm{}; + this->dts_fmt_lock = -1; + this->dts_fmt_len = -1; + this->dts_last_tv = timeval{}; + this->dts_last_tm = exttm{}; + } + + /** + * Unlock this scanner so that the format is rediscovered. + */ + void unlock() + { + this->dts_fmt_lock = -1; + this->dts_fmt_len = -1; + } + + void set_base_time(time_t base_time, const tm& local_tm); + + /** + * Convert a timestamp to local time. + * + * Calling localtime_r is slow since it wants to lookup the timezone on + * every call, so we cache the result and only call it again if the + * requested time falls outside of a fifteen minute range. + */ + void to_localtime(time_t t, struct exttm& tm_out); + + bool dts_keep_base_tz{false}; + bool dts_local_time{false}; + time_t dts_base_time{0}; + struct exttm dts_base_tm; + int dts_fmt_lock{-1}; + int dts_fmt_len{-1}; + struct exttm dts_last_tm {}; + struct timeval dts_last_tv {}; + time_t dts_local_offset_cache{0}; + time_t dts_local_offset_valid{0}; + time_t dts_local_offset_expiry{0}; + + static const int EXPIRE_TIME = 15 * 60; + + const char* scan(const char* time_src, + size_t time_len, + const char* const time_fmt[], + struct exttm* tm_out, + struct timeval& tv_out, + bool convert_local = true); + + size_t ftime(char* dst, + size_t len, + const char* const time_fmt[], + const struct exttm& tm) const; + + bool convert_to_timeval(const char* time_src, + ssize_t time_len, + const char* const time_fmt[], + struct timeval& tv_out) + { + struct exttm tm; + + if (time_len == -1) { + time_len = strlen(time_src); + } + if (this->scan(time_src, time_len, time_fmt, &tm, tv_out) != nullptr) { + return true; + } + return false; + } + + bool convert_to_timeval(const std::string& time_src, struct timeval& tv_out) + { + struct exttm tm; + + if (this->scan(time_src.c_str(), time_src.size(), nullptr, &tm, tv_out) + != nullptr) + { + return true; + } + return false; + } +}; + +#endif diff --git a/src/base/enum_util.hh b/src/base/enum_util.hh new file mode 100644 index 0000000..437292d --- /dev/null +++ b/src/base/enum_util.hh @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2019, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_enum_util_hh +#define lnav_enum_util_hh + +#include + +namespace lnav { +namespace enums { + +template +constexpr auto +to_underlying(E e) noexcept +{ + return static_cast>(e); +} + +} // namespace enums +} // namespace lnav + +#endif diff --git a/src/base/file_range.hh b/src/base/file_range.hh new file mode 100644 index 0000000..d9a3de4 --- /dev/null +++ b/src/base/file_range.hh @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2019, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_file_range_hh +#define lnav_file_range_hh + +#include + +#include "intern_string.hh" + +using file_off_t = int64_t; +using file_size_t = uint64_t; +using file_ssize_t = int64_t; + +class file_range { +public: + struct metadata { + bool m_valid_utf{true}; + bool m_has_ansi{false}; + }; + + file_off_t fr_offset{0}; + file_ssize_t fr_size{0}; + metadata fr_metadata; + + void clear() + { + this->fr_offset = 0; + this->fr_size = 0; + } + + ssize_t next_offset() const { return this->fr_offset + this->fr_size; } + + bool empty() const { return this->fr_size == 0; } +}; + +struct source_location { + source_location() + : sl_source(intern_string::lookup("unknown")), sl_line_number(0) + { + } + + explicit source_location(intern_string_t source, int32_t line = 0) + : sl_source(source), sl_line_number(line) + { + } + + intern_string_t sl_source; + int32_t sl_line_number; +}; + +#endif diff --git a/src/base/fs_util.cc b/src/base/fs_util.cc new file mode 100644 index 0000000..f72aaa6 --- /dev/null +++ b/src/base/fs_util.cc @@ -0,0 +1,183 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "fs_util.hh" + +#include "config.h" +#include "fmt/format.h" +#include "itertools.hh" +#include "opt_util.hh" + +namespace lnav { +namespace filesystem { + +Result +create_file(const ghc::filesystem::path& path, int flags, mode_t mode) +{ + auto fd = openp(path, flags | O_CREAT, mode); + + if (fd == -1) { + return Err(fmt::format(FMT_STRING("Failed to open: {} -- {}"), + path.string(), + strerror(errno))); + } + + return Ok(auto_fd(fd)); +} + +Result +open_file(const ghc::filesystem::path& path, int flags) +{ + auto fd = openp(path, flags); + + if (fd == -1) { + return Err(fmt::format(FMT_STRING("Failed to open: {} -- {}"), + path.string(), + strerror(errno))); + } + + return Ok(auto_fd(fd)); +} + +Result, std::string> +open_temp_file(const ghc::filesystem::path& pattern) +{ + auto pattern_str = pattern.string(); + char pattern_copy[pattern_str.size() + 1]; + int fd; + + strcpy(pattern_copy, pattern_str.c_str()); + if ((fd = mkstemp(pattern_copy)) == -1) { + return Err( + fmt::format(FMT_STRING("unable to create temporary file: {} -- {}"), + pattern.string(), + strerror(errno))); + } + + return Ok(std::make_pair(ghc::filesystem::path(pattern_copy), auto_fd(fd))); +} + +Result +read_file(const ghc::filesystem::path& path) +{ + try { + ghc::filesystem::ifstream file_stream(path); + + if (!file_stream) { + return Err(std::string(strerror(errno))); + } + + std::string retval; + retval.assign((std::istreambuf_iterator(file_stream)), + std::istreambuf_iterator()); + return Ok(retval); + } catch (const std::exception& e) { + return Err(std::string(e.what())); + } +} + +Result +write_file(const ghc::filesystem::path& path, const string_fragment& content) +{ + auto tmp_pattern = path; + tmp_pattern += ".XXXXXX"; + + auto tmp_pair = TRY(open_temp_file(tmp_pattern)); + auto bytes_written + = write(tmp_pair.second.get(), content.data(), content.length()); + if (bytes_written < 0) { + return Err( + fmt::format(FMT_STRING("unable to write to temporary file {}: {}"), + tmp_pair.first.string(), + strerror(errno))); + } + if (bytes_written != content.length()) { + return Err(fmt::format(FMT_STRING("short write to file {}: {} < {}"), + tmp_pair.first.string(), + bytes_written, + content.length())); + } + std::error_code ec; + ghc::filesystem::rename(tmp_pair.first, path, ec); + if (ec) { + return Err( + fmt::format(FMT_STRING("unable to move temporary file {}: {}"), + tmp_pair.first.string(), + ec.message())); + } + + return Ok(); +} + +std::string +build_path(const std::vector& paths) +{ + return paths + | lnav::itertools::map([](const auto& path) { return path.string(); }) + | lnav::itertools::append(getenv_opt("PATH").value_or("")) + | lnav::itertools::filter_out(&std::string::empty) + | lnav::itertools::fold( + [](const auto& elem, auto& accum) { + if (!accum.empty()) { + accum.push_back(':'); + } + return accum.append(elem); + }, + std::string()); +} + +Result +stat_file(const ghc::filesystem::path& path) +{ + struct stat retval; + + if (statp(path, &retval) == 0) { + return Ok(retval); + } + + return Err(fmt::format(FMT_STRING("failed to find file: {} -- {}"), + path.string(), + strerror(errno))); +} + +file_lock::file_lock(const ghc::filesystem::path& archive_path) +{ + auto lock_path = archive_path; + + lock_path += ".lck"; + auto open_res + = lnav::filesystem::create_file(lock_path, O_RDWR | O_CLOEXEC, 0600); + if (open_res.isErr()) { + throw std::runtime_error(open_res.unwrapErr()); + } + this->lh_fd = open_res.unwrap(); +} + +} // namespace filesystem +} // namespace lnav diff --git a/src/base/fs_util.hh b/src/base/fs_util.hh new file mode 100644 index 0000000..b9253ff --- /dev/null +++ b/src/base/fs_util.hh @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_fs_util_hh +#define lnav_fs_util_hh + +#include +#include + +#include "auto_fd.hh" +#include "ghc/filesystem.hpp" +#include "intern_string.hh" +#include "result.h" + +namespace lnav { +namespace filesystem { + +inline int +statp(const ghc::filesystem::path& path, struct stat* buf) +{ + return stat(path.c_str(), buf); +} + +inline int +openp(const ghc::filesystem::path& path, int flags) +{ + return open(path.c_str(), flags); +} + +inline int +openp(const ghc::filesystem::path& path, int flags, mode_t mode) +{ + return open(path.c_str(), flags, mode); +} + +Result create_file(const ghc::filesystem::path& path, + int flags, + mode_t mode); + +Result open_file(const ghc::filesystem::path& path, + int flags); + +Result stat_file(const ghc::filesystem::path& path); + +Result, std::string> open_temp_file( + const ghc::filesystem::path& pattern); + +Result read_file(const ghc::filesystem::path& path); + +Result write_file(const ghc::filesystem::path& path, + const string_fragment& content); + +std::string build_path(const std::vector& paths); + +class file_lock { +public: + class guard { + public: + explicit guard(file_lock* arc_lock) : g_lock(arc_lock) + { + this->g_lock->lock(); + } + + guard(guard&& other) noexcept + : g_lock(std::exchange(other.g_lock, nullptr)) + { + } + + ~guard() + { + if (this->g_lock != nullptr) { + this->g_lock->unlock(); + } + } + + guard(const guard&) = delete; + guard& operator=(const guard&) = delete; + guard& operator=(guard&&) = delete; + + private: + file_lock* g_lock; + }; + + void lock() const { lockf(this->lh_fd, F_LOCK, 0); } + + void unlock() const { lockf(this->lh_fd, F_ULOCK, 0); } + + explicit file_lock(const ghc::filesystem::path& archive_path); + + auto_fd lh_fd; +}; + +} // namespace filesystem +} // namespace lnav + +#endif diff --git a/src/base/fs_util.tests.cc b/src/base/fs_util.tests.cc new file mode 100644 index 0000000..9ed3377 --- /dev/null +++ b/src/base/fs_util.tests.cc @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "base/fs_util.hh" + +#include "config.h" +#include "doctest/doctest.h" + +TEST_CASE("fs_util::build_path") +{ + auto* old_path = getenv("PATH"); + unsetenv("PATH"); + + CHECK("" == lnav::filesystem::build_path({})); + + CHECK("/bin:/usr/bin" + == lnav::filesystem::build_path({"", "/bin", "/usr/bin", ""})); + setenv("PATH", "/usr/local/bin", 1); + CHECK("/bin:/usr/bin:/usr/local/bin" + == lnav::filesystem::build_path({"", "/bin", "/usr/bin", ""})); + setenv("PATH", "/usr/local/bin:/opt/bin", 1); + CHECK("/usr/local/bin:/opt/bin" == lnav::filesystem::build_path({})); + CHECK("/bin:/usr/bin:/usr/local/bin:/opt/bin" + == lnav::filesystem::build_path({"", "/bin", "/usr/bin", ""})); + if (old_path != nullptr) { + setenv("PATH", old_path, 1); + } +} diff --git a/src/base/func_util.hh b/src/base/func_util.hh new file mode 100644 index 0000000..01a2328 --- /dev/null +++ b/src/base/func_util.hh @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_func_util_hh +#define lnav_func_util_hh + +#include +#include + +template +decltype(auto) +bind_mem(F&& f, FrontArg&& frontArg) +{ + return [f = std::forward(f), + frontArg = std::forward(frontArg)](auto&&... backArgs) { + return (frontArg->*f)(std::forward(backArgs)...); + }; +} + +struct noop_func { + struct anything { + template + operator T() + { + return {}; + } + // optional reference support. Somewhat evil. + template + operator T&() const + { + static T t{}; + return t; + } + }; + template + anything operator()(Args&&...) const + { + return {}; + } +}; + +namespace lnav { +namespace func { + +class scoped_cb { +public: + class guard { + public: + explicit guard(scoped_cb* owner) : g_owner(owner) {} + + guard(const guard&) = delete; + guard& operator=(const guard&) = delete; + + guard(guard&& gu) noexcept : g_owner(std::exchange(gu.g_owner, nullptr)) + { + } + + guard& operator=(guard&& gu) noexcept + { + this->g_owner = std::exchange(gu.g_owner, nullptr); + return *this; + } + + ~guard() + { + if (this->g_owner != nullptr) { + this->g_owner->s_callback = {}; + } + } + + private: + scoped_cb* g_owner; + }; + + guard install(std::function cb) + { + this->s_callback = std::move(cb); + + return guard{this}; + } + + void operator()() + { + if (s_callback) { + s_callback(); + } + } + +private: + std::function s_callback; +}; + +template>{}, int> = 0> +constexpr decltype(auto) +invoke(Fn&& f, Args&&... args) noexcept( + noexcept(std::mem_fn(f)(std::forward(args)...))) +{ + return std::mem_fn(f)(std::forward(args)...); +} + +template>{}, int> = 0> +constexpr decltype(auto) +invoke(Fn&& f, Args&&... args) noexcept( + noexcept(std::forward(f)(std::forward(args)...))) +{ + return std::forward(f)(std::forward(args)...); +} + +template +struct is_invocable { + template + static auto test(U&& p) + -> decltype((std::declval().*p)(std::declval()...), + void(), + std::true_type()); + template + static auto test(U* p) -> decltype((*p)(std::declval()...), + void(), + std::true_type()); + template + static auto test(...) -> decltype(std::false_type()); + + static constexpr bool value = decltype(test(0))::value; +}; + +} // namespace func +} // namespace lnav + +#endif diff --git a/src/base/future_util.hh b/src/base/future_util.hh new file mode 100644 index 0000000..8797faa --- /dev/null +++ b/src/base/future_util.hh @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_future_util_hh +#define lnav_future_util_hh + +#include +#include + +namespace lnav { +namespace futures { + +/** + * Create a future that is ready to immediately return a result. + * + * @tparam T The result type of the future. + * @param t The value the future should return. + * @return The new future. + */ +template +std::future> +make_ready_future(T&& t) +{ + std::promise> pr; + auto r = pr.get_future(); + pr.set_value(std::forward(t)); + return r; +} + +/** + * A queue used to limit the number of futures that are running concurrently. + * + * @tparam T The result of the futures. + * @tparam MAX_QUEUE_SIZE The maximum number of futures that can be in flight. + */ +template +class future_queue { +public: + /** + * @param processor The function to execute with the result of a future. + */ + explicit future_queue(std::function processor) + : fq_processor(processor){}; + + ~future_queue() + { + this->pop_to(); + } + + /** + * Add a future to the queue. If the size of the queue is greater than the + * MAX_QUEUE_SIZE, this call will block waiting for the first queued + * future to return a result. + * + * @param f The future to add to the queue. + */ + void push_back(std::future&& f) + { + this->fq_deque.emplace_back(std::move(f)); + this->pop_to(MAX_QUEUE_SIZE); + } + + /** + * Removes the next future from the queue, waits for the result, and then + * repeats until the queue reaches the given size. + * + * @param size The new desired size of the queue. + */ + void pop_to(size_t size = 0) + { + while (this->fq_deque.size() > size) { + auto v = this->fq_deque.front().get(); + this->fq_processor(v); + this->fq_deque.pop_front(); + } + } + + std::function fq_processor; + std::deque> fq_deque; +}; + +} // namespace futures +} // namespace lnav + +#endif diff --git a/src/base/humanize.cc b/src/base/humanize.cc new file mode 100644 index 0000000..5e96bf3 --- /dev/null +++ b/src/base/humanize.cc @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2019, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include "humanize.hh" + +#include "config.h" +#include "fmt/format.h" + +namespace humanize { + +std::string +file_size(file_ssize_t value, alignment align) +{ + static const double LN1024 = log(1024.0); + static const std::vector UNITS = { + " ", + "K", + "M", + "G", + "T", + "P", + "E", + }; + + if (value < 0) { + return "Unknown"; + } + + if (value == 0) { + switch (align) { + case alignment::none: + return "0B"; + case alignment::columnar: + return "0.0 B"; + } + } + + auto exp + = floor(std::min(log(value) / LN1024, (double) (UNITS.size() - 1))); + auto divisor = pow(1024, exp); + + if (align == alignment::none && divisor <= 1) { + return fmt::format(FMT_STRING("{}B"), value, UNITS[exp]); + } + return fmt::format(FMT_STRING("{:.1f}{}B"), + divisor == 0 ? value : value / divisor, + UNITS[exp]); +} + +const std::string& +sparkline(double value, nonstd::optional upper_opt) +{ + static const std::string ZERO = " "; + static const std::string BARS[] = { + "\u2581", + "\u2582", + "\u2583", + "\u2584", + "\u2585", + "\u2586", + "\u2587", + "\u2588", + }; + static const double BARS_COUNT = std::distance(begin(BARS), end(BARS)); + + if (value <= 0.0) { + return ZERO; + } + + auto upper = upper_opt.value_or(100.0); + + if (value >= upper) { + return BARS[(size_t) BARS_COUNT - 1]; + } + + size_t index = ceil((value / upper) * BARS_COUNT) - 1; + + return BARS[index]; +} + +} // namespace humanize diff --git a/src/base/humanize.file_size.tests.cc b/src/base/humanize.file_size.tests.cc new file mode 100644 index 0000000..ff70c7e --- /dev/null +++ b/src/base/humanize.file_size.tests.cc @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "base/humanize.hh" + +#include "config.h" +#include "doctest/doctest.h" + +TEST_CASE("humanize::file_size") +{ + CHECK(humanize::file_size(0, humanize::alignment::columnar) == "0.0 B"); + CHECK(humanize::file_size(1, humanize::alignment::columnar) == "1.0 B"); + CHECK(humanize::file_size(1024, humanize::alignment::columnar) == "1.0KB"); + CHECK(humanize::file_size(1500, humanize::alignment::columnar) == "1.5KB"); + CHECK(humanize::file_size(55LL * 784LL * 1024LL * 1024LL, + humanize::alignment::columnar) + == "42.1GB"); + CHECK(humanize::file_size(-1LL, humanize::alignment::columnar) + == "Unknown"); + CHECK(humanize::file_size(std::numeric_limits::max(), + humanize::alignment::columnar) + == "8.0EB"); +} diff --git a/src/base/humanize.hh b/src/base/humanize.hh new file mode 100644 index 0000000..3f9f66c --- /dev/null +++ b/src/base/humanize.hh @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_humanize_hh +#define lnav_humanize_hh + +#include + +#include + +#include "file_range.hh" + +namespace humanize { + +enum class alignment { + none, + columnar, +}; + +/** + * Format the given size as a human-friendly string. + * + * @param value The value to format. + * @return The formatted string. + */ +std::string file_size(file_ssize_t value, alignment align); + +const std::string& sparkline(double value, nonstd::optional upper); + +} // namespace humanize + +#endif diff --git a/src/base/humanize.network.cc b/src/base/humanize.network.cc new file mode 100644 index 0000000..2bf390d --- /dev/null +++ b/src/base/humanize.network.cc @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "humanize.network.hh" + +#include "config.h" +#include "pcrepp/pcre2pp.hh" + +namespace humanize { +namespace network { +namespace path { + +nonstd::optional<::network::path> +from_str(string_fragment sf) +{ + static const auto REMOTE_PATTERN = lnav::pcre2pp::code::from_const( + "^(?:(?[\\w\\._\\-]+)@)?" + "(?:\\[(?[^\\]]+)\\]|(?[^\\[/:]+)):" + "(?.*)$"); + static thread_local auto REMOTE_MATCH_DATA + = REMOTE_PATTERN.create_match_data(); + + auto match_res = REMOTE_PATTERN.capture_from(sf) + .into(REMOTE_MATCH_DATA) + .matches() + .ignore_error(); + + if (!match_res) { + return nonstd::nullopt; + } + + const auto username = REMOTE_MATCH_DATA["username"].map( + [](auto sf) { return sf.to_string(); }); + const auto ipv6 = REMOTE_MATCH_DATA["ipv6"]; + const auto hostname = REMOTE_MATCH_DATA["hostname"]; + const auto locality_hostname = ipv6 ? ipv6.value() : hostname.value(); + auto path = *REMOTE_MATCH_DATA["path"]; + + if (path.empty()) { + path = string_fragment::from_const("."); + } + return ::network::path{ + {username, locality_hostname.to_string(), nonstd::nullopt}, + path.to_string(), + }; +} + +} // namespace path +} // namespace network +} // namespace humanize diff --git a/src/base/humanize.network.hh b/src/base/humanize.network.hh new file mode 100644 index 0000000..609f57d --- /dev/null +++ b/src/base/humanize.network.hh @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_humanize_network_hh +#define lnav_humanize_network_hh + +#include + +#include "fmt/format.h" +#include "intern_string.hh" +#include "network.tcp.hh" +#include "optional.hpp" + +namespace fmt { + +template<> +struct formatter { + constexpr auto parse(format_parse_context& ctx) + { + const auto it = ctx.begin(); + const auto end = ctx.end(); + + // Check if reached the end of the range: + if (it != end && *it != '}') { + throw format_error("invalid format"); + } + + // Return an iterator past the end of the parsed range: + return it; + } + + template + auto format(const network::locality& l, FormatContext& ctx) + { + bool is_ipv6 = l.l_hostname.find(':') != std::string::npos; + + return format_to(ctx.out(), + "{}{}{}{}{}", + l.l_username.value_or(std::string()), + l.l_username ? "@" : "", + is_ipv6 ? "[" : "", + l.l_hostname, + is_ipv6 ? "]" : ""); + } +}; + +template<> +struct formatter { + constexpr auto parse(format_parse_context& ctx) + { + const auto it = ctx.begin(); + const auto end = ctx.end(); + + // Check if reached the end of the range: + if (it != end && *it != '}') { + throw format_error("invalid format"); + } + + // Return an iterator past the end of the parsed range: + return it; + } + + template + auto format(const network::path& p, FormatContext& ctx) + { + return format_to( + ctx.out(), "{}:{}", p.p_locality, p.p_path == "." ? "" : p.p_path); + } +}; + +} // namespace fmt + +namespace humanize { +namespace network { +namespace path { + +nonstd::optional<::network::path> from_str(string_fragment sf); + +} // namespace path +} // namespace network +} // namespace humanize + +#endif diff --git a/src/base/humanize.network.tests.cc b/src/base/humanize.network.tests.cc new file mode 100644 index 0000000..fe2e9d2 --- /dev/null +++ b/src/base/humanize.network.tests.cc @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "base/humanize.network.hh" +#include "config.h" +#include "doctest/doctest.h" + +TEST_CASE("humanize::network::path") +{ + { + auto rp_opt = humanize::network::path::from_str( + string_fragment::from_const("foobar")); + CHECK(!rp_opt); + } + { + auto rp_opt = humanize::network::path::from_str( + string_fragment::from_const("dean@foobar/bar")); + CHECK(!rp_opt); + } + + { + auto rp_opt = humanize::network::path::from_str( + string_fragment::from_const("dean@host1.example.com:/var/log")); + CHECK(rp_opt.has_value()); + + auto rp = *rp_opt; + CHECK(rp.p_locality.l_username.has_value()); + CHECK(rp.p_locality.l_username.value() == "dean"); + CHECK(rp.p_locality.l_hostname == "host1.example.com"); + CHECK(!rp.p_locality.l_service.has_value()); + CHECK(rp.p_path == "/var/log"); + } + + { + auto rp_opt + = humanize::network::path::from_str(string_fragment::from_const( + "dean@[fe80::184f:c67:baf1:fe02%en0]:/var/log")); + CHECK(rp_opt.has_value()); + + auto rp = *rp_opt; + CHECK(rp.p_locality.l_username.has_value()); + CHECK(rp.p_locality.l_username.value() == "dean"); + CHECK(rp.p_locality.l_hostname == "fe80::184f:c67:baf1:fe02%en0"); + CHECK(!rp.p_locality.l_service.has_value()); + CHECK(rp.p_path == "/var/log"); + + CHECK(fmt::format("{}", rp.p_locality) + == "dean@[fe80::184f:c67:baf1:fe02%en0]"); + } + + { + auto rp_opt + = humanize::network::path::from_str(string_fragment::from_const( + "[fe80::184f:c67:baf1:fe02%en0]:/var/log")); + CHECK(rp_opt.has_value()); + + auto rp = *rp_opt; + CHECK(!rp.p_locality.l_username.has_value()); + CHECK(rp.p_locality.l_hostname == "fe80::184f:c67:baf1:fe02%en0"); + CHECK(!rp.p_locality.l_service.has_value()); + CHECK(rp.p_path == "/var/log"); + + CHECK(fmt::format("{}", rp.p_locality) + == "[fe80::184f:c67:baf1:fe02%en0]"); + } + + { + auto rp_opt = humanize::network::path::from_str( + string_fragment::from_const("host1.example.com:/var/log")); + CHECK(rp_opt.has_value()); + + auto rp = *rp_opt; + CHECK(!rp.p_locality.l_username.has_value()); + CHECK(rp.p_locality.l_hostname == "host1.example.com"); + CHECK(!rp.p_locality.l_service.has_value()); + CHECK(rp.p_path == "/var/log"); + } + + { + auto rp_opt = humanize::network::path::from_str( + string_fragment::from_const("host1.example.com:")); + CHECK(rp_opt.has_value()); + + auto rp = *rp_opt; + CHECK(!rp.p_locality.l_username.has_value()); + CHECK(rp.p_locality.l_hostname == "host1.example.com"); + CHECK(!rp.p_locality.l_service.has_value()); + CHECK(rp.p_path == "."); + } +} diff --git a/src/base/humanize.time.cc b/src/base/humanize.time.cc new file mode 100644 index 0000000..68cfe0f --- /dev/null +++ b/src/base/humanize.time.cc @@ -0,0 +1,208 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "humanize.time.hh" + +#include "config.h" +#include "fmt/format.h" +#include "time_util.hh" + +namespace humanize { +namespace time { + +using namespace std::chrono_literals; + +point +point::from_tv(const timeval& tv) +{ + return point(tv); +} + +std::string +point::as_time_ago() const +{ + struct timeval current_time + = this->p_recent_point.value_or(current_timeval()); + + if (this->p_convert_to_local) { + current_time.tv_sec = convert_log_time_to_local(current_time.tv_sec); + } + + auto delta + = std::chrono::seconds(current_time.tv_sec - this->p_past_point.tv_sec); + if (delta < 0s) { + return "in the future"; + } + if (delta < 1min) { + return "just now"; + } + if (delta < 2min) { + return "one minute ago"; + } + if (delta < 1h) { + return fmt::format( + FMT_STRING("{} minutes ago"), + std::chrono::duration_cast(delta).count()); + } + if (delta < 2h) { + return "one hour ago"; + } + if (delta < 24h) { + return fmt::format( + FMT_STRING("{} hours ago"), + std::chrono::duration_cast(delta).count()); + } + if (delta < 48h) { + return "one day ago"; + } + if (delta < 365 * 24h) { + return fmt::format(FMT_STRING("{} days ago"), delta / 24h); + } + if (delta < (2 * 365 * 24h)) { + return "over a year ago"; + } + return fmt::format(FMT_STRING("over {} years ago"), delta / (365 * 24h)); +} + +std::string +point::as_precise_time_ago() const +{ + struct timeval now, diff; + + now = this->p_recent_point.value_or(current_timeval()); + if (this->p_convert_to_local) { + now.tv_sec = convert_log_time_to_local(now.tv_sec); + } + + timersub(&now, &this->p_past_point, &diff); + if (diff.tv_sec < 0) { + return this->as_time_ago(); + } else if (diff.tv_sec <= 1) { + return "a second ago"; + } else if (diff.tv_sec < (10 * 60)) { + if (diff.tv_sec < 60) { + return fmt::format(FMT_STRING("{:2} seconds ago"), diff.tv_sec); + } + + time_t seconds = diff.tv_sec % 60; + time_t minutes = diff.tv_sec / 60; + + return fmt::format(FMT_STRING("{:2} minute{} and {:2} second{} ago"), + minutes, + minutes > 1 ? "s" : "", + seconds, + seconds == 1 ? "" : "s"); + } else { + return this->as_time_ago(); + } +} + +duration +duration::from_tv(const struct timeval& tv) +{ + return duration{tv}; +} + +std::string +duration::to_string() const +{ + /* 24h22m33s111 */ + + static const struct rel_interval { + uint64_t length; + const char* format; + const char* symbol; + } intervals[] = { + {1000, "%03lld%s", ""}, + {60, "%lld%s", "s"}, + {60, "%lld%s", "m"}, + {24, "%lld%s", "h"}, + {0, "%lld%s", "d"}, + }; + + const auto* curr_interval = intervals; + auto usecs = std::chrono::duration_cast( + std::chrono::seconds(this->d_timeval.tv_sec)) + + std::chrono::microseconds(this->d_timeval.tv_usec); + auto millis = std::chrono::duration_cast(usecs); + std::string retval; + bool neg = false; + + if (millis < 0s) { + neg = true; + millis = -millis; + } + + uint64_t remaining; + if (millis >= 10min) { + remaining + = std::chrono::duration_cast(millis).count(); + curr_interval += 1; + } else { + remaining = millis.count(); + } + + for (; curr_interval != std::end(intervals); curr_interval++) { + uint64_t amount; + char segment[32]; + + if (curr_interval->length) { + amount = remaining % curr_interval->length; + remaining = remaining / curr_interval->length; + } else { + amount = remaining; + remaining = 0; + } + + if (!amount && !remaining) { + break; + } + + snprintf(segment, + sizeof(segment), + curr_interval->format, + amount, + curr_interval->symbol); + retval.insert(0, segment); + if (remaining > 0 && amount < 10 && curr_interval->symbol[0]) { + retval.insert(0, "0"); + } + } + + if (neg) { + retval.insert(0, "-"); + } + + return retval; +} + +} // namespace time +} // namespace humanize diff --git a/src/base/humanize.time.hh b/src/base/humanize.time.hh new file mode 100644 index 0000000..96edebd --- /dev/null +++ b/src/base/humanize.time.hh @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_humanize_time_hh +#define lnav_humanize_time_hh + +#include + +#include + +#include "optional.hpp" + +namespace humanize { +namespace time { + +class point { +public: + static point from_tv(const struct timeval& tv); + + point& with_recent_point(const struct timeval& tv) + { + this->p_recent_point = tv; + return *this; + } + + point& with_convert_to_local(bool convert_to_local) + { + this->p_convert_to_local = convert_to_local; + return *this; + } + + std::string as_time_ago() const; + + std::string as_precise_time_ago() const; + +private: + explicit point(const struct timeval& tv) + : p_past_point{tv.tv_sec, tv.tv_usec} + { + } + + struct timeval p_past_point; + nonstd::optional p_recent_point; + bool p_convert_to_local{false}; +}; + +class duration { +public: + static duration from_tv(const struct timeval& tv); + + std::string to_string() const; + +private: + explicit duration(const struct timeval& tv) : d_timeval(tv) {} + + struct timeval d_timeval; +}; + +} // namespace time +} // namespace humanize + +#endif diff --git a/src/base/humanize.time.tests.cc b/src/base/humanize.time.tests.cc new file mode 100644 index 0000000..853dd04 --- /dev/null +++ b/src/base/humanize.time.tests.cc @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include "config.h" +#include "doctest/doctest.h" +#include "humanize.time.hh" + +TEST_CASE("time ago") +{ + using namespace std::chrono_literals; + + time_t t1 = 1610000000; + auto t1_chrono = std::chrono::seconds(t1); + + auto p1 = humanize::time::point::from_tv({t1, 0}).with_recent_point( + {(time_t) t1 + 5, 0}); + + CHECK(p1.as_time_ago() == "just now"); + CHECK(p1.as_precise_time_ago() == " 5 seconds ago"); + + auto p2 = humanize::time::point::from_tv({t1, 0}).with_recent_point( + {(time_t) t1 + 65, 0}); + + CHECK(p2.as_time_ago() == "one minute ago"); + CHECK(p2.as_precise_time_ago() == " 1 minute and 5 seconds ago"); + + auto p3 = humanize::time::point::from_tv({t1, 0}).with_recent_point( + {(time_t) t1 + (3 * 60 + 5), 0}); + + CHECK(p3.as_time_ago() == "3 minutes ago"); + CHECK(p3.as_precise_time_ago() == " 3 minutes and 5 seconds ago"); + + auto p4 = humanize::time::point::from_tv({t1, 0}).with_recent_point( + {(time_t) (t1_chrono + 65min).count(), 0}); + + CHECK(p4.as_time_ago() == "one hour ago"); + CHECK(p4.as_precise_time_ago() == "one hour ago"); + + auto p5 = humanize::time::point::from_tv({t1, 0}).with_recent_point( + {(time_t) (t1_chrono + 3h).count(), 0}); + + CHECK(p5.as_time_ago() == "3 hours ago"); + CHECK(p5.as_precise_time_ago() == "3 hours ago"); + + auto p6 = humanize::time::point::from_tv({t1, 0}).with_recent_point( + {(time_t) (t1_chrono + 25h).count(), 0}); + + CHECK(p6.as_time_ago() == "one day ago"); + CHECK(p6.as_precise_time_ago() == "one day ago"); + + auto p7 = humanize::time::point::from_tv({t1, 0}).with_recent_point( + {(time_t) (t1_chrono + 50h).count(), 0}); + + CHECK(p7.as_time_ago() == "2 days ago"); + CHECK(p7.as_precise_time_ago() == "2 days ago"); + + auto p8 = humanize::time::point::from_tv({t1, 0}).with_recent_point( + {(time_t) (t1_chrono + 370 * 24h).count(), 0}); + + CHECK(p8.as_time_ago() == "over a year ago"); + CHECK(p8.as_precise_time_ago() == "over a year ago"); + + auto p9 = humanize::time::point::from_tv({t1, 0}).with_recent_point( + {(time_t) (t1_chrono + 800 * 24h).count(), 0}); + + CHECK(p9.as_time_ago() == "over 2 years ago"); + CHECK(p9.as_precise_time_ago() == "over 2 years ago"); + + CHECK(humanize::time::point::from_tv({1610000000, 0}) + .with_recent_point({(time_t) 1612000000, 0}) + .as_time_ago() + == "23 days ago"); +} + +TEST_CASE("duration to_string") +{ + std::string val; + + val = humanize::time::duration::from_tv({25 * 60 * 60, 123000}).to_string(); + CHECK(val == "1d01h00m00s"); + val = humanize::time::duration::from_tv({25 * 60 * 60 + 25 * 60, 123000}) + .to_string(); + CHECK(val == "1d01h25m00s"); + val = humanize::time::duration::from_tv({10, 123000}).to_string(); + CHECK(val == "10s123"); + val = humanize::time::duration::from_tv({10, 0}).to_string(); + CHECK(val == "10s000"); + val = humanize::time::duration::from_tv({0, 100000}).to_string(); + CHECK(val == "100"); + val = humanize::time::duration::from_tv({0, 0}).to_string(); + CHECK(val == ""); + val = humanize::time::duration::from_tv({0, -10000}).to_string(); + CHECK(val == "-010"); + val = humanize::time::duration::from_tv({-10, 0}).to_string(); + CHECK(val == "-10s000"); +} diff --git a/src/base/injector.bind.hh b/src/base/injector.bind.hh new file mode 100644 index 0000000..f240cd1 --- /dev/null +++ b/src/base/injector.bind.hh @@ -0,0 +1,173 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file injector.bind.hh + */ + +#ifndef lnav_injector_bind_hh +#define lnav_injector_bind_hh + +#include "injector.hh" + +namespace injector { + +namespace details { + +template +std::function()> +create_factory(R (*)(Args...)) +{ + return []() { return std::make_shared(::injector::get()...); }; +} + +template::value, bool> = true> +std::function()> +create_factory() +{ + typename I::injectable* i = nullptr; + + return create_factory(i); +} + +template::value, bool> = true> +std::function()> +create_factory() noexcept +{ + return []() { return std::make_shared(); }; +} + +} // namespace details + +template +struct bind : singleton_storage { + template::value, bool> = true> + static bool to_singleton() noexcept + { + typename I::injectable* i = nullptr; + singleton_storage::ss_owner + = create_from_injectable(i)(); + singleton_storage::ss_data + = singleton_storage::ss_owner.get(); + singleton_storage::ss_scope = scope::singleton; + + return true; + } + + template::value, bool> = true> + static bool to_singleton() noexcept + { + singleton_storage::ss_owner = std::make_shared(); + singleton_storage::ss_data + = singleton_storage::ss_owner.get(); + singleton_storage::ss_scope = scope::singleton; + return true; + } + + struct lifetime { + ~lifetime() + { + singleton_storage::ss_owner = nullptr; + singleton_storage::ss_data = nullptr; + } + }; + + template::value, bool> = true> + static lifetime to_scoped_singleton() noexcept + { + typename I::injectable* i = nullptr; + singleton_storage::ss_owner + = create_from_injectable(i)(); + singleton_storage::ss_data + = singleton_storage::ss_owner.get(); + singleton_storage::ss_scope = scope::singleton; + + return {}; + } + + template + static bool to_instance(T* (*f)(Args...)) noexcept + { + singleton_storage::ss_data + = f(::injector::get()...); + singleton_storage::ss_scope = scope::singleton; + return true; + } + + static bool to_instance(T* data) noexcept + { + singleton_storage::ss_data = data; + singleton_storage::ss_scope = scope::singleton; + return true; + } + + template + static bool to() noexcept + { + singleton_storage::ss_factory + = details::create_factory(); + singleton_storage::ss_scope = scope::none; + return true; + } +}; + +template +struct bind_multiple : multiple_storage { + bind_multiple() noexcept = default; + + template + bind_multiple& add() noexcept + { + multiple_storage::get_factories()[typeid(I).name()] + = details::create_factory(); + + return *this; + } + + template + bind_multiple& add_singleton() noexcept + { + auto factory = details::create_factory(); + auto single = factory(); + + if (sizeof...(Annotations) > 0) { + bind::to_instance(single.get()); + } + bind::to_instance(single.get()); + multiple_storage::get_factories()[typeid(I).name()] + = [single]() { return single; }; + + return *this; + } +}; + +} // namespace injector + +#endif diff --git a/src/base/injector.hh b/src/base/injector.hh new file mode 100644 index 0000000..c6848a6 --- /dev/null +++ b/src/base/injector.hh @@ -0,0 +1,230 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file injector.hh + */ + +#ifndef lnav_injector_hh +#define lnav_injector_hh + +#include +#include +#include +#include + +#include + +#include "base/lnav_log.hh" + +namespace injector { + +enum class scope { + undefined, + none, + singleton, +}; + +template +void force_linking(Annotation anno); + +template +using void_t = void; + +template +struct with_annotations { + T value; +}; + +template +struct has_injectable : std::false_type {}; + +template +struct has_injectable> : std::true_type {}; + +template +struct singleton_storage { + static scope get_scope() { return ss_scope; } + + static T* get() + { + static int _[] = {0, (force_linking(Annotations{}), 0)...}; + (void) _; + return ss_data; + } + + static std::shared_ptr get_owner() + { + static int _[] = {0, (force_linking(Annotations{}), 0)...}; + (void) _; + return ss_owner; + } + + static std::shared_ptr create() + { + static int _[] = {0, (force_linking(Annotations{}), 0)...}; + (void) _; + return ss_factory(); + } + +protected: + static scope ss_scope; + static T* ss_data; + static std::shared_ptr ss_owner; + static std::function()> ss_factory; +}; + +template +T* singleton_storage::ss_data = nullptr; + +template +scope singleton_storage::ss_scope = scope::undefined; + +template +std::shared_ptr singleton_storage::ss_owner; + +template +std::function()> + singleton_storage::ss_factory; + +template +struct Impl { + using type = T; +}; + +template +struct multiple_storage { + static std::vector> create() + { + std::vector> retval; + + for (const auto& pair : get_factories()) { + retval.template emplace_back(pair.second()); + } + return retval; + } + +protected: + using factory_map_t + = std::map()>>; + + static factory_map_t& get_factories() + { + static factory_map_t retval; + + return retval; + } +}; + +template::value, bool> = true> +T +get() +{ + using plain_t = std::remove_const_t>; + + return *singleton_storage::get(); +} + +template::value, bool> = true> +T +get() +{ + using plain_t = std::remove_const_t>; + + return singleton_storage::get(); +} + +template +struct is_shared_ptr : std::false_type {}; + +template +struct is_shared_ptr> : std::true_type {}; + +template +struct is_vector : std::false_type {}; + +template +struct is_vector> : std::true_type {}; + +template +std::function()> create_from_injectable(R (*)(IArgs...), + Args&... args); + +template::value, bool> + = true, + std::enable_if_t::value, bool> = true> +T +get(Args&... args) +{ + typename T::element_type::injectable* i = nullptr; + + if (singleton_storage::get_scope() + == scope::singleton) + { + return singleton_storage::get_owner(); + } + return create_from_injectable(i, args...)(); +} + +template< + typename T, + typename... Annotations, + std::enable_if_t::value, bool> + = true, + std::enable_if_t::value, bool> = true> +T +get() +{ + return singleton_storage::get_owner(); +} + +template::value, bool> = true> +T +get() +{ + return multiple_storage::create(); +} + +template +std::function()> +create_from_injectable(R (*)(IArgs...), Args&... args) +{ + return [&]() { + return std::make_shared(args..., ::injector::get()...); + }; +} + +} // namespace injector + +#endif diff --git a/src/base/intern_string.cc b/src/base/intern_string.cc new file mode 100644 index 0000000..676d2bc --- /dev/null +++ b/src/base/intern_string.cc @@ -0,0 +1,303 @@ +/** + * Copyright (c) 2014, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file intern_string.cc + */ + +#include + +#include "intern_string.hh" + +#include + +#include "config.h" +#include "pcrepp/pcre2pp.hh" +#include "xxHash/xxhash.h" + +const static int TABLE_SIZE = 4095; + +struct intern_string::intern_table { + ~intern_table() + { + for (auto is : this->it_table) { + auto curr = is; + + while (curr != nullptr) { + auto next = curr->is_next; + + delete curr; + curr = next; + } + } + } + + intern_string* it_table[TABLE_SIZE]; +}; + +intern_table_lifetime +intern_string::get_table_lifetime() +{ + static intern_table_lifetime retval = std::make_shared(); + + return retval; +} + +unsigned long +hash_str(const char* str, size_t len) +{ + return XXH3_64bits(str, len); +} + +const intern_string* +intern_string::lookup(const char* str, ssize_t len) noexcept +{ + unsigned long h; + intern_string* curr; + + if (len == -1) { + len = strlen(str); + } + h = hash_str(str, len) % TABLE_SIZE; + + { + static std::mutex table_mutex; + + std::lock_guard lk(table_mutex); + auto tab = get_table_lifetime(); + + curr = tab->it_table[h]; + while (curr != nullptr) { + if (static_cast(curr->is_str.size()) == len + && strncmp(curr->is_str.c_str(), str, len) == 0) + { + return curr; + } + curr = curr->is_next; + } + + curr = new intern_string(str, len); + curr->is_next = tab->it_table[h]; + tab->it_table[h] = curr; + + return curr; + } +} + +const intern_string* +intern_string::lookup(const string_fragment& sf) noexcept +{ + return lookup(sf.data(), sf.length()); +} + +const intern_string* +intern_string::lookup(const std::string& str) noexcept +{ + return lookup(str.c_str(), str.size()); +} + +bool +intern_string::startswith(const char* prefix) const +{ + const char* curr = this->is_str.data(); + + while (*prefix != '\0' && *prefix == *curr) { + prefix += 1; + curr += 1; + } + + return *prefix == '\0'; +} + +string_fragment +string_fragment::trim(const char* tokens) const +{ + string_fragment retval = *this; + + while (retval.sf_begin < retval.sf_end) { + bool found = false; + + for (int lpc = 0; tokens[lpc] != '\0'; lpc++) { + if (retval.sf_string[retval.sf_begin] == tokens[lpc]) { + found = true; + break; + } + } + if (!found) { + break; + } + + retval.sf_begin += 1; + } + while (retval.sf_begin < retval.sf_end) { + bool found = false; + + for (int lpc = 0; tokens[lpc] != '\0'; lpc++) { + if (retval.sf_string[retval.sf_end - 1] == tokens[lpc]) { + found = true; + break; + } + } + if (!found) { + break; + } + + retval.sf_end -= 1; + } + + return retval; +} + +string_fragment +string_fragment::trim() const +{ + return this->trim(" \t\r\n"); +} + +nonstd::optional +string_fragment::consume_n(int amount) const +{ + if (amount > this->length()) { + return nonstd::nullopt; + } + + return string_fragment{ + this->sf_string, + this->sf_begin + amount, + this->sf_end, + }; +} + +string_fragment::split_result +string_fragment::split_n(int amount) const +{ + if (amount > this->length()) { + return nonstd::nullopt; + } + + return std::make_pair( + string_fragment{ + this->sf_string, + this->sf_begin, + this->sf_begin + amount, + }, + string_fragment{ + this->sf_string, + this->sf_begin + amount, + this->sf_end, + }); +} + +std::vector +string_fragment::split_lines() const +{ + std::vector retval; + int start = this->sf_begin; + + for (auto index = start; index < this->sf_end; index++) { + if (this->sf_string[index] == '\n') { + retval.emplace_back(this->sf_string, start, index + 1); + start = index + 1; + } + } + retval.emplace_back(this->sf_string, start, this->sf_end); + + return retval; +} + +Result +string_fragment::utf8_length() const +{ + ssize_t retval = 0; + + for (ssize_t byte_index = this->sf_begin; byte_index < this->sf_end;) { + auto ch_size = TRY(ww898::utf::utf8::char_size([this, byte_index]() { + return std::make_pair(this->sf_string[byte_index], + this->sf_end - byte_index); + })); + byte_index += ch_size; + retval += 1; + } + + return Ok(retval); +} + +string_fragment::case_style +string_fragment::detect_text_case_style() const +{ + static const auto LOWER_RE + = lnav::pcre2pp::code::from_const(R"(^[^A-Z]+$)"); + static const auto UPPER_RE + = lnav::pcre2pp::code::from_const(R"(^[^a-z]+$)"); + static const auto CAMEL_RE + = lnav::pcre2pp::code::from_const(R"(^(?:[A-Z][a-z0-9]+)+$)"); + + if (LOWER_RE.find_in(*this).ignore_error().has_value()) { + return case_style::lower; + } + if (UPPER_RE.find_in(*this).ignore_error().has_value()) { + return case_style::upper; + } + if (CAMEL_RE.find_in(*this).ignore_error().has_value()) { + return case_style::camel; + } + + return case_style::mixed; +} + +std::string +string_fragment::to_string_with_case_style(case_style style) const +{ + std::string retval; + + switch (style) { + case case_style::lower: { + for (auto ch : *this) { + retval.append(1, std::tolower(ch)); + } + break; + } + case case_style::upper: { + for (auto ch : *this) { + retval.append(1, std::toupper(ch)); + } + break; + } + case case_style::camel: { + retval = this->to_string(); + if (!this->empty()) { + retval[0] = toupper(retval[0]); + } + break; + } + case case_style::mixed: { + return this->to_string(); + } + } + + return retval; +} diff --git a/src/base/intern_string.hh b/src/base/intern_string.hh new file mode 100644 index 0000000..4ae40da --- /dev/null +++ b/src/base/intern_string.hh @@ -0,0 +1,865 @@ +/** + * Copyright (c) 2014, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file intern_string.hh + */ + +#ifndef intern_string_hh +#define intern_string_hh + +#include +#include +#include + +#include +#include +#include + +#include "fmt/format.h" +#include "optional.hpp" +#include "scn/util/string_view.h" +#include "strnatcmp.h" +#include "ww898/cp_utf8.hpp" + +struct string_fragment { + using iterator = const char*; + + static string_fragment invalid() + { + string_fragment retval; + + retval.invalidate(); + return retval; + } + + static string_fragment from_c_str(const char* str) + { + return string_fragment{str, 0, str != nullptr ? (int) strlen(str) : 0}; + } + + static string_fragment from_c_str(const unsigned char* str) + { + return string_fragment{ + str, 0, str != nullptr ? (int) strlen((char*) str) : 0}; + } + + template + static string_fragment from_const(const T (&str)[N]) + { + return string_fragment{str, 0, (int) N - 1}; + } + + static string_fragment from_str(const std::string& str) + { + return string_fragment{str.c_str(), 0, (int) str.size()}; + } + + static string_fragment from_substr(const std::string& str, + size_t offset, + size_t length) + { + return string_fragment{ + str.c_str(), (int) offset, (int) (offset + length)}; + } + + static string_fragment from_str_range(const std::string& str, + size_t begin, + size_t end) + { + return string_fragment{str.c_str(), (int) begin, (int) end}; + } + + static string_fragment from_bytes(const char* bytes, size_t len) + { + return string_fragment{bytes, 0, (int) len}; + } + + static string_fragment from_bytes(const unsigned char* bytes, size_t len) + { + return string_fragment{(const char*) bytes, 0, (int) len}; + } + + static string_fragment from_memory_buffer(const fmt::memory_buffer& buf) + { + return string_fragment{buf.data(), 0, (int) buf.size()}; + } + + static string_fragment from_byte_range(const char* bytes, + size_t begin, + size_t end) + { + return string_fragment{bytes, (int) begin, (int) end}; + } + + explicit string_fragment(const char* str = "", int begin = 0, int end = -1) + : sf_string(str), sf_begin(begin), sf_end(end == -1 ? strlen(str) : end) + { + } + + explicit string_fragment(const unsigned char* str, + int begin = 0, + int end = -1) + : sf_string((const char*) str), sf_begin(begin), + sf_end(end == -1 ? strlen((const char*) str) : end) + { + } + + string_fragment(const std::string& str) + : sf_string(str.c_str()), sf_begin(0), sf_end(str.length()) + { + } + + bool is_valid() const + { + return this->sf_begin != -1 && this->sf_begin <= this->sf_end; + } + + int length() const { return this->sf_end - this->sf_begin; } + + Result utf8_length() const; + + const char* data() const { return &this->sf_string[this->sf_begin]; } + + const unsigned char* udata() const + { + return (const unsigned char*) &this->sf_string[this->sf_begin]; + } + + char* writable_data(int offset = 0) + { + return (char*) &this->sf_string[this->sf_begin + offset]; + } + + char front() const { return this->sf_string[this->sf_begin]; } + + uint32_t front_codepoint() const + { + size_t index = 0; + try { + return ww898::utf::utf8::read( + [this, &index]() { return this->data()[index++]; }); + } catch (const std::runtime_error& e) { + return this->data()[0]; + } + } + + char back() const { return this->sf_string[this->sf_end - 1]; } + + void pop_back() + { + if (!this->empty()) { + this->sf_end -= 1; + } + } + + iterator begin() const { return &this->sf_string[this->sf_begin]; } + + iterator end() const { return &this->sf_string[this->sf_end]; } + + bool empty() const { return !this->is_valid() || length() == 0; } + + Result codepoint_to_byte_index(ssize_t cp_index) const + { + ssize_t retval = 0; + + while (cp_index > 0) { + if (retval >= this->length()) { + return Err("index is beyond the end of the string"); + } + auto ch_len = TRY(ww898::utf::utf8::char_size([this, retval]() { + return std::make_pair(this->data()[retval], + this->length() - retval - 1); + })); + + retval += ch_len; + cp_index -= 1; + } + + return Ok(retval); + } + + const char& operator[](int index) const + { + return this->sf_string[sf_begin + index]; + } + + bool operator==(const std::string& str) const + { + if (this->length() != (int) str.length()) { + return false; + } + + return memcmp( + &this->sf_string[this->sf_begin], str.c_str(), str.length()) + == 0; + } + + bool operator==(const string_fragment& sf) const + { + if (this->length() != sf.length()) { + return false; + } + + return memcmp(this->data(), sf.data(), sf.length()) == 0; + } + + bool iequal(const string_fragment& sf) const + { + if (this->length() != sf.length()) { + return false; + } + + return strnatcasecmp( + this->length(), this->data(), sf.length(), sf.data()) + == 0; + } + + bool operator==(const char* str) const + { + size_t len = strlen(str); + + return len == (size_t) this->length() + && strncmp(this->data(), str, this->length()) == 0; + } + + bool operator!=(const char* str) const { return !(*this == str); } + + bool startswith(const char* prefix) const + { + const auto* iter = this->begin(); + + while (*prefix != '\0' && iter < this->end() && *prefix == *iter) { + prefix += 1; + iter += 1; + } + + return *prefix == '\0'; + } + + bool endswith(const char* suffix) const + { + int suffix_len = strlen(suffix); + + if (suffix_len > this->length()) { + return false; + } + + const auto* curr = this->end() - suffix_len; + while (*suffix != '\0' && *curr == *suffix) { + suffix += 1; + curr += 1; + } + + return *suffix == '\0'; + } + + string_fragment substr(int begin) const + { + return string_fragment{ + this->sf_string, this->sf_begin + begin, this->sf_end}; + } + + string_fragment sub_range(int begin, int end) const + { + return string_fragment{ + this->sf_string, this->sf_begin + begin, this->sf_begin + end}; + } + + size_t count(char ch) const + { + size_t retval = 0; + + for (int lpc = this->sf_begin; lpc < this->sf_end; lpc++) { + if (this->sf_string[lpc] == ch) { + retval += 1; + } + } + + return retval; + } + + nonstd::optional find(char ch) const + { + for (int lpc = this->sf_begin; lpc < this->sf_end; lpc++) { + if (this->sf_string[lpc] == ch) { + return lpc - this->sf_begin; + } + } + + return nonstd::nullopt; + } + + template + string_fragment find_left_boundary(size_t start, P&& predicate) const + { + assert((int) start <= this->length()); + + if (start > 0 && start == this->length()) { + start -= 1; + } + while (start > 0) { + if (predicate(this->data()[start])) { + start += 1; + break; + } + start -= 1; + } + + return string_fragment{ + this->sf_string, + (int) start, + this->sf_end, + }; + } + + template + string_fragment find_right_boundary(size_t start, P&& predicate) const + { + while ((int) start < this->length()) { + if (predicate(this->data()[start])) { + break; + } + start += 1; + } + + return string_fragment{ + this->sf_string, + this->sf_begin, + this->sf_begin + (int) start, + }; + } + + template + string_fragment find_boundaries_around(size_t start, P&& predicate) const + { + return this->template find_left_boundary(start, predicate) + .find_right_boundary(0, predicate); + } + + nonstd::optional> consume_codepoint() + const + { + auto cp = this->front_codepoint(); + auto index_res = this->codepoint_to_byte_index(1); + + if (index_res.isErr()) { + return nonstd::nullopt; + } + + return std::make_pair(cp, this->substr(index_res.unwrap())); + } + + template + nonstd::optional consume(P predicate) const + { + int consumed = 0; + while (consumed < this->length()) { + if (!predicate(this->data()[consumed])) { + break; + } + + consumed += 1; + } + + if (consumed == 0) { + return nonstd::nullopt; + } + + return string_fragment{ + this->sf_string, + this->sf_begin + consumed, + this->sf_end, + }; + } + + nonstd::optional consume_n(int amount) const; + + template + string_fragment skip(P predicate) const + { + int offset = 0; + while (offset < this->length() && predicate(this->data()[offset])) { + offset += 1; + } + + return string_fragment{ + this->sf_string, + this->sf_begin + offset, + this->sf_end, + }; + } + + using split_result + = nonstd::optional>; + + template + split_result split_while(P&& predicate) const + { + int consumed = 0; + while (consumed < this->length()) { + if (!predicate(this->data()[consumed])) { + break; + } + + consumed += 1; + } + + if (consumed == 0) { + return nonstd::nullopt; + } + + return std::make_pair( + string_fragment{ + this->sf_string, + this->sf_begin, + this->sf_begin + consumed, + }, + string_fragment{ + this->sf_string, + this->sf_begin + consumed, + this->sf_end, + }); + } + + template + split_result split_when(P&& predicate) const + { + int consumed = 0; + while (consumed < this->length()) { + if (predicate(this->data()[consumed])) { + break; + } + + consumed += 1; + } + + if (consumed == 0) { + return nonstd::nullopt; + } + + return std::make_pair( + string_fragment{ + this->sf_string, + this->sf_begin, + this->sf_begin + consumed, + }, + string_fragment{ + this->sf_string, + this->sf_begin + consumed + 1, + this->sf_end, + }); + } + + split_result split_n(int amount) const; + + std::vector split_lines() const; + + struct tag1 { + const char t_value; + + bool operator()(char ch) const { return this->t_value == ch; } + }; + + struct quoted_string_body { + bool qs_in_escape{false}; + + bool operator()(char ch) + { + if (this->qs_in_escape) { + this->qs_in_escape = false; + return true; + } else if (ch == '\\') { + this->qs_in_escape = true; + return true; + } else if (ch == '"') { + return false; + } else { + return true; + } + } + }; + + const char* to_string(char* buf) const + { + memcpy(buf, this->data(), this->length()); + buf[this->length()] = '\0'; + + return buf; + } + + std::string to_string() const + { + return {this->data(), (size_t) this->length()}; + } + + void clear() + { + this->sf_begin = 0; + this->sf_end = 0; + } + + void invalidate() + { + this->sf_begin = -1; + this->sf_end = -1; + } + + string_fragment trim(const char* tokens) const; + string_fragment trim() const; + + string_fragment prepend(const char* str, int amount) const + { + return string_fragment{ + str, + this->sf_begin + amount, + this->sf_end + amount, + }; + } + + string_fragment erase_before(const char* str, int amount) const + { + return string_fragment{ + str, + this->sf_begin - amount, + this->sf_end - amount, + }; + } + + string_fragment erase(const char* str, int amount) const + { + return string_fragment{ + str, + this->sf_begin, + this->sf_end - amount, + }; + } + + template + const char* to_c_str(A allocator) const + { + auto* retval = allocator.allocate(this->length() + 1); + memcpy(retval, this->data(), this->length()); + retval[this->length()] = '\0'; + return retval; + } + + template + string_fragment to_owned(A allocator) const + { + return string_fragment{ + this->template to_c_str(allocator), + 0, + this->length(), + }; + } + + scn::string_view to_string_view() const + { + return scn::string_view{this->begin(), this->end()}; + } + + enum class case_style { + lower, + upper, + camel, + mixed, + }; + + case_style detect_text_case_style() const; + + std::string to_string_with_case_style(case_style style) const; + + const char* sf_string; + int sf_begin; + int sf_end; +}; + +inline bool +operator==(const std::string& left, const string_fragment& right) +{ + return right == left; +} + +inline bool +operator<(const char* left, const string_fragment& right) +{ + int rc = strncmp(left, right.data(), right.length()); + return rc < 0; +} + +inline void +operator+=(std::string& left, const string_fragment& right) +{ + left.append(right.data(), right.length()); +} + +inline bool +operator<(const string_fragment& left, const char* right) +{ + return strncmp(left.data(), right, left.length()) < 0; +} + +inline std::ostream& +operator<<(std::ostream& os, const string_fragment& sf) +{ + os.write(sf.data(), sf.length()); + return os; +} + +class intern_string { +public: + static const intern_string* lookup(const char* str, ssize_t len) noexcept; + + static const intern_string* lookup(const string_fragment& sf) noexcept; + + static const intern_string* lookup(const std::string& str) noexcept; + + const char* get() const { return this->is_str.c_str(); }; + + size_t size() const { return this->is_str.size(); } + + std::string to_string() const { return this->is_str; } + + string_fragment to_string_fragment() const + { + return string_fragment{this->is_str}; + } + + bool startswith(const char* prefix) const; + + struct intern_table; + static std::shared_ptr get_table_lifetime(); + +private: + friend intern_table; + + intern_string(const char* str, ssize_t len) + : is_next(nullptr), is_str(str, (size_t) len) + { + } + + intern_string* is_next; + std::string is_str; +}; + +using intern_table_lifetime = std::shared_ptr; + +class intern_string_t { +public: + using iterator = const char*; + + intern_string_t(const intern_string* is = nullptr) : ist_interned_string(is) + { + } + + const intern_string* unwrap() const { return this->ist_interned_string; } + + void clear() { this->ist_interned_string = nullptr; }; + + bool empty() const { return this->ist_interned_string == nullptr; } + + const char* get() const + { + if (this->empty()) { + return ""; + } + return this->ist_interned_string->get(); + } + + const char* c_str() const { return this->get(); } + + iterator begin() const { return this->get(); } + + iterator end() const { return this->get() + this->size(); } + + size_t size() const + { + if (this->ist_interned_string == nullptr) { + return 0; + } + return this->ist_interned_string->size(); + } + + size_t hash() const + { + auto ptr = (uintptr_t) this->ist_interned_string; + + return ptr; + } + + std::string to_string() const + { + if (this->ist_interned_string == nullptr) { + return ""; + } + return this->ist_interned_string->to_string(); + } + + string_fragment to_string_fragment() const + { + if (this->ist_interned_string == nullptr) { + return string_fragment{"", 0, 0}; + } + return this->ist_interned_string->to_string_fragment(); + } + + bool operator<(const intern_string_t& rhs) const + { + return strcmp(this->get(), rhs.get()) < 0; + } + + bool operator==(const intern_string_t& rhs) const + { + return this->ist_interned_string == rhs.ist_interned_string; + } + + bool operator!=(const intern_string_t& rhs) const + { + return !(*this == rhs); + } + + bool operator==(const char* rhs) const + { + return strcmp(this->get(), rhs) == 0; + } + + bool operator!=(const char* rhs) const + { + return strcmp(this->get(), rhs) != 0; + } + + static bool case_lt(const intern_string_t& lhs, const intern_string_t& rhs) + { + return strnatcasecmp(lhs.size(), lhs.get(), rhs.size(), rhs.get()) < 0; + } + +private: + const intern_string* ist_interned_string; +}; + +unsigned long hash_str(const char* str, size_t len); + +namespace fmt { +template<> +struct formatter : formatter { + template + auto format(const string_fragment& sf, FormatContext& ctx) + { + return formatter::format( + string_view{sf.data(), (size_t) sf.length()}, ctx); + } +}; + +template<> +struct formatter : formatter { + template + auto format(const intern_string_t& is, FormatContext& ctx) + { + return formatter::format( + string_view{is.get(), (size_t) is.size()}, ctx); + } +}; +} // namespace fmt + +namespace std { +template<> +struct hash { + std::size_t operator()(const intern_string_t& ist) const + { + return ist.hash(); + } +}; +} // namespace std + +inline bool +operator<(const char* left, const intern_string_t& right) +{ + int rc = strncmp(left, right.get(), right.size()); + return rc < 0; +} + +inline bool +operator<(const intern_string_t& left, const char* right) +{ + return strncmp(left.get(), right, left.size()) < 0; +} + +inline bool +operator==(const intern_string_t& left, const string_fragment& sf) +{ + return ((int) left.size() == sf.length()) + && (memcmp(left.get(), sf.data(), left.size()) == 0); +} + +inline bool +operator==(const string_fragment& left, const intern_string_t& right) +{ + return (left.length() == (int) right.size()) + && (memcmp(left.data(), right.get(), left.length()) == 0); +} + +namespace std { +inline string +to_string(const string_fragment& s) +{ + return {s.data(), (size_t) s.length()}; +} + +inline string +to_string(const intern_string_t& s) +{ + return s.to_string(); +} +} // namespace std + +inline string_fragment +to_string_fragment(const string_fragment& s) +{ + return s; +} + +inline string_fragment +to_string_fragment(const intern_string_t& s) +{ + return string_fragment(s.get(), 0, s.size()); +} + +inline string_fragment +to_string_fragment(const std::string& s) +{ + return string_fragment(s.c_str(), 0, s.length()); +} + +struct frag_hasher { + size_t operator()(const string_fragment& sf) const + { + return hash_str(sf.data(), sf.length()); + } +}; + +#endif diff --git a/src/base/intern_string.tests.cc b/src/base/intern_string.tests.cc new file mode 100644 index 0000000..8816803 --- /dev/null +++ b/src/base/intern_string.tests.cc @@ -0,0 +1,152 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include + +#include "intern_string.hh" + +#include "config.h" +#include "doctest/doctest.h" + +TEST_CASE("string_fragment::startswith") +{ + std::string empty; + auto sf = string_fragment{empty}; + + CHECK_FALSE(sf.startswith("abc")); +} + +TEST_CASE("split_lines") +{ + std::string in1 = "Hello, World!"; + std::string in2 = "Hello, World!\nGoodbye, World!"; + + { + auto sf = string_fragment(in1); + auto split = sf.split_lines(); + + CHECK(1 == split.size()); + CHECK(in1 == split[0].to_string()); + } + + { + auto sf = string_fragment::from_str_range(in1, 7, -1); + auto split = sf.split_lines(); + + CHECK(1 == split.size()); + CHECK("World!" == split[0].to_string()); + } + + { + auto sf = string_fragment(in2); + auto split = sf.split_lines(); + + CHECK(2 == split.size()); + CHECK("Hello, World!\n" == split[0].to_string()); + CHECK("Goodbye, World!" == split[1].to_string()); + } +} + +TEST_CASE("consume") +{ + auto is_eq = string_fragment::tag1{'='}; + auto is_dq = string_fragment::tag1{'"'}; + auto is_colon = string_fragment::tag1{':'}; + + const char* pair = "foo = bar"; + auto sf = string_fragment(pair); + + auto split_sf = sf.split_while(isalnum); + + CHECK(split_sf.has_value()); + CHECK(split_sf->first.to_string() == "foo"); + CHECK(split_sf->second.to_string() == " = bar"); + + auto value_frag = split_sf->second.skip(isspace).consume(is_eq); + + CHECK(value_frag.has_value()); + CHECK(value_frag->to_string() == " bar"); + + auto stripped_value_frag = value_frag->consume(isspace); + + CHECK(stripped_value_frag.has_value()); + CHECK(stripped_value_frag->to_string() == "bar"); + + auto no_value = sf.consume(is_colon); + CHECK(!no_value.has_value()); + + const char* qs = R"("foo \" bar")"; + auto qs_sf = string_fragment{qs}; + + auto qs_body = qs_sf.consume(is_dq); + string_fragment::quoted_string_body qsb; + auto split_body = qs_body->split_while(qsb); + + CHECK(split_body.has_value()); + CHECK(split_body->first.to_string() == "foo \\\" bar"); + CHECK(split_body->second.to_string() == "\""); + + auto empty = split_body->second.consume(is_dq); + + CHECK(empty.has_value()); + CHECK(empty->empty()); +} + +TEST_CASE("find_left_boundary") +{ + std::string in1 = "Hello,\nWorld!\n"; + + { + auto sf = string_fragment{in1}; + + auto world_sf = sf.find_left_boundary( + in1.length() - 3, [](auto ch) { return ch == '\n'; }); + CHECK(world_sf.to_string() == "World!\n"); + auto full_sf + = sf.find_left_boundary(3, [](auto ch) { return ch == '\n'; }); + CHECK(full_sf.to_string() == in1); + } +} + +TEST_CASE("find_right_boundary") +{ + std::string in1 = "Hello,\nWorld!\n"; + + { + auto sf = string_fragment{in1}; + + auto world_sf = sf.find_right_boundary( + in1.length() - 3, [](auto ch) { return ch == '\n'; }); + CHECK(world_sf.to_string() == "Hello,\nWorld!"); + auto hello_sf + = sf.find_right_boundary(3, [](auto ch) { return ch == '\n'; }); + CHECK(hello_sf.to_string() == "Hello,"); + } +} diff --git a/src/base/is_utf8.cc b/src/base/is_utf8.cc new file mode 100644 index 0000000..f55dfe0 --- /dev/null +++ b/src/base/is_utf8.cc @@ -0,0 +1,313 @@ +/* + * is_utf8 is distributed under the following terms: + * + * Copyright (c) 2013 Palard Julien. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "is_utf8.hh" + +#include "config.h" + +/* + Check if the given unsigned char * is a valid utf-8 sequence. + + Return value : + If the string is valid utf-8, 0 is returned. + Else the position, starting from 1, is returned. + + Source: + http://www.unicode.org/versions/Unicode7.0.0/UnicodeStandard-7.0.pdf + page 124, 3.9 "Unicode Encoding Forms", "UTF-8" + + + Table 3-7. Well-Formed UTF-8 Byte Sequences + ----------------------------------------------------------------------------- + | Code Points | First Byte | Second Byte | Third Byte | Fourth Byte | + | U+0000..U+007F | 00..7F | | | | + | U+0080..U+07FF | C2..DF | 80..BF | | | + | U+0800..U+0FFF | E0 | A0..BF | 80..BF | | + | U+1000..U+CFFF | E1..EC | 80..BF | 80..BF | | + | U+D000..U+D7FF | ED | 80..9F | 80..BF | | + | U+E000..U+FFFF | EE..EF | 80..BF | 80..BF | | + | U+10000..U+3FFFF | F0 | 90..BF | 80..BF | 80..BF | + | U+40000..U+FFFFF | F1..F3 | 80..BF | 80..BF | 80..BF | + | U+100000..U+10FFFF | F4 | 80..8F | 80..BF | 80..BF | + ----------------------------------------------------------------------------- + + Returns the first erroneous byte position, and give in + `faulty_bytes` the number of actually existing bytes taking part in this + error. +*/ +utf8_scan_result +is_utf8(string_fragment str, nonstd::optional terminator) +{ + const auto* ustr = str.udata(); + utf8_scan_result retval; + ssize_t i = 0; + + while (i < str.length()) { + if (ustr[i] == '\x1b') { + retval.usr_has_ansi = true; + } + + if (terminator && ustr[i] == terminator.value()) { + if (retval.usr_message == nullptr) { + retval.usr_valid_frag = str.sub_range(0, i); + } + retval.usr_remaining = str.substr(i + 1); + break; + } + + if (retval.usr_message != nullptr) { + i += 1; + continue; + } + + retval.usr_valid_frag = str.sub_range(0, i); + if (ustr[i] <= 0x7F) /* 00..7F */ { + i += 1; + } else if (ustr[i] >= 0xC2 && ustr[i] <= 0xDF) /* C2..DF 80..BF */ { + if (i + 1 < str.length()) /* Expect a 2nd byte */ { + if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0xBF) { + retval.usr_message + = "After a first byte between C2 and DF, expecting a " + "2nd byte between 80 and BF"; + retval.usr_faulty_bytes = 2; + continue; + } + } else { + retval.usr_message + = "After a first byte between C2 and DF, expecting a 2nd " + "byte."; + retval.usr_faulty_bytes = 1; + continue; + } + i += 2; + } else if (ustr[i] == 0xE0) /* E0 A0..BF 80..BF */ { + if (i + 2 < str.length()) /* Expect a 2nd and 3rd byte */ { + if (ustr[i + 1] < 0xA0 || ustr[i + 1] > 0xBF) { + retval.usr_message + = "After a first byte of E0, expecting a 2nd byte " + "between A0 and BF."; + retval.usr_faulty_bytes = 2; + continue; + } + if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) { + retval.usr_message + = "After a first byte of E0, expecting a 3nd byte " + "between 80 and BF."; + retval.usr_faulty_bytes = 3; + continue; + } + } else { + retval.usr_message + = "After a first byte of E0, expecting two following " + "bytes."; + retval.usr_faulty_bytes = 1; + continue; + } + i += 3; + } else if (ustr[i] >= 0xE1 + && ustr[i] <= 0xEC) /* E1..EC 80..BF 80..BF */ + { + if (i + 2 < str.length()) /* Expect a 2nd and 3rd byte */ { + if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0xBF) { + retval.usr_message + = "After a first byte between E1 and EC, expecting the " + "2nd byte between 80 and BF."; + retval.usr_faulty_bytes = 2; + continue; + } + if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) { + retval.usr_message + = "After a first byte between E1 and EC, expecting the " + "3rd byte between 80 and BF."; + retval.usr_faulty_bytes = 3; + continue; + } + } else { + retval.usr_message + = "After a first byte between E1 and EC, expecting two " + "following bytes."; + retval.usr_faulty_bytes = 1; + continue; + } + i += 3; + } else if (ustr[i] == 0xED) /* ED 80..9F 80..BF */ { + if (i + 2 < str.length()) /* Expect a 2nd and 3rd byte */ { + if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0x9F) { + retval.usr_message + = "After a first byte of ED, expecting 2nd byte " + "between 80 and 9F."; + retval.usr_faulty_bytes = 2; + continue; + } + if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) { + retval.usr_message + = "After a first byte of ED, expecting 3rd byte " + "between 80 and BF."; + retval.usr_faulty_bytes = 3; + continue; + } + } else { + retval.usr_message + = "After a first byte of ED, expecting two following " + "bytes."; + retval.usr_faulty_bytes = 1; + continue; + } + i += 3; + } else if (ustr[i] >= 0xEE + && ustr[i] <= 0xEF) /* EE..EF 80..BF 80..BF */ + { + if (i + 2 < str.length()) /* Expect a 2nd and 3rd byte */ { + if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0xBF) { + retval.usr_message + = "After a first byte between EE and EF, expecting 2nd " + "byte between 80 and BF."; + retval.usr_faulty_bytes = 2; + continue; + } + if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) { + retval.usr_message + = "After a first byte between EE and EF, expecting 3rd " + "byte between 80 and BF."; + retval.usr_faulty_bytes = 3; + continue; + } + } else { + retval.usr_message + = "After a first byte between EE and EF, two following " + "bytes."; + retval.usr_faulty_bytes = 1; + continue; + } + i += 3; + } else if (ustr[i] == 0xF0) /* F0 90..BF 80..BF 80..BF */ { + if (i + 3 < str.length()) /* Expect a 2nd, 3rd 3th byte */ { + if (ustr[i + 1] < 0x90 || ustr[i + 1] > 0xBF) { + retval.usr_message + = "After a first byte of F0, expecting 2nd byte " + "between 90 and BF."; + retval.usr_faulty_bytes = 2; + continue; + } + if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) { + retval.usr_message + = "After a first byte of F0, expecting 3rd byte " + "between 80 and BF."; + retval.usr_faulty_bytes = 3; + continue; + } + if (ustr[i + 3] < 0x80 || ustr[i + 3] > 0xBF) { + retval.usr_message + = "After a first byte of F0, expecting 4th byte " + "between 80 and BF."; + retval.usr_faulty_bytes = 4; + continue; + } + } else { + retval.usr_message + = "After a first byte of F0, expecting three following " + "bytes."; + retval.usr_faulty_bytes = 1; + continue; + } + i += 4; + } else if (ustr[i] >= 0xF1 + && ustr[i] <= 0xF3) /* F1..F3 80..BF 80..BF 80..BF */ + { + if (i + 3 < str.length()) /* Expect a 2nd, 3rd 3th byte */ { + if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0xBF) { + retval.usr_message + = "After a first byte of F1, F2, or F3, expecting a " + "2nd byte between 80 and BF."; + retval.usr_faulty_bytes = 2; + continue; + } + if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) { + retval.usr_message + = "After a first byte of F1, F2, or F3, expecting a " + "3rd byte between 80 and BF."; + retval.usr_faulty_bytes = 3; + continue; + } + if (ustr[i + 3] < 0x80 || ustr[i + 3] > 0xBF) { + retval.usr_message + = "After a first byte of F1, F2, or F3, expecting a " + "4th byte between 80 and BF."; + retval.usr_faulty_bytes = 4; + continue; + } + } else { + retval.usr_message + = "After a first byte of F1, F2, or F3, expecting three " + "following bytes."; + retval.usr_faulty_bytes = 1; + continue; + } + i += 4; + } else if (ustr[i] == 0xF4) /* F4 80..8F 80..BF 80..BF */ { + if (i + 3 < str.length()) /* Expect a 2nd, 3rd 3th byte */ { + if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0x8F) { + retval.usr_message + = "After a first byte of F4, expecting 2nd byte " + "between 80 and 8F."; + retval.usr_faulty_bytes = 2; + continue; + } + if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) { + retval.usr_message + = "After a first byte of F4, expecting 3rd byte " + "between 80 and BF."; + retval.usr_faulty_bytes = 3; + continue; + } + if (ustr[i + 3] < 0x80 || ustr[i + 3] > 0xBF) { + retval.usr_message + = "After a first byte of F4, expecting 4th byte " + "between 80 and BF."; + retval.usr_faulty_bytes = 4; + continue; + } + } else { + retval.usr_message + = "After a first byte of F4, expecting three following " + "bytes."; + retval.usr_faulty_bytes = 1; + continue; + } + i += 4; + } else { + retval.usr_message + = "Expecting bytes in the following ranges: 00..7F C2..F4."; + retval.usr_faulty_bytes = 1; + continue; + } + } + if (retval.usr_message == nullptr) { + retval.usr_valid_frag = str.sub_range(0, i); + } + return retval; +} diff --git a/src/base/is_utf8.hh b/src/base/is_utf8.hh new file mode 100644 index 0000000..56a959f --- /dev/null +++ b/src/base/is_utf8.hh @@ -0,0 +1,59 @@ +/* + * is_utf8 is distributed under the following terms: + * + * Copyright (c) 2013 Palard Julien. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#ifndef _IS_UTF8_H +#define _IS_UTF8_H + +#include +#include + +#include "intern_string.hh" +#include "optional.hpp" + +struct utf8_scan_result { + const char* usr_message{nullptr}; + size_t usr_faulty_bytes{0}; + string_fragment usr_valid_frag{string_fragment::invalid()}; + nonstd::optional usr_remaining; + bool usr_has_ansi{false}; + + const char* remaining_ptr(const string_fragment& frag) const + { + if (this->usr_remaining) { + return this->usr_remaining->begin(); + } else { + return nullptr; + } + } + bool is_valid() const { return this->usr_message == nullptr; } +}; + +utf8_scan_result is_utf8(string_fragment frag, + nonstd::optional terminator + = nonstd::nullopt); + +#endif /* _IS_UTF8_H */ diff --git a/src/base/isc.cc b/src/base/isc.cc new file mode 100644 index 0000000..7dbce11 --- /dev/null +++ b/src/base/isc.cc @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file isc.cc + */ + +#include + +#include "isc.hh" + +#include "config.h" + +namespace isc { + +void +service_base::start() +{ + log_debug("starting service thread for: %s", this->s_name.c_str()); + this->s_thread = std::thread(&service_base::run, this); + this->s_started = true; +} + +void* +service_base::run() +{ + log_info("BEGIN isc thread: %s", this->s_name.c_str()); + while (this->s_looping) { + mstime_t current_time = getmstime(); + auto timeout = this->compute_timeout(current_time); + + this->s_port.process_for(timeout); + this->s_children.cleanup_children(); + + try { + this->loop_body(); + } catch (const std::exception& e) { + log_error("%s: loop_body() failed with -- %s", + this->s_name.c_str(), + e.what()); + this->s_looping = false; + } catch (...) { + log_error("%s: loop_body() failed with non-standard exception", + this->s_name.c_str()); + this->s_looping = false; + } + } + if (!this->s_children.empty()) { + log_debug("stopping children of service: %s", this->s_name.c_str()); + this->s_children.stop_children(); + } + this->stopped(); + log_info("END isc thread: %s", this->s_name.c_str()); + + return nullptr; +} + +void +service_base::stop() +{ + if (this->s_started) { + log_debug("stopping service thread: %s", this->s_name.c_str()); + if (this->s_looping) { + this->s_looping = false; + this->s_port.send(empty_msg()); + } + log_debug("waiting for service thread: %s", this->s_name.c_str()); + this->s_thread.join(); + log_debug("joined service thread: %s", this->s_name.c_str()); + this->s_started = false; + } +} + +supervisor::supervisor(service_list servs, service_base* parent) + : s_service_list(std::move(servs)), s_parent(parent) +{ + for (auto& serv : this->s_service_list) { + serv->start(); + } +} + +supervisor::~supervisor() +{ + this->stop_children(); +} + +void +supervisor::stop_children() +{ + for (auto& serv : this->s_service_list) { + serv->stop(); + } + this->cleanup_children(); +} + +void +supervisor::cleanup_children() +{ + this->s_service_list.erase( + std::remove_if(this->s_service_list.begin(), + this->s_service_list.end(), + [this](auto& child) { + if (child->is_looping()) { + return false; + } + + child->stop(); + if (this->s_parent != nullptr) { + this->s_parent->child_finished(child); + } + return true; + }), + this->s_service_list.end()); +} + +void +supervisor::add_child_service(std::shared_ptr new_service) +{ + this->s_service_list.emplace_back(new_service); + new_service->start(); +} + +} // namespace isc diff --git a/src/base/isc.hh b/src/base/isc.hh new file mode 100644 index 0000000..339dcaf --- /dev/null +++ b/src/base/isc.hh @@ -0,0 +1,225 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file isc.hh + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "injector.hh" +#include "safe/safe.h" +#include "time_util.hh" + +#ifndef lnav_isc_hh +# define lnav_isc_hh + +namespace isc { + +struct msg { + std::function m_callback; +}; + +inline msg +empty_msg() +{ + return {[]() {}}; +} + +class msg_port { +public: + msg_port() = default; + + void send(msg&& m) + { + safe::WriteAccess writable_msgs( + this->mp_messages); + + writable_msgs->emplace_back(m); + this->sp_cond.notify_all(); + } + + template + void process_for(const std::chrono::duration& rel_time) + { + std::deque tmp_msgs; + + { + safe::WriteAccess + writable_msgs(this->mp_messages); + + if (writable_msgs->empty() && rel_time.count() > 0) { + this->sp_cond.template wait_for(writable_msgs.lock, rel_time); + } + + tmp_msgs.swap(*writable_msgs); + } + while (!tmp_msgs.empty()) { + auto& m = tmp_msgs.front(); + + m.m_callback(); + tmp_msgs.pop_front(); + } + } + +private: + using message_list = std::deque; + using safe_message_list = safe::Safe; + + std::condition_variable sp_cond; + safe_message_list mp_messages; +}; + +class service_base; +using service_list = std::vector>; + +struct supervisor { + explicit supervisor(service_list servs = {}, + service_base* parent = nullptr); + + ~supervisor(); + + bool empty() const { return this->s_service_list.empty(); } + + void add_child_service(std::shared_ptr new_service); + + void stop_children(); + + void cleanup_children(); + +protected: + service_list s_service_list; + service_base* s_parent; +}; + +class service_base : public std::enable_shared_from_this { +public: + explicit service_base(std::string name) + : s_name(std::move(name)), s_children({}, this) + { + } + + virtual ~service_base() = default; + + bool is_looping() const { return this->s_looping; } + + msg_port& get_port() { return this->s_port; } + + friend supervisor; + +private: + void start(); + + void stop(); + +protected: + virtual void* run(); + virtual void loop_body() {} + virtual void child_finished(std::shared_ptr child) {} + virtual void stopped() {} + virtual std::chrono::milliseconds compute_timeout( + mstime_t current_time) const + { + using namespace std::literals::chrono_literals; + + return 1s; + } + + const std::string s_name; + bool s_started{false}; + std::thread s_thread; + std::atomic s_looping{true}; + msg_port s_port; + supervisor s_children; +}; + +template +class service : public service_base { +public: + explicit service(std::string sub_name = "") + : service_base(std::string(__PRETTY_FUNCTION__) + " " + sub_name) + { + } + + template + void send(F msg) + { + this->s_port.send({[lifetime = this->shared_from_this(), this, msg]() { + msg(*(static_cast(this))); + }}); + } + + template + void send_and_wait(F msg, + const std::chrono::duration& rel_time) + { + msg_port reply_port; + + this->s_port.send( + {[lifetime = this->shared_from_this(), this, &reply_port, msg]() { + msg(*(static_cast(this))); + reply_port.send(empty_msg()); + }}); + reply_port.template process_for(rel_time); + } +}; + +template +struct to { + void send(std::function cb) + { + auto& service = injector::get(); + + service.send(cb); + } + + template + void send_and_wait(std::function cb, + const std::chrono::duration& rel_time) + { + auto& service = injector::get(); + + service.send_and_wait(cb, rel_time); + } + + void send_and_wait(std::function cb) + { + using namespace std::literals::chrono_literals; + + this->send_and_wait(cb, 48h); + } +}; + +} // namespace isc + +#endif diff --git a/src/base/itertools.hh b/src/base/itertools.hh new file mode 100644 index 0000000..058ceb8 --- /dev/null +++ b/src/base/itertools.hh @@ -0,0 +1,785 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_itertools_hh +#define lnav_itertools_hh + +#include +#include +#include +#include +#include + +#include "func_util.hh" +#include "optional.hpp" + +namespace lnav { +namespace itertools { + +struct empty {}; + +struct not_empty {}; + +struct full { + size_t f_max_size; +}; + +namespace details { + +template +struct unwrap_or { + T uo_value; +}; + +template +struct find_if { + P fi_predicate; +}; + +template +struct find { + T f_value; +}; + +struct first {}; + +struct second {}; + +template +struct filter_in { + F f_func; +}; + +template +struct filter_out { + F f_func; +}; + +template +struct sort_by { + C sb_cmp; +}; + +struct sorted {}; + +template +struct mapper { + F m_func; +}; + +template +struct flat_mapper { + F fm_func; +}; + +template +struct for_eacher { + F fe_func; +}; + +template +struct folder { + R f_func; + T f_init; +}; + +template +struct prepend { + T p_value; +}; + +template +struct append { + T p_value; +}; + +struct nth { + nonstd::optional a_index; +}; + +struct skip { + size_t a_count; +}; + +struct unique {}; + +struct max_value {}; + +template +struct max_with_init { + T m_init; +}; + +struct sum {}; + +} // namespace details + +template +inline details::unwrap_or +unwrap_or(T value) +{ + return details::unwrap_or{ + value, + }; +} + +template +inline details::find_if

+find_if(P predicate) +{ + return details::find_if

{ + predicate, + }; +} + +template +inline details::find +find(T value) +{ + return details::find{ + value, + }; +} + +inline details::first +first() +{ + return details::first{}; +} + +inline details::second +second() +{ + return details::second{}; +} + +inline details::nth +nth(nonstd::optional index) +{ + return details::nth{ + index, + }; +} + +inline details::skip +skip(size_t count) +{ + return details::skip{ + count, + }; +} + +template +inline details::filter_in +filter_in(F func) +{ + return details::filter_in{ + func, + }; +} + +template +inline details::filter_out +filter_out(F func) +{ + return details::filter_out{ + func, + }; +} + +template +inline details::prepend +prepend(T value) +{ + return details::prepend{ + std::move(value), + }; +} + +template +inline details::append +append(T value) +{ + return details::append{ + std::move(value), + }; +} + +template +inline details::sort_by +sort_with(C cmp) +{ + return details::sort_by{cmp}; +} + +template +inline auto +sort_by(T C::*m) +{ + return sort_with( + [m](const C& lhs, const C& rhs) { return lhs.*m < rhs.*m; }); +} + +template +inline details::mapper +map(F func) +{ + return details::mapper{func}; +} + +template +inline details::flat_mapper +flat_map(F func) +{ + return details::flat_mapper{func}; +} + +template +inline details::for_eacher +for_each(F func) +{ + return details::for_eacher{func}; +} + +inline auto +deref() +{ + return map([](auto iter) { return *iter; }); +} + +template +inline details::folder +fold(R func, T init) +{ + return details::folder{func, init}; +} + +inline details::unique +unique() +{ + return details::unique{}; +} + +inline details::sorted +sorted() +{ + return details::sorted{}; +} + +template +T +chain(const T& value1, const Args&... args) +{ + T retval; + + for (const auto& arg : {value1, args...}) { + for (const auto& elem : arg) { + retval.emplace_back(elem); + } + } + + return retval; +} + +inline details::max_value +max() +{ + return details::max_value{}; +} + +template +inline details::max_with_init +max(T init) +{ + return details::max_with_init{init}; +} + +inline details::sum +sum() +{ + return details::sum{}; +} + +} // namespace itertools +} // namespace lnav + +template +nonstd::optional>::value, + typename std::remove_reference_t::const_iterator, + typename std::remove_reference_t::iterator>> +operator|(C&& in, const lnav::itertools::details::find_if

& finder) +{ + for (auto iter = in.begin(); iter != in.end(); ++iter) { + if (lnav::func::invoke(finder.fi_predicate, *iter)) { + return nonstd::make_optional(iter); + } + } + + return nonstd::nullopt; +} + +template +nonstd::optional +operator|(const C& in, const lnav::itertools::details::find& finder) +{ + size_t retval = 0; + for (const auto& elem : in) { + if (elem == finder.f_value) { + return nonstd::make_optional(retval); + } + retval += 1; + } + + return nonstd::nullopt; +} + +template +nonstd::optional +operator|(const C& in, const lnav::itertools::details::nth indexer) +{ + if (!indexer.a_index.has_value()) { + return nonstd::nullopt; + } + + if (indexer.a_index.value() < in.size()) { + auto iter = in.begin(); + + std::advance(iter, indexer.a_index.value()); + return nonstd::make_optional(iter); + } + + return nonstd::nullopt; +} + +template +std::vector +operator|(const C& in, const lnav::itertools::details::first indexer) +{ + std::vector retval; + + for (const auto& pair : in) { + retval.emplace_back(pair.first); + } + + return retval; +} + +template +nonstd::optional +operator|(const C& in, const lnav::itertools::details::max_value maxer) +{ + nonstd::optional retval; + + for (const auto& elem : in) { + if (!retval) { + retval = elem; + continue; + } + + if (elem > retval.value()) { + retval = elem; + } + } + + return retval; +} + +template +typename C::value_type +operator|(const C& in, const lnav::itertools::details::max_with_init maxer) +{ + typename C::value_type retval = (typename C::value_type) maxer.m_init; + + for (const auto& elem : in) { + if (elem > retval) { + retval = elem; + } + } + + return retval; +} + +template +typename C::value_type +operator|(const C& in, const lnav::itertools::details::sum summer) +{ + typename C::value_type retval{0}; + + for (const auto& elem : in) { + retval += elem; + } + + return retval; +} + +template +C +operator|(const C& in, const lnav::itertools::details::skip& skipper) +{ + C retval; + + if (skipper.a_count < in.size()) { + auto iter = in.begin(); + std::advance(iter, skipper.a_count); + for (; iter != in.end(); ++iter) { + retval.emplace_back(*iter); + } + } + + return retval; +} + +template +std::vector +operator|(const std::vector>& in, + const lnav::itertools::details::filter_in& filterer) +{ + std::vector retval; + + for (const auto& elem : in) { + if (lnav::func::invoke(filterer.f_func, elem)) { + retval.emplace_back(elem.get()); + } + } + + return retval; +} + +template +C +operator|(const C& in, const lnav::itertools::details::filter_in& filterer) +{ + C retval; + + for (const auto& elem : in) { + if (lnav::func::invoke(filterer.f_func, elem)) { + retval.emplace_back(elem); + } + } + + return retval; +} + +template +C +operator|(const C& in, const lnav::itertools::details::filter_out& filterer) +{ + C retval; + + for (const auto& elem : in) { + if (!lnav::func::invoke(filterer.f_func, elem)) { + retval.emplace_back(elem); + } + } + + return retval; +} + +template +C +operator|(C in, const lnav::itertools::details::prepend& prepender) +{ + in.emplace(in.begin(), prepender.p_value); + + return in; +} + +template +C +operator|(C in, const lnav::itertools::details::append& appender) +{ + in.emplace_back(appender.p_value); + + return in; +} + +template +T +operator|(const C& in, const lnav::itertools::details::folder& folder) +{ + auto accum = folder.f_init; + + for (const auto& elem : in) { + accum = folder.f_func(elem, accum); + } + + return accum; +} + +template +std::set +operator|(C&& in, const lnav::itertools::details::unique& sorter) +{ + return {in.begin(), in.end()}; +} + +template +T +operator|(T in, const lnav::itertools::details::sort_by& sorter) +{ + std::sort(in.begin(), in.end(), sorter.sb_cmp); + + return in; +} + +template +T +operator|(T in, const lnav::itertools::details::sorted& sorter) +{ + std::sort(in.begin(), in.end()); + + return in; +} + +template::value, int> = 0> +auto +operator|(nonstd::optional in, + const lnav::itertools::details::flat_mapper& mapper) -> + typename std::remove_const_t> +{ + if (!in) { + return nonstd::nullopt; + } + + return lnav::func::invoke(mapper.fm_func, in.value()); +} + +template::value, int> = 0> +void +operator|(nonstd::optional in, + const lnav::itertools::details::for_eacher& eacher) +{ + if (!in) { + return; + } + + lnav::func::invoke(eacher.fe_func, in.value()); +} + +template::value, int> = 0> +void +operator|(std::vector>& in, + const lnav::itertools::details::for_eacher& eacher) +{ + for (auto& elem : in) { + lnav::func::invoke(eacher.fe_func, *elem); + } +} + +template::value, int> = 0> +auto +operator|(nonstd::optional in, + const lnav::itertools::details::mapper& mapper) + -> nonstd::optional< + typename std::remove_const_t>> +{ + if (!in) { + return nonstd::nullopt; + } + + return nonstd::make_optional(lnav::func::invoke(mapper.m_func, in.value())); +} + +template +auto +operator|(const T& in, const lnav::itertools::details::mapper& mapper) + -> std::vector()))>>> +{ + using return_type = std::vector()))>>>; + return_type retval; + + retval.reserve(in.size()); + std::transform( + in.begin(), in.end(), std::back_inserter(retval), mapper.m_func); + + return retval; +} + +template +auto +operator|(const T& in, const lnav::itertools::details::mapper& mapper) + -> std::vector< + std::remove_const_t> +{ + using return_type = std::vector< + std::remove_const_t>; + return_type retval; + + retval.reserve(in.size()); + for (const auto& elem : in) { + retval.template emplace_back((elem.*mapper.m_func)()); + } + + return retval; +} + +template +auto +operator|(const std::vector& in, + const lnav::itertools::details::mapper& mapper) + -> std::vector()).*mapper.m_func)())>>> +{ + using return_type + = std::vector()).*mapper.m_func)())>>>; + return_type retval; + + retval.reserve(in.size()); + std::transform( + in.begin(), + in.end(), + std::back_inserter(retval), + [&mapper](const auto& elem) { return ((*elem).*mapper.m_func)(); }); + + return retval; +} + +template +auto +operator|(const std::set& in, + const lnav::itertools::details::mapper& mapper) + -> std::vector()).*mapper.m_func)())>>> +{ + using return_type + = std::vector()).*mapper.m_func)())>>>; + return_type retval; + + retval.reserve(in.size()); + std::transform( + in.begin(), + in.end(), + std::back_inserter(retval), + [&mapper](const auto& elem) { return ((*elem).*mapper.m_func)(); }); + + return retval; +} + +template::value, int> = 0> +auto +operator|(const std::vector>& in, + const lnav::itertools::details::mapper& mapper) + -> std::vector>> +{ + using return_type = std::vector< + typename std::remove_const_t>>; + return_type retval; + + retval.reserve(in.size()); + for (const auto& elem : in) { + retval.template emplace_back(((*elem).*mapper.m_func)); + } + + return retval; +} + +template::value, int> = 0> +auto +operator|(const std::vector& in, + const lnav::itertools::details::mapper& mapper) + -> std::vector< + typename std::remove_const_t>> +{ + using return_type = std::vector< + typename std::remove_const_t>>; + return_type retval; + + retval.reserve(in.size()); + for (const auto& elem : in) { + retval.template emplace_back(elem.*mapper.m_func); + } + + return retval; +} + +template::value, int> = 0> +auto +operator|(nonstd::optional in, + const lnav::itertools::details::mapper& mapper) + -> nonstd::optional>> +{ + if (!in) { + return nonstd::nullopt; + } + + return nonstd::make_optional((in.value()).*mapper.m_func); +} + +template::value, int> = 0> +auto +operator|(nonstd::optional in, + const lnav::itertools::details::mapper& mapper) + -> nonstd::optional< + typename std::remove_const_t>> +{ + if (!in) { + return nonstd::nullopt; + } + + return nonstd::make_optional((*in.value()).*mapper.m_func); +} + +template +T +operator|(nonstd::optional in, + const lnav::itertools::details::unwrap_or& unwrapper) +{ + return in.value_or(unwrapper.uo_value); +} + +#endif diff --git a/src/base/lnav.console.cc b/src/base/lnav.console.cc new file mode 100644 index 0000000..a34ebac --- /dev/null +++ b/src/base/lnav.console.cc @@ -0,0 +1,494 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "lnav.console.hh" + +#include "config.h" +#include "fmt/color.h" +#include "itertools.hh" +#include "lnav.console.into.hh" +#include "log_level_enum.hh" +#include "pcrepp/pcre2pp.hh" +#include "snippet_highlighters.hh" +#include "view_curses.hh" + +using namespace lnav::roles::literals; + +namespace lnav { +namespace console { + +user_message +user_message::raw(const attr_line_t& al) +{ + user_message retval; + + retval.um_level = level::raw; + retval.um_message.append(al); + return retval; +} + +user_message +user_message::error(const attr_line_t& al) +{ + user_message retval; + + retval.um_level = level::error; + retval.um_message.append(al); + return retval; +} + +user_message +user_message::info(const attr_line_t& al) +{ + user_message retval; + + retval.um_level = level::info; + retval.um_message.append(al); + return retval; +} + +user_message +user_message::ok(const attr_line_t& al) +{ + user_message retval; + + retval.um_level = level::ok; + retval.um_message.append(al); + return retval; +} + +user_message +user_message::warning(const attr_line_t& al) +{ + user_message retval; + + retval.um_level = level::warning; + retval.um_message.append(al); + return retval; +} + +attr_line_t +user_message::to_attr_line(std::set flags) const +{ + auto indent = 1; + attr_line_t retval; + + if (this->um_level == level::warning) { + indent = 3; + } + + if (flags.count(render_flags::prefix)) { + switch (this->um_level) { + case level::raw: + break; + case level::ok: + retval.append(lnav::roles::ok("\u2714 ")); + break; + case level::info: + retval.append("\u24d8 info"_info).append(": "); + break; + case level::warning: + retval.append(lnav::roles::warning("\u26a0 warning")) + .append(": "); + break; + case level::error: + retval.append(lnav::roles::error("\u2718 error")).append(": "); + break; + } + } + + retval.append(this->um_message).append("\n"); + if (!this->um_reason.empty()) { + bool first_line = true; + for (const auto& line : this->um_reason.split_lines()) { + auto role = this->um_level == level::error ? role_t::VCR_ERROR + : role_t::VCR_WARNING; + attr_line_t prefix; + + if (first_line) { + prefix.append(indent, ' ') + .append("reason", VC_ROLE.value(role)) + .append(": "); + first_line = false; + } else { + prefix.append(" | ", VC_ROLE.value(role)) + .append(indent, ' '); + } + retval.append(prefix).append(line).append("\n"); + } + } + if (!this->um_snippets.empty()) { + for (const auto& snip : this->um_snippets) { + attr_line_t header; + + header.append(" --> "_snippet_border) + .append(lnav::roles::file(snip.s_location.sl_source.get())); + if (snip.s_location.sl_line_number > 0) { + header.append(":").appendf(FMT_STRING("{}"), + snip.s_location.sl_line_number); + } + retval.append(header).append("\n"); + if (!snip.s_content.blank()) { + auto snippet_lines = snip.s_content.split_lines(); + auto longest_line_length = snippet_lines + | lnav::itertools::map(&attr_line_t::utf8_length_or_length) + | lnav::itertools::max(40); + + for (auto& line : snippet_lines) { + line.pad_to(longest_line_length); + retval.append(" | "_snippet_border) + .append(line) + .append("\n"); + } + } + } + } + if (!this->um_notes.empty()) { + for (const auto& note : this->um_notes) { + bool first_line = true; + for (const auto& line : note.split_lines()) { + attr_line_t prefix; + + if (first_line) { + prefix.append(" ="_snippet_border) + .append(indent, ' ') + .append("note"_snippet_border) + .append(": "); + first_line = false; + } else { + prefix.append(" ").append(indent, ' '); + } + + retval.append(prefix).append(line).append("\n"); + } + } + } + if (!this->um_help.empty()) { + bool first_line = true; + for (const auto& line : this->um_help.split_lines()) { + attr_line_t prefix; + + if (first_line) { + prefix.append(" ="_snippet_border) + .append(indent, ' ') + .append("help"_snippet_border) + .append(": "); + first_line = false; + } else { + prefix.append(" "); + } + + retval.append(prefix).append(line).append("\n"); + } + } + + return retval; +} + +static nonstd::optional +curses_color_to_terminal_color(int curses_color) +{ + switch (curses_color) { + case COLOR_BLACK: + return fmt::terminal_color::black; + case COLOR_CYAN: + return fmt::terminal_color::cyan; + case COLOR_WHITE: + return fmt::terminal_color::white; + case COLOR_MAGENTA: + return fmt::terminal_color::magenta; + case COLOR_BLUE: + return fmt::terminal_color::blue; + case COLOR_YELLOW: + return fmt::terminal_color::yellow; + case COLOR_GREEN: + return fmt::terminal_color::green; + case COLOR_RED: + return fmt::terminal_color::red; + default: + return nonstd::nullopt; + } +} + +void +println(FILE* file, const attr_line_t& al) +{ + const auto& str = al.get_string(); + + if (getenv("NO_COLOR") != nullptr + || (!isatty(fileno(file)) && getenv("YES_COLOR") == nullptr)) + { + fmt::print(file, "{}\n", str); + return; + } + + std::set points = {0, static_cast(al.length())}; + + for (const auto& attr : al.get_attrs()) { + if (!attr.sa_range.is_valid()) { + continue; + } + points.insert(attr.sa_range.lr_start); + if (attr.sa_range.lr_end > 0) { + points.insert(attr.sa_range.lr_end); + } + } + + nonstd::optional last_point; + for (const auto& point : points) { + if (!last_point) { + last_point = point; + continue; + } + auto default_fg_style = fmt::text_style{}; + auto default_bg_style = fmt::text_style{}; + auto line_style = fmt::text_style{}; + auto fg_style = fmt::text_style{}; + auto start = last_point.value(); + + for (const auto& attr : al.get_attrs()) { + if (!attr.sa_range.contains(start) + && !attr.sa_range.contains(point - 1)) + { + continue; + } + + try { + if (attr.sa_type == &VC_BACKGROUND) { + auto saw = string_attr_wrapper(&attr); + auto color_opt = curses_color_to_terminal_color(saw.get()); + + if (color_opt) { + line_style |= fmt::bg(color_opt.value()); + } + } else if (attr.sa_type == &VC_FOREGROUND) { + auto saw = string_attr_wrapper(&attr); + auto color_opt = curses_color_to_terminal_color(saw.get()); + + if (color_opt) { + fg_style = fmt::fg(color_opt.value()); + } + } else if (attr.sa_type == &VC_STYLE) { + auto saw = string_attr_wrapper(&attr); + auto style = saw.get(); + + if (style.ta_attrs & A_REVERSE) { + line_style |= fmt::emphasis::reverse; + } + if (style.ta_attrs & A_BOLD) { + line_style |= fmt::emphasis::bold; + } + if (style.ta_attrs & A_UNDERLINE) { + line_style |= fmt::emphasis::underline; + } + if (style.ta_fg_color) { + auto color_opt = curses_color_to_terminal_color( + style.ta_fg_color.value()); + + if (color_opt) { + fg_style = fmt::fg(color_opt.value()); + } + } + if (style.ta_bg_color) { + auto color_opt = curses_color_to_terminal_color( + style.ta_bg_color.value()); + + if (color_opt) { + line_style |= fmt::bg(color_opt.value()); + } + } + } else if (attr.sa_type == &SA_LEVEL) { + auto level = static_cast( + attr.sa_value.get()); + + switch (level) { + case LEVEL_FATAL: + case LEVEL_CRITICAL: + case LEVEL_ERROR: + line_style |= fmt::fg(fmt::terminal_color::red); + break; + case LEVEL_WARNING: + line_style |= fmt::fg(fmt::terminal_color::yellow); + break; + default: + break; + } + } else if (attr.sa_type == &VC_ROLE) { + auto saw = string_attr_wrapper(&attr); + auto role = saw.get(); + + switch (role) { + case role_t::VCR_TEXT: + case role_t::VCR_IDENTIFIER: + break; + case role_t::VCR_SEARCH: + line_style |= fmt::emphasis::reverse; + break; + case role_t::VCR_ERROR: + line_style |= fmt::fg(fmt::terminal_color::red) + | fmt::emphasis::bold; + break; + case role_t::VCR_WARNING: + case role_t::VCR_RE_REPEAT: + line_style |= fmt::fg(fmt::terminal_color::yellow); + break; + case role_t::VCR_COMMENT: + line_style |= fmt::fg(fmt::terminal_color::green); + break; + case role_t::VCR_SNIPPET_BORDER: + line_style |= fmt::fg(fmt::terminal_color::cyan); + break; + case role_t::VCR_OK: + line_style |= fmt::emphasis::bold + | fmt::fg(fmt::terminal_color::green); + break; + case role_t::VCR_INFO: + case role_t::VCR_STATUS: + line_style |= fmt::emphasis::bold + | fmt::fg(fmt::terminal_color::magenta); + break; + case role_t::VCR_KEYWORD: + case role_t::VCR_RE_SPECIAL: + line_style |= fmt::emphasis::bold + | fmt::fg(fmt::terminal_color::cyan); + break; + case role_t::VCR_STRING: + line_style |= fmt::fg(fmt::terminal_color::magenta); + break; + case role_t::VCR_VARIABLE: + line_style |= fmt::emphasis::underline; + break; + case role_t::VCR_SYMBOL: + case role_t::VCR_NUMBER: + case role_t::VCR_FILE: + line_style |= fmt::emphasis::bold; + break; + case role_t::VCR_H1: + line_style |= fmt::emphasis::bold + | fmt::fg(fmt::terminal_color::magenta); + break; + case role_t::VCR_H2: + line_style |= fmt::emphasis::bold; + break; + case role_t::VCR_H3: + case role_t::VCR_H4: + case role_t::VCR_H5: + case role_t::VCR_H6: + line_style |= fmt::emphasis::underline; + break; + case role_t::VCR_LIST_GLYPH: + line_style |= fmt::fg(fmt::terminal_color::yellow); + break; + case role_t::VCR_QUOTED_CODE: + default_fg_style + = fmt::fg(fmt::terminal_color::white); + default_bg_style + = fmt::bg(fmt::terminal_color::black); + break; + case role_t::VCR_LOW_THRESHOLD: + line_style |= fmt::bg(fmt::terminal_color::green); + break; + case role_t::VCR_MED_THRESHOLD: + line_style |= fmt::bg(fmt::terminal_color::yellow); + break; + case role_t::VCR_HIGH_THRESHOLD: + line_style |= fmt::bg(fmt::terminal_color::red); + break; + default: + // log_debug("missing role handler %d", (int) role); + break; + } + } + } catch (const fmt::format_error& e) { + log_error("style error: %s", e.what()); + } + } + + if (!line_style.has_foreground() && fg_style.has_foreground()) { + line_style |= fg_style; + } + if (!line_style.has_foreground() && default_fg_style.has_foreground()) { + line_style |= default_fg_style; + } + if (!line_style.has_background() && default_bg_style.has_background()) { + line_style |= default_bg_style; + } + + if (start < str.size()) { + auto actual_end = std::min(str.size(), static_cast(point)); + fmt::print(file, + line_style, + FMT_STRING("{}"), + str.substr(start, actual_end - start)); + } + last_point = point; + } + fmt::print(file, "\n"); +} + +void +print(FILE* file, const user_message& um) +{ + auto al = um.to_attr_line(); + + if (endswith(al.get_string(), "\n")) { + al.erase(al.length() - 1); + } + println(file, al); +} + +user_message +to_user_message(intern_string_t src, const lnav::pcre2pp::compile_error& ce) +{ + attr_line_t pcre_error_content{ce.ce_pattern}; + + lnav::snippets::regex_highlighter(pcre_error_content, + pcre_error_content.length(), + line_range{ + 0, + (int) pcre_error_content.length(), + }); + pcre_error_content.append("\n") + .append(ce.ce_offset, ' ') + .append(lnav::roles::error("^ ")) + .append(lnav::roles::error(ce.get_message())) + .with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE)); + + return user_message::error( + attr_line_t() + .append_quoted(ce.ce_pattern) + .append(" is not a valid regular expression")) + .with_reason(ce.get_message()) + .with_snippet(lnav::console::snippet::from(src, pcre_error_content)); +} + +} // namespace console +} // namespace lnav diff --git a/src/base/lnav.console.hh b/src/base/lnav.console.hh new file mode 100644 index 0000000..ac4c2b0 --- /dev/null +++ b/src/base/lnav.console.hh @@ -0,0 +1,176 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_console_hh +#define lnav_console_hh + +#include +#include + +#include "base/attr_line.hh" +#include "base/file_range.hh" + +namespace lnav { +namespace console { + +void println(FILE* file, const attr_line_t& al); + +struct snippet { + static snippet from(intern_string_t src, const attr_line_t& content) + { + snippet retval; + + retval.s_location.sl_source = src; + retval.s_content = content; + return retval; + } + + static snippet from(source_location loc, const attr_line_t& content) + { + snippet retval; + + retval.s_location = loc; + retval.s_content = content; + return retval; + } + + snippet& with_line(int32_t line) + { + this->s_location.sl_line_number = line; + return *this; + } + + source_location s_location; + attr_line_t s_content; +}; + +struct user_message { + enum class level { + raw, + ok, + info, + warning, + error, + }; + + static user_message raw(const attr_line_t& al); + + static user_message error(const attr_line_t& al); + + static user_message warning(const attr_line_t& al); + + static user_message info(const attr_line_t& al); + + static user_message ok(const attr_line_t& al); + + user_message& with_reason(const attr_line_t& al) + { + this->um_reason = al; + this->um_reason.rtrim(); + return *this; + } + + user_message& with_reason(const user_message& um) + { + return this->with_reason(um.to_attr_line({})); + } + + user_message& with_errno_reason() + { + this->um_reason = strerror(errno); + return *this; + } + + user_message& with_snippet(const snippet& sn) + { + this->um_snippets.emplace_back(sn); + return *this; + } + + template + user_message& with_snippets(C snippets) + { + this->um_snippets.insert(this->um_snippets.end(), + std::make_move_iterator(std::begin(snippets)), + std::make_move_iterator(std::end(snippets))); + if (this->um_snippets.size() > 1) { + for (auto iter = this->um_snippets.begin(); + iter != this->um_snippets.end();) { + if (iter->s_content.empty()) { + iter = this->um_snippets.erase(iter); + } else { + ++iter; + } + } + } + return *this; + } + + user_message& with_note(const attr_line_t& al) + { + if (!al.blank()) { + this->um_notes.emplace_back(al); + } + + return *this; + } + + user_message& with_help(const attr_line_t& al) + { + if (al.blank()) { + this->um_help.clear(); + } else { + this->um_help = al; + this->um_help.rtrim(); + } + + return *this; + } + + enum class render_flags { + prefix, + }; + + attr_line_t to_attr_line(std::set flags + = {render_flags::prefix}) const; + + level um_level{level::ok}; + attr_line_t um_message; + std::vector um_snippets; + attr_line_t um_reason; + std::vector um_notes; + attr_line_t um_help; +}; + +void print(FILE* file, const user_message& um); + +} // namespace console +} // namespace lnav + +#endif diff --git a/src/base/lnav.console.into.hh b/src/base/lnav.console.into.hh new file mode 100644 index 0000000..206d563 --- /dev/null +++ b/src/base/lnav.console.into.hh @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_console_into_hh +#define lnav_console_into_hh + +#include "intern_string.hh" +#include "lnav.console.hh" + +namespace lnav { +namespace pcre2pp { + +struct compile_error; + +} + +namespace console { + +user_message to_user_message(intern_string_t src, + const pcre2pp::compile_error& ce); + +} +} // namespace lnav + +#endif diff --git a/src/base/lnav.gzip.cc b/src/base/lnav.gzip.cc new file mode 100644 index 0000000..6b31dad --- /dev/null +++ b/src/base/lnav.gzip.cc @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file lnav.gzip.cc + */ + +#include "lnav.gzip.hh" + +#include + +#include "config.h" +#include "fmt/format.h" + +namespace lnav { +namespace gzip { + +bool +is_gzipped(const char* buffer, size_t len) +{ + return len > 2 && buffer[0] == '\037' && buffer[1] == '\213'; +} + +Result +compress(const void* input, size_t len) +{ + auto retval = auto_buffer::alloc(len + 4096); + + z_stream zs; + zs.zalloc = Z_NULL; + zs.zfree = Z_NULL; + zs.opaque = Z_NULL; + zs.avail_in = (uInt) len; + zs.next_in = (Bytef*) input; + zs.avail_out = (uInt) retval.capacity(); + zs.next_out = (Bytef*) retval.in(); + zs.total_out = 0; + + auto rc = deflateInit2( + &zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY); + if (rc != Z_OK) { + return Err(fmt::format( + FMT_STRING("unable to initialize compressor -- {}"), zError(rc))); + } + rc = deflate(&zs, Z_FINISH); + if (rc != Z_STREAM_END) { + return Err(fmt::format(FMT_STRING("unable to compress data -- {}"), + zError(rc))); + } + rc = deflateEnd(&zs); + if (rc != Z_OK) { + return Err(fmt::format( + FMT_STRING("unable to finalize compression -- {}"), zError(rc))); + } + return Ok(std::move(retval.resize(zs.total_out))); +} + +Result +uncompress(const std::string& src, const void* buffer, size_t size) +{ + auto uncomp = auto_buffer::alloc(size * 2); + z_stream strm; + int err; + + strm.next_in = (Bytef*) buffer; + strm.msg = Z_NULL; + strm.avail_in = size; + strm.total_in = 0; + strm.total_out = 0; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + + if ((err = inflateInit2(&strm, (16 + MAX_WBITS))) != Z_OK) { + return Err(fmt::format(FMT_STRING("invalid gzip data: {} -- {}"), + src, + strm.msg ? strm.msg : zError(err))); + } + + bool done = false; + + while (!done) { + if (strm.total_out >= uncomp.size()) { + uncomp.expand_by(size / 2); + } + + strm.next_out = (Bytef*) (uncomp.in() + strm.total_out); + strm.avail_out = uncomp.capacity() - strm.total_out; + + // Inflate another chunk. + err = inflate(&strm, Z_SYNC_FLUSH); + if (err == Z_STREAM_END) { + done = true; + } else if (err != Z_OK) { + inflateEnd(&strm); + return Err(fmt::format(FMT_STRING("unable to uncompress: {} -- {}"), + src, + strm.msg ? strm.msg : zError(err))); + } + } + + if (inflateEnd(&strm) != Z_OK) { + return Err(fmt::format(FMT_STRING("unable to uncompress: {} -- {}"), + src, + strm.msg ? strm.msg : zError(err))); + } + + return Ok(std::move(uncomp.resize(strm.total_out))); +} + +} // namespace gzip +} // namespace lnav diff --git a/src/base/lnav.gzip.hh b/src/base/lnav.gzip.hh new file mode 100644 index 0000000..bd73965 --- /dev/null +++ b/src/base/lnav.gzip.hh @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file lnav.gzip.hh + */ + +#ifndef lnav_gzip_hh +#define lnav_gzip_hh + +#include + +#include "auto_mem.hh" +#include "result.h" + +namespace lnav { +namespace gzip { + +bool is_gzipped(const char* buffer, size_t len); + +Result compress(const void* input, size_t len); + +Result uncompress(const std::string& src, + const void* buffer, + size_t size); + +} // namespace gzip +} // namespace lnav + +#endif diff --git a/src/base/lnav.gzip.tests.cc b/src/base/lnav.gzip.tests.cc new file mode 100644 index 0000000..2f6939e --- /dev/null +++ b/src/base/lnav.gzip.tests.cc @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include + +#include "base/lnav.gzip.hh" +#include "config.h" +#include "doctest/doctest.h" + +TEST_CASE("lnav::gzip::uncompress") +{ + { + auto u_res = lnav::gzip::uncompress("empty", nullptr, 0); + + CHECK(u_res.isErr()); + CHECK(u_res.unwrapErr() + == "unable to uncompress: empty -- stream error"); + } + + { + auto u_res = lnav::gzip::uncompress("garbage", "abc", 3); + + CHECK(u_res.isErr()); + CHECK(u_res.unwrapErr() + == "unable to uncompress: garbage -- incorrect header check"); + } +} + +TEST_CASE("lnav::gzip::roundtrip") +{ + const char msg[] = "Hello, World!"; + + auto c_res = lnav::gzip::compress(msg, sizeof(msg)); + auto buf = c_res.unwrap(); + auto u_res = lnav::gzip::uncompress("test", buf.in(), buf.size()); + auto buf2 = u_res.unwrap(); + + CHECK(std::string(msg) == std::string(buf2.in())); +} diff --git a/src/base/lnav_log.cc b/src/base/lnav_log.cc new file mode 100644 index 0000000..2e5dbba --- /dev/null +++ b/src/base/lnav_log.cc @@ -0,0 +1,686 @@ +/** + * Copyright (c) 2014, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file lnav_log.cc + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "config.h" + +#ifdef HAVE_EXECINFO_H +# include +#endif +#if BACKWARD_HAS_DW == 1 +# include "backward-cpp/backward.hpp" +#endif + +#include +#include +#include +#include + +#define PCRE2_CODE_UNIT_WIDTH 8 +#include +#include +#include +#include +#include +#include + +#if defined HAVE_NCURSESW_CURSES_H +# include +# include +#elif defined HAVE_NCURSESW_H +# include +# include +#elif defined HAVE_NCURSES_CURSES_H +# include +# include +#elif defined HAVE_NCURSES_H +# include +# include +#elif defined HAVE_CURSES_H +# include +# include +#else +# error "SysV or X/Open-compatible Curses header file required" +#endif + +#include "auto_mem.hh" +#include "enum_util.hh" +#include "lnav_log.hh" +#include "opt_util.hh" + +static const size_t BUFFER_SIZE = 256 * 1024; +static const size_t MAX_LOG_LINE_SIZE = 2 * 1024; + +static const char* CRASH_MSG + = "\n" + "\n" + "==== GURU MEDITATION ====\n" + "Unfortunately, lnav has crashed, sorry for the inconvenience.\n" + "\n" + "You can help improve lnav by sending the following file " + "to " PACKAGE_BUGREPORT + " :\n" + " %s\n" + "=========================\n"; + +nonstd::optional lnav_log_file; +lnav_log_level_t lnav_log_level = lnav_log_level_t::DEBUG; +const char* lnav_log_crash_dir; +nonstd::optional lnav_log_orig_termios; +// NOTE: This mutex is leaked so that it is not destroyed during exit. +// Otherwise, any attempts to log will fail. +static std::mutex* +lnav_log_mutex() +{ + static auto* retval = new std::mutex(); + + return retval; +} + +static std::vector& +DUMPER_LIST() +{ + static auto* retval = new std::vector(); + + return *retval; +} +static std::vector CRASH_LIST; + +struct thid { + static uint32_t COUNTER; + + thid() noexcept : t_id(COUNTER++) {} + + uint32_t t_id; +}; + +uint32_t thid::COUNTER = 0; + +thread_local thid current_thid; +thread_local std::string thread_log_prefix; + +static struct { + size_t lr_length; + off_t lr_frag_start; + off_t lr_frag_end; + char lr_data[BUFFER_SIZE]; +} log_ring = {0, BUFFER_SIZE, 0, {}}; + +static const char* LEVEL_NAMES[] = { + "T", + "D", + "I", + "W", + "E", +}; + +static char* +log_alloc() +{ + off_t data_end = log_ring.lr_length + MAX_LOG_LINE_SIZE; + + if (data_end >= (off_t) BUFFER_SIZE) { + const char* new_start = &log_ring.lr_data[MAX_LOG_LINE_SIZE]; + + new_start = (const char*) memchr( + new_start, '\n', log_ring.lr_length - MAX_LOG_LINE_SIZE); + log_ring.lr_frag_start = new_start - log_ring.lr_data; + log_ring.lr_frag_end = log_ring.lr_length; + log_ring.lr_length = 0; + + assert(log_ring.lr_frag_start >= 0); + assert(log_ring.lr_frag_start <= (off_t) BUFFER_SIZE); + } else if (data_end >= log_ring.lr_frag_start) { + const char* new_start = &log_ring.lr_data[log_ring.lr_frag_start]; + + new_start = (const char*) memchr( + new_start, '\n', log_ring.lr_frag_end - log_ring.lr_frag_start); + assert(new_start != nullptr); + log_ring.lr_frag_start = new_start - log_ring.lr_data; + assert(log_ring.lr_frag_start >= 0); + assert(log_ring.lr_frag_start <= (off_t) BUFFER_SIZE); + } + + return &log_ring.lr_data[log_ring.lr_length]; +} + +void +log_argv(int argc, char* argv[]) +{ + const char* log_path = getenv("LNAV_LOG_PATH"); + + if (log_path != nullptr) { + lnav_log_file = make_optional_from_nullable(fopen(log_path, "a")); + } + + log_info("argv[%d] =", argc); + for (int lpc = 0; lpc < argc; lpc++) { + log_info(" [%d] = %s", lpc, argv[lpc]); + } +} + +void +log_set_thread_prefix(std::string prefix) +{ + // thread_log_prefix = std::move(prefix); +} + +void +log_host_info() +{ + char cwd[MAXPATHLEN]; + char jittarget[128]; + struct utsname un; + struct rusage ru; + uint32_t pcre_jit; + + uname(&un); + pcre2_config(PCRE2_CONFIG_JIT, &pcre_jit); + pcre2_config(PCRE2_CONFIG_JITTARGET, jittarget); + + log_info("uname:"); + log_info(" sysname=%s", un.sysname); + log_info(" nodename=%s", un.nodename); + log_info(" machine=%s", un.machine); + log_info(" release=%s", un.release); + log_info(" version=%s", un.version); + log_info("PCRE:"); + log_info(" jit=%d", pcre_jit); + log_info(" jittarget=%s", jittarget); + log_info("Environment:"); + log_info(" HOME=%s", getenv("HOME")); + log_info(" XDG_CONFIG_HOME=%s", getenv("XDG_CONFIG_HOME")); + log_info(" LANG=%s", getenv("LANG")); + log_info(" PATH=%s", getenv("PATH")); + log_info(" TERM=%s", getenv("TERM")); + log_info(" TZ=%s", getenv("TZ")); + log_info("Process:"); + log_info(" pid=%d", getpid()); + log_info(" ppid=%d", getppid()); + log_info(" pgrp=%d", getpgrp()); + log_info(" uid=%d", getuid()); + log_info(" gid=%d", getgid()); + log_info(" euid=%d", geteuid()); + log_info(" egid=%d", getegid()); + if (getcwd(cwd, sizeof(cwd)) == nullptr) { + log_info(" ERROR: getcwd failed"); + } else { + log_info(" cwd=%s", cwd); + } + log_info("Executable:"); + log_info(" version=%s", VCS_PACKAGE_STRING); + + getrusage(RUSAGE_SELF, &ru); + log_rusage(lnav_log_level_t::INFO, ru); +} + +void +log_rusage_raw(enum lnav_log_level_t level, + const char* src_file, + int line_number, + const struct rusage& ru) +{ + log_msg(level, src_file, line_number, "rusage:"); + log_msg(level, + src_file, + line_number, + " utime=%d.%06d", + ru.ru_utime.tv_sec, + ru.ru_utime.tv_usec); + log_msg(level, + src_file, + line_number, + " stime=%d.%06d", + ru.ru_stime.tv_sec, + ru.ru_stime.tv_usec); + log_msg(level, src_file, line_number, " maxrss=%ld", ru.ru_maxrss); + log_msg(level, src_file, line_number, " ixrss=%ld", ru.ru_ixrss); + log_msg(level, src_file, line_number, " idrss=%ld", ru.ru_idrss); + log_msg(level, src_file, line_number, " isrss=%ld", ru.ru_isrss); + log_msg(level, src_file, line_number, " minflt=%ld", ru.ru_minflt); + log_msg(level, src_file, line_number, " majflt=%ld", ru.ru_majflt); + log_msg(level, src_file, line_number, " nswap=%ld", ru.ru_nswap); + log_msg(level, src_file, line_number, " inblock=%ld", ru.ru_inblock); + log_msg(level, src_file, line_number, " oublock=%ld", ru.ru_oublock); + log_msg(level, src_file, line_number, " msgsnd=%ld", ru.ru_msgsnd); + log_msg(level, src_file, line_number, " msgrcv=%ld", ru.ru_msgrcv); + log_msg(level, src_file, line_number, " nsignals=%ld", ru.ru_nsignals); + log_msg(level, src_file, line_number, " nvcsw=%ld", ru.ru_nvcsw); + log_msg(level, src_file, line_number, " nivcsw=%ld", ru.ru_nivcsw); +} + +void +log_msg(lnav_log_level_t level, + const char* src_file, + int line_number, + const char* fmt, + ...) +{ + struct timeval curr_time; + struct tm localtm; + ssize_t prefix_size; + va_list args; + ssize_t rc; + + if (level < lnav_log_level) { + return; + } + + std::lock_guard log_lock(*lnav_log_mutex()); + + { + // get the base name of the file. NB: can't use basename() since it + // can modify its argument + const char* last_slash = src_file; + + for (int lpc = 0; src_file[lpc]; lpc++) { + if (src_file[lpc] == '/' || src_file[lpc] == '\\') { + last_slash = &src_file[lpc + 1]; + } + } + + src_file = last_slash; + } + + va_start(args, fmt); + gettimeofday(&curr_time, nullptr); + localtime_r(&curr_time.tv_sec, &localtm); + auto line = log_alloc(); + prefix_size = snprintf(line, + MAX_LOG_LINE_SIZE, + "%4d-%02d-%02dT%02d:%02d:%02d.%03d %s t%u %s:%d ", + localtm.tm_year + 1900, + localtm.tm_mon + 1, + localtm.tm_mday, + localtm.tm_hour, + localtm.tm_min, + localtm.tm_sec, + (int) (curr_time.tv_usec / 1000), + LEVEL_NAMES[lnav::enums::to_underlying(level)], + current_thid.t_id, + src_file, + line_number); +#if 0 + if (!thread_log_prefix.empty()) { + prefix_size += snprintf( + &line[prefix_size], MAX_LOG_LINE_SIZE - prefix_size, + "%s ", + thread_log_prefix.c_str()); + } +#endif + rc = vsnprintf( + &line[prefix_size], MAX_LOG_LINE_SIZE - prefix_size, fmt, args); + if (rc >= (ssize_t) (MAX_LOG_LINE_SIZE - prefix_size)) { + rc = MAX_LOG_LINE_SIZE - prefix_size - 1; + } + line[prefix_size + rc] = '\n'; + log_ring.lr_length += prefix_size + rc + 1; + lnav_log_file | [&](auto file) { + fwrite(line, 1, prefix_size + rc + 1, file); + fflush(file); + }; + va_end(args); +} + +void +log_msg_extra(const char* fmt, ...) +{ + std::lock_guard mg(*lnav_log_mutex()); + va_list args; + + va_start(args, fmt); + auto line = log_alloc(); + auto rc = vsnprintf(line, MAX_LOG_LINE_SIZE - 1, fmt, args); + log_ring.lr_length += rc; + lnav_log_file | [&](auto file) { + fwrite(line, 1, rc, file); + fflush(file); + }; + va_end(args); +} + +void +log_msg_extra_complete() +{ + std::lock_guard mg(*lnav_log_mutex()); + auto line = log_alloc(); + line[0] = '\n'; + log_ring.lr_length += 1; + lnav_log_file | [&](auto file) { + fwrite(line, 1, 1, file); + fflush(file); + }; +} + +void +log_backtrace(lnav_log_level_t level) +{ +#ifdef HAVE_EXECINFO_H + int frame_count; + void* frames[128]; + + frame_count = backtrace(frames, 128); + auto bt = backtrace_symbols(frames, frame_count); + for (int lpc = 0; lpc < frame_count; lpc++) { + log_msg(level, __FILE__, __LINE__, "%s", bt[lpc]); + } +#endif +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-result" +static void +sigabrt(int sig, siginfo_t* info, void* ctx) +{ + char crash_path[1024], latest_crash_path[1024]; + int fd; +#ifdef HAVE_EXECINFO_H + int frame_count; + void* frames[128]; +#endif + struct tm localtm; + time_t curr_time; + + if (lnav_log_crash_dir == nullptr) { + printf("%*s", (int) log_ring.lr_length, log_ring.lr_data); + return; + } + + log_error("Received signal: %d", sig); + +#ifdef HAVE_EXECINFO_H + frame_count = backtrace(frames, 128); +#endif + curr_time = time(nullptr); + localtime_r(&curr_time, &localtm); + snprintf(crash_path, + sizeof(crash_path), + "%s/crash-%4d-%02d-%02d-%02d-%02d-%02d.%d.log", + lnav_log_crash_dir, + localtm.tm_year + 1900, + localtm.tm_mon + 1, + localtm.tm_mday, + localtm.tm_hour, + localtm.tm_min, + localtm.tm_sec, + getpid()); + snprintf(latest_crash_path, + sizeof(latest_crash_path), + "%s/latest-crash.log", + lnav_log_crash_dir); + if ((fd = open(crash_path, O_CREAT | O_TRUNC | O_RDWR, 0600)) != -1) { + if (log_ring.lr_frag_start < (off_t) BUFFER_SIZE) { + (void) write(fd, + &log_ring.lr_data[log_ring.lr_frag_start], + log_ring.lr_frag_end - log_ring.lr_frag_start); + } + (void) write(fd, log_ring.lr_data, log_ring.lr_length); +#ifdef HAVE_EXECINFO_H + backtrace_symbols_fd(frames, frame_count, fd); +#endif +#if BACKWARD_HAS_DW == 1 + { + ucontext_t* uctx = static_cast(ctx); + void* error_addr = nullptr; + +# ifdef REG_RIP // x86_64 + error_addr + = reinterpret_cast(uctx->uc_mcontext.gregs[REG_RIP]); +# elif defined(REG_EIP) // x86_32 + error_addr + = reinterpret_cast(uctx->uc_mcontext.gregs[REG_EIP]); +# endif + + backward::StackTrace st; + + if (error_addr) { + st.load_from(error_addr, + 32, + reinterpret_cast(uctx), + info->si_addr); + } else { + st.load_here(32, reinterpret_cast(uctx), info->si_addr); + } + backward::TraceResolver tr; + + tr.load_stacktrace(st); + for (size_t lpc = 0; lpc < st.size(); lpc++) { + auto trace = tr.resolve(st[lpc]); + char buf[1024]; + + snprintf(buf, + sizeof(buf), + "Frame %lu:%s:%s (%s:%d)\n", + lpc, + trace.object_filename.c_str(), + trace.object_function.c_str(), + trace.source.filename.c_str(), + trace.source.line); + write(fd, buf, strlen(buf)); + } + } +#endif + log_ring.lr_length = 0; + log_ring.lr_frag_start = BUFFER_SIZE; + log_ring.lr_frag_end = 0; + + log_host_info(); + + for (auto lsd : DUMPER_LIST()) { + lsd->log_state(); + } + + if (log_ring.lr_frag_start < (off_t) BUFFER_SIZE) { + write(fd, + &log_ring.lr_data[log_ring.lr_frag_start], + log_ring.lr_frag_end - log_ring.lr_frag_start); + } + write(fd, log_ring.lr_data, log_ring.lr_length); + if (getenv("DUMP_CRASH") != nullptr) { + char buffer[1024]; + int rc; + + lseek(fd, 0, SEEK_SET); + while ((rc = read(fd, buffer, sizeof(buffer))) > 0) { + write(STDERR_FILENO, buffer, rc); + } + } + close(fd); + + remove(latest_crash_path); + symlink(crash_path, latest_crash_path); + } + + lnav_log_orig_termios | [](auto termios) { + for (auto lcr : CRASH_LIST) { + lcr->log_crash_recover(); + } + + tcsetattr(STDOUT_FILENO, TCSAFLUSH, termios); + dup2(STDOUT_FILENO, STDERR_FILENO); + }; + fprintf(stderr, CRASH_MSG, crash_path); + +#ifndef ATTACH_ON_SIGNAL + if (isatty(STDIN_FILENO)) { + char response; + + fprintf(stderr, "\nWould you like to attach a debugger? (y/N) "); + fflush(stderr); + + if (scanf("%c", &response) > 0 && tolower(response) == 'y') { + pid_t lnav_pid = getpid(); + pid_t child_pid; + + switch ((child_pid = fork())) { + case 0: { + char pid_str[32]; + + snprintf(pid_str, sizeof(pid_str), "--pid=%d", lnav_pid); + execlp("gdb", "gdb", pid_str, nullptr); + + snprintf(pid_str, sizeof(pid_str), "%d", lnav_pid); + execlp("lldb", "lldb", "--attach-pid", pid_str, nullptr); + + fprintf(stderr, "Could not attach gdb or lldb, exiting.\n"); + _exit(1); + break; + } + + case -1: { + break; + } + + default: { + int status; + + while (wait(&status) < 0) { + } + break; + } + } + } + } +#endif + + _exit(1); +} +#pragma GCC diagnostic pop + +void +log_install_handlers() +{ + const size_t stack_size = 8 * 1024 * 1024; + const int sigs[] = { + SIGABRT, + SIGSEGV, + SIGBUS, + SIGILL, + SIGFPE, + }; + static auto_mem stack_content; + + stack_t ss; + + stack_content = malloc(stack_size); + ss.ss_sp = stack_content; + ss.ss_size = stack_size; + ss.ss_flags = 0; + sigaltstack(&ss, nullptr); + for (const auto sig : sigs) { + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER | SA_RESETHAND; + sigfillset(&sa.sa_mask); + sigdelset(&sa.sa_mask, sig); + sa.sa_sigaction = sigabrt; + + sigaction(sig, &sa, nullptr); + } +} + +void +log_abort() +{ + raise(SIGABRT); + _exit(1); +} + +void +log_pipe_err(int fd) +{ + std::thread reader([fd]() { + char buffer[1024]; + bool done = false; + + while (!done) { + int rc = read(fd, buffer, sizeof(buffer)); + + switch (rc) { + case -1: + case 0: + done = true; + break; + default: + while (buffer[rc - 1] == '\n' || buffer[rc - 1] == '\r') { + rc -= 1; + } + + log_error("%.*s", rc, buffer); + break; + } + } + + close(fd); + }); + + reader.detach(); +} + +log_state_dumper::log_state_dumper() +{ + DUMPER_LIST().push_back(this); +} + +log_state_dumper::~log_state_dumper() +{ + auto iter = std::find(DUMPER_LIST().begin(), DUMPER_LIST().end(), this); + if (iter != DUMPER_LIST().end()) { + DUMPER_LIST().erase(iter); + } +} + +log_crash_recoverer::log_crash_recoverer() +{ + CRASH_LIST.push_back(this); +} + +log_crash_recoverer::~log_crash_recoverer() +{ + auto iter = std::find(CRASH_LIST.begin(), CRASH_LIST.end(), this); + + if (iter != CRASH_LIST.end()) { + CRASH_LIST.erase(iter); + } +} diff --git a/src/base/lnav_log.hh b/src/base/lnav_log.hh new file mode 100644 index 0000000..551b4f8 --- /dev/null +++ b/src/base/lnav_log.hh @@ -0,0 +1,188 @@ +/** + * Copyright (c) 2014, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file lnav_log.hh + */ + +#ifndef lnav_log_hh +#define lnav_log_hh + +#include +#include + +#include +#include +#include + +#ifndef lnav_dead2 +# define lnav_dead2 __attribute__((noreturn)) +#endif + +#include "optional.hpp" + +struct termios; + +enum class lnav_log_level_t : uint32_t { + TRACE, + DEBUG, + INFO, + WARNING, + ERROR, +}; + +void log_argv(int argc, char* argv[]); +void log_host_info(); +void log_rusage_raw(enum lnav_log_level_t level, + const char* src_file, + int line_number, + const struct rusage& ru); +void log_msg(enum lnav_log_level_t level, + const char* src_file, + int line_number, + const char* fmt, + ...); +void log_msg_extra(const char* fmt, ...); +void log_msg_extra_complete(); +void log_install_handlers(); +void log_abort() lnav_dead2; +void log_pipe_err(int fd); +void log_set_thread_prefix(std::string prefix); +void log_backtrace(lnav_log_level_t level); + +struct log_state_dumper { +public: + log_state_dumper(); + + virtual ~log_state_dumper(); + + virtual void log_state(){ + + }; + + log_state_dumper(const log_state_dumper&) = delete; + log_state_dumper& operator=(const log_state_dumper&) = delete; +}; + +struct log_crash_recoverer { +public: + log_crash_recoverer(); + + virtual ~log_crash_recoverer(); + + virtual void log_crash_recover() = 0; +}; + +extern nonstd::optional lnav_log_file; +extern const char* lnav_log_crash_dir; +extern nonstd::optional lnav_log_orig_termios; +extern enum lnav_log_level_t lnav_log_level; + +#define log_msg_wrapper(level, fmt...) \ + do { \ + if (lnav_log_level <= level) { \ + log_msg(level, __FILE__, __LINE__, fmt); \ + } \ + } while (false) + +#define log_rusage(level, ru) log_rusage_raw(level, __FILE__, __LINE__, ru); + +#define log_error(fmt...) log_msg_wrapper(lnav_log_level_t::ERROR, fmt); + +#define log_warning(fmt...) log_msg_wrapper(lnav_log_level_t::WARNING, fmt); + +#define log_info(fmt...) log_msg_wrapper(lnav_log_level_t::INFO, fmt); + +#define log_debug(fmt...) log_msg_wrapper(lnav_log_level_t::DEBUG, fmt); + +#define log_trace(fmt...) log_msg_wrapper(lnav_log_level_t::TRACE, fmt); + +#define require(e) ((void) ((e) ? 0 : lnav_require(#e, __FILE__, __LINE__))) +#define lnav_require(e, file, line) \ + (log_msg( \ + lnav_log_level_t::ERROR, file, line, "failed precondition `%s'", e), \ + log_abort(), \ + 1) + +#define require_true(lhs) \ + ((void) ((lhs) ? 0 : lnav_require_unary(#lhs, lhs, __FILE__, __LINE__))) +#define require_false(lhs) \ + ((void) ((!lhs) ? 0 : lnav_require_unary(#lhs, lhs, __FILE__, __LINE__))) +#define lnav_require_unary(e, lhs, file, line) \ + (log_msg(lnav_log_level_t::ERROR, \ + file, \ + line, \ + "failed precondition `%s' (lhs=%s)", \ + e, \ + std::to_string(lhs).c_str()), \ + log_abort(), \ + 1) + +#define require_ge(lhs, rhs) \ + ((void) ((lhs >= rhs) \ + ? 0 \ + : lnav_require_binary( \ + #lhs " >= " #rhs, lhs, rhs, __FILE__, __LINE__))) +#define require_gt(lhs, rhs) \ + ((void) ((lhs > rhs) ? 0 \ + : lnav_require_binary( \ + #lhs " > " #rhs, lhs, rhs, __FILE__, __LINE__))) +#define require_lt(lhs, rhs) \ + ((void) ((lhs < rhs) ? 0 \ + : lnav_require_binary( \ + #lhs " < " #rhs, lhs, rhs, __FILE__, __LINE__))) + +#define lnav_require_binary(e, lhs, rhs, file, line) \ + (log_msg(lnav_log_level_t::ERROR, \ + file, \ + line, \ + "failed precondition `%s' (lhs=%s; rhs=%s)", \ + e, \ + std::to_string(lhs).c_str(), \ + std::to_string(rhs).c_str()), \ + log_abort(), \ + 1) + +#define ensure(e) ((void) ((e) ? 0 : lnav_ensure(#e, __FILE__, __LINE__))) +#define lnav_ensure(e, file, line) \ + (log_msg( \ + lnav_log_level_t::ERROR, file, line, "failed postcondition `%s'", e), \ + log_abort(), \ + 1) + +#define log_perror(e) \ + ((void) ((e != -1) ? 0 : lnav_log_perror(#e, __FILE__, __LINE__))) +#define lnav_log_perror(e, file, line) \ + (log_msg(lnav_log_level_t::ERROR, \ + file, \ + line, \ + "syscall failed `%s' -- %s", \ + e, \ + strerror(errno)), \ + 1) + +#endif diff --git a/src/base/log_level_enum.hh b/src/base/log_level_enum.hh new file mode 100644 index 0000000..983fd5c --- /dev/null +++ b/src/base/log_level_enum.hh @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_log_level_enum_hh +#define lnav_log_level_enum_hh + +/** + * The logging level identifiers for a line(s). + */ +enum log_level_t : int { + LEVEL_UNKNOWN, + LEVEL_TRACE, + LEVEL_DEBUG5, + LEVEL_DEBUG4, + LEVEL_DEBUG3, + LEVEL_DEBUG2, + LEVEL_DEBUG, + LEVEL_INFO, + LEVEL_STATS, + LEVEL_NOTICE, + LEVEL_WARNING, + LEVEL_ERROR, + LEVEL_CRITICAL, + LEVEL_FATAL, + LEVEL_INVALID, + + LEVEL__MAX, + + LEVEL_IGNORE = 0x10, /*< Ignore */ + LEVEL_TIME_SKEW = 0x20, /*< Received after timestamp. */ + LEVEL_MARK = 0x40, /*< Bookmarked line. */ + LEVEL_CONTINUED = 0x80, /*< Continuation of multiline entry. */ + + /** Mask of flags for the level field. */ + LEVEL__FLAGS + = (LEVEL_IGNORE | LEVEL_TIME_SKEW | LEVEL_MARK | LEVEL_CONTINUED) +}; + +#endif diff --git a/src/base/lrucache.hpp b/src/base/lrucache.hpp new file mode 100644 index 0000000..8bcbad6 --- /dev/null +++ b/src/base/lrucache.hpp @@ -0,0 +1,83 @@ +/* + * File: lrucache.hpp + * Author: Alexander Ponomarev + * + * Created on June 20, 2013, 5:09 PM + */ + +#ifndef _LRUCACHE_HPP_INCLUDED_ +#define _LRUCACHE_HPP_INCLUDED_ + +#include +#include +#include +#include + +#include "optional.hpp" + +namespace cache { + +template +class lru_cache { +public: + typedef typename std::pair key_value_pair_t; + typedef typename std::list::iterator list_iterator_t; + + lru_cache(size_t max_size) : + _max_size(max_size) { + } + + void put(const key_t& key, const value_t& value) { + auto it = _cache_items_map.find(key); + _cache_items_list.push_front(key_value_pair_t(key, value)); + if (it != _cache_items_map.end()) { + _cache_items_list.erase(it->second); + _cache_items_map.erase(it); + } + _cache_items_map[key] = _cache_items_list.begin(); + + if (_cache_items_map.size() > _max_size) { + auto last = _cache_items_list.end(); + last--; + _cache_items_map.erase(last->first); + _cache_items_list.pop_back(); + } + } + + nonstd::optional get(const key_t& key) { + auto it = _cache_items_map.find(key); + if (it == _cache_items_map.end()) { + return nonstd::nullopt; + } + + _cache_items_list.splice(_cache_items_list.begin(), _cache_items_list, it->second); + return it->second->second; + } + + bool exists(const key_t& key) const { + return _cache_items_map.find(key) != _cache_items_map.end(); + } + + size_t size() const { + return _cache_items_map.size(); + } + + void set_max_size(size_t max_size) { + this->_max_size = max_size; + } + + void clear() { + this->_cache_items_map.clear(); + this->_cache_items_list.clear(); + } + +private: + std::list _cache_items_list; + std::map _cache_items_map; + size_t _max_size; +}; + +} // namespace cache + +#endif /* _LRUCACHE_HPP_INCLUDED_ */ + diff --git a/src/base/math_util.hh b/src/base/math_util.hh new file mode 100644 index 0000000..842b319 --- /dev/null +++ b/src/base/math_util.hh @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_math_util_hh +#define lnav_math_util_hh + +#include + +#undef rounddown + +/** + * Round down a number based on a given granularity. + * + * @param + * @param step The granularity. + */ +template +inline int +rounddown(Size size, Step step) +{ + return size - (size % step); +} + +inline int +rounddown_offset(size_t size, int step, int offset) +{ + return size - ((size - offset) % step); +} + +inline size_t +roundup_size(size_t size, int step) +{ + size_t retval = size + step; + + retval -= (retval % step); + + return retval; +} + +template +T +abs_diff(T a, T b) +{ + return a > b ? a - b : b - a; +} + +#endif diff --git a/src/base/network.tcp.cc b/src/base/network.tcp.cc new file mode 100644 index 0000000..0194b6f --- /dev/null +++ b/src/base/network.tcp.cc @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "network.tcp.hh" + +#include +#include +#include + +#include "auto_mem.hh" +#include "config.h" +#include "fmt/format.h" + +namespace network { +namespace tcp { + +Result +connect(const char* hostname, const char* servname) +{ + struct addrinfo hints; + auto_mem ai(freeaddrinfo); + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + auto rc = getaddrinfo(hostname, servname, &hints, ai.out()); + + if (rc != 0) { + return Err(fmt::format(FMT_STRING("unable to resolve {}:{} -- {}"), + hostname, + servname, + gai_strerror(rc))); + } + + auto retval = auto_fd(socket(ai->ai_family, ai->ai_socktype, 0)); + + rc = ::connect(retval, ai->ai_addr, ai->ai_addrlen); + if (rc != 0) { + return Err(fmt::format(FMT_STRING("unable to connect to {}:{} -- {}"), + hostname, + servname, + strerror(rc))); + } + + return Ok(std::move(retval)); +} + +} // namespace tcp +} // namespace network diff --git a/src/base/network.tcp.hh b/src/base/network.tcp.hh new file mode 100644 index 0000000..49fc392 --- /dev/null +++ b/src/base/network.tcp.hh @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_network_tcp_hh +#define lnav_network_tcp_hh + +#include + +#include "auto_fd.hh" +#include "result.h" + +namespace network { + +struct locality { + locality(nonstd::optional username, + std::string hostname, + nonstd::optional service) + : l_username(std::move(username)), l_hostname(std::move(hostname)), + l_service(std::move(service)) + { + } + + nonstd::optional l_username; + std::string l_hostname; + nonstd::optional l_service; +}; + +struct path { + locality p_locality; + std::string p_path; + + path(locality loc, std::string path) + : p_locality(std::move(loc)), p_path(std::move(path)) + { + } + + path home() const + { + return { + this->p_locality, + ".", + }; + } +}; + +namespace tcp { + +Result connect(const char* hostname, + const char* servname); + +} +} // namespace network + +#endif diff --git a/src/base/network.tcp.tests.cc b/src/base/network.tcp.tests.cc new file mode 100644 index 0000000..76846ca --- /dev/null +++ b/src/base/network.tcp.tests.cc @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "config.h" +#include "doctest/doctest.h" +#include "network.tcp.hh" + +TEST_CASE("bad hostname") +{ + auto connect_res = network::tcp::connect("foobar.bazzer", "http"); + CHECK(connect_res.unwrapErr() == + "unable to resolve foobar.bazzer:http -- nodename nor servname " + "provided, or not known"); +} + +TEST_CASE("bad servname") +{ + auto connect_res = network::tcp::connect("www.cnn.com", "non-existent"); + CHECK(connect_res.unwrapErr() == + "unable to resolve www.cnn.com:non-existent -- nodename nor " + "servname provided, or not known"); +} diff --git a/src/base/opt_util.hh b/src/base/opt_util.hh new file mode 100644 index 0000000..aea038e --- /dev/null +++ b/src/base/opt_util.hh @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2019, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_opt_util_hh +#define lnav_opt_util_hh + +#include + +#include "optional.hpp" + +namespace detail { + +template +typename std::enable_if::value, T>::type +void_or_nullopt() +{ + return; +} + +template +typename std::enable_if::value, T>::type +void_or_nullopt() +{ + return nonstd::nullopt; +} + +template +struct is_optional : std::false_type { +}; + +template +struct is_optional> : std::true_type { +}; +} // namespace detail + +template>::value, int> = 0> +auto +operator|(T&& t, F f) + -> decltype(detail::void_or_nullopt(t). + operator*()))>()) +{ + using return_type = decltype(f(std::forward(t).operator*())); + if (t) + return f(std::forward(t).operator*()); + else + return detail::void_or_nullopt(); +} + +template +optional_constexpr nonstd::optional::type> +make_optional_from_nullable(T&& v) +{ + if (v != nullptr) { + return nonstd::optional::type>( + std::forward(v)); + } + return nonstd::nullopt; +} + +template class C, typename T> +nonstd::optional +cget(const C& container, size_t index) +{ + if (index < container.size()) { + return container[index]; + } + + return nonstd::nullopt; +} + +inline nonstd::optional +getenv_opt(const char* name) +{ + return make_optional_from_nullable(getenv(name)); +} + +#endif diff --git a/src/base/paths.cc b/src/base/paths.cc new file mode 100644 index 0000000..ca53a07 --- /dev/null +++ b/src/base/paths.cc @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" + +#ifdef __CYGWIN__ +# include +# include +#endif + +#include "fmt/format.h" +#include "paths.hh" + +namespace lnav { +namespace paths { + +#ifdef __CYGWIN__ +char* +windows_to_unix_file_path(char* input) +{ + if (input == nullptr) { + return nullptr; + } + std::string file_path; + file_path.assign(input); + + // Replace the slashes + std::replace(file_path.begin(), + file_path.end(), + WINDOWS_FILE_PATH_SEPARATOR, + UNIX_FILE_PATH_SEPARATOR); + + // Convert the drive letter to lowercase + std::transform( + file_path.begin(), + file_path.begin() + 1, + file_path.begin(), + [](unsigned char character) { return std::tolower(character); }); + + // Remove the colon + const auto drive_letter = file_path.substr(0, 1); + const auto remaining_path = file_path.substr(2, file_path.size() - 2); + file_path = drive_letter + remaining_path; + + std::stringstream stringstream; + stringstream << "/cygdrive/"; + stringstream << file_path; + + return const_cast(stringstream.str().c_str()); +} +#endif + +ghc::filesystem::path +dotlnav() +{ +#ifdef __CYGWIN__ + auto home_env = windows_to_unix_file_path(getenv("APPDATA")); +#else + auto home_env = getenv("HOME"); +#endif + auto xdg_config_home = getenv("XDG_CONFIG_HOME"); + + if (home_env != nullptr) { + auto home_path = ghc::filesystem::path(home_env); + + if (ghc::filesystem::is_directory(home_path)) { + auto home_lnav = home_path / ".lnav"; + + if (ghc::filesystem::is_directory(home_lnav)) { + return home_lnav; + } + + if (xdg_config_home != nullptr) { + auto xdg_path = ghc::filesystem::path(xdg_config_home); + + if (ghc::filesystem::is_directory(xdg_path)) { + return xdg_path / "lnav"; + } + } + + auto home_config = home_path / ".config"; + + if (ghc::filesystem::is_directory(home_config)) { + return home_config / "lnav"; + } + + return home_lnav; + } + } + + return ghc::filesystem::current_path(); +} + +ghc::filesystem::path +workdir() +{ + auto subdir_name = fmt::format(FMT_STRING("lnav-user-{}-work"), getuid()); + auto tmp_path = ghc::filesystem::temp_directory_path(); + + return tmp_path / ghc::filesystem::path(subdir_name); +} + +} // namespace paths +} // namespace lnav diff --git a/src/base/paths.hh b/src/base/paths.hh new file mode 100644 index 0000000..6c43a2a --- /dev/null +++ b/src/base/paths.hh @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_paths_hh +#define lnav_paths_hh + +#include "ghc/filesystem.hpp" + +namespace lnav { +namespace paths { + +#ifdef __CYGWIN__ +static const char WINDOWS_FILE_PATH_SEPARATOR = '\\'; +static const char UNIX_FILE_PATH_SEPARATOR = '/'; + +char* windows_to_unix_file_path(char* input); +#endif + +/** + * Compute the path to a file in the user's '.lnav' directory. + * + * @param sub The path to the file in the '.lnav' directory. + * @return The full path + */ +ghc::filesystem::path dotlnav(); + +ghc::filesystem::path workdir(); + +} // namespace paths +} // namespace lnav + +#endif diff --git a/src/base/result.h b/src/base/result.h new file mode 100644 index 0000000..e3e8ac5 --- /dev/null +++ b/src/base/result.h @@ -0,0 +1,1032 @@ +/* + Mathieu Stefani, 03 mai 2016 + + This header provides a Result type that can be used to replace exceptions in + code that has to handle error. + + Result can be used to return and propagate an error to the caller. + Result is an algebraic data type that can either Ok(T) to represent + success or Err(E) to represent an error. +*/ + +#pragma once + +#include +#include +#include + +#include + +namespace types { +template +struct Ok { + Ok(const T& val) : val(val) {} + Ok(T&& val) : val(std::move(val)) {} + + T val; +}; + +template<> +struct Ok {}; + +template +struct Err { + Err(const E& val) : val(val) {} + Err(E&& val) : val(std::move(val)) {} + + E val; +}; + +template<> +struct Err {}; +}; // namespace types + +template::type> +types::Ok +Ok(T&& val) +{ + return types::Ok(std::forward(val)); +} + +inline types::Ok +Ok() +{ + return {}; +} + +template::type> +types::Err +Err(E&& val) +{ + return types::Err(std::forward(val)); +} + +inline types::Err +Err() +{ + return {}; +} + +template +struct Result; + +namespace details { + +template +struct void_t { + typedef void type; +}; + +namespace impl { +template +struct result_of; + +template +struct result_of : public result_of {}; + +template +struct result_of> { + typedef Ret type; +}; +} // namespace impl + +template +struct result_of : public impl::result_of {}; + +template +struct result_of { + typedef Ret type; +}; + +template +struct result_of { + typedef Ret type; +}; + +template +struct ResultOkType { + typedef typename std::decay::type type; +}; + +template +struct ResultOkType> { + typedef T type; +}; + +template +struct ResultErrType { + typedef R type; +}; + +template +struct ResultErrType> { + typedef typename std::remove_reference::type type; +}; + +template +struct IsResult : public std::false_type {}; +template +struct IsResult> : public std::true_type {}; + +namespace ok { + +namespace impl { + +template +struct Map; + +template +struct Map : public Map {}; + +template +struct Map : public Map {}; + +// General implementation +template +struct Map { + static_assert( + !IsResult::value, + "Can not map a callback returning a Result, use then instead"); + + template + static Result map(const Result& result, Func func) + { + static_assert( + std::is_same::value || std::is_convertible::value, + "Incompatible types detected"); + + if (result.isOk()) { + auto res = func(result.storage().template get()); + return types::Ok(std::move(res)); + } + + return types::Err(result.storage().template get()); + } +}; + +// Specialization for callback returning void +template +struct Map { + template + static Result map(const Result& result, Func func) + { + if (result.isOk()) { + func(result.storage().template get()); + return types::Ok(); + } + + return types::Err(result.storage().template get()); + } +}; + +// Specialization for a void Result +template +struct Map { + template + static Result map(const Result& result, Func func) + { + static_assert(std::is_same::value, + "Can not map a void callback on a non-void Result"); + + if (result.isOk()) { + auto ret = func(); + return types::Ok(std::move(ret)); + } + + return types::Err(result.storage().template get()); + } +}; + +// Specialization for callback returning void on a void Result +template<> +struct Map { + template + static Result map(const Result& result, Func func) + { + static_assert(std::is_same::value, + "Can not map a void callback on a non-void Result"); + + if (result.isOk()) { + func(); + return types::Ok(); + } + + return types::Err(result.storage().template get()); + } +}; + +// General specialization for a callback returning a Result +template +struct Map(Arg)> { + template + static Result map(const Result& result, Func func) + { + static_assert( + std::is_same::value || std::is_convertible::value, + "Incompatible types detected"); + + if (result.isOk()) { + auto res = func(result.storage().template get()); + return res; + } + + return types::Err(result.storage().template get()); + } +}; + +// Specialization for a void callback returning a Result +template +struct Map(void)> { + template + static Result map(const Result& result, Func func) + { + static_assert(std::is_same::value, + "Can not call a void-callback on a non-void Result"); + + if (result.isOk()) { + auto res = func(); + return res; + } + + return types::Err(result.storage().template get()); + } +}; + +} // namespace impl + +template +struct Map; + +template +struct Map : public impl::Map {}; + +template +struct Map : public impl::Map {}; + +template +struct Map : public impl::Map {}; + +template +struct Map : public impl::Map {}; + +template +struct Map> : public impl::Map {}; + +} // namespace ok + +namespace err { + +namespace impl { + +template +struct Map; + +template +struct Map { + static_assert( + !IsResult::value, + "Can not map a callback returning a Result, use orElse instead"); + + template + static Result map(const Result& result, Func func) + { + if (result.isErr()) { + auto res = func(result.storage().template get()); + return types::Err(res); + } + + return types::Ok(result.storage().template get()); + } + + template + static Result map(const Result& result, Func func) + { + if (result.isErr()) { + auto res = func(result.storage().template get()); + return types::Err(res); + } + + return types::Ok(); + } +}; + +} // namespace impl + +template +struct Map : public impl::Map {}; + +} // namespace err + +namespace And { + +namespace impl { + +template +struct Then; + +template +struct Then : public Then {}; + +template +struct Then : public Then {}; + +template +struct Then : public Then {}; + +template +struct Then { + static_assert(std::is_same::value, + "then() should not return anything, use map() instead"); + + template + static Result then(const Result& result, Func func) + { + if (result.isOk()) { + func(result.storage().template get()); + } + return result; + } +}; + +template +struct Then { + static_assert(std::is_same::value, + "then() should not return anything, use map() instead"); + + template + static Result then(const Result& result, Func func) + { + static_assert(std::is_same::value, + "Can not call a void-callback on a non-void Result"); + + if (result.isOk()) { + func(); + } + + return result; + } +}; + +} // namespace impl + +template +struct Then : public impl::Then {}; + +template +struct Then : public impl::Then {}; + +template +struct Then : public impl::Then {}; + +template +struct Then : public impl::Then {}; + +template +struct Then : public impl::Then {}; + +template +struct Then> : public impl::Then {}; + +} // namespace And + +namespace Or { + +namespace impl { + +template +struct Else; + +template +struct Else : public Else {}; + +template +struct Else : public Else {}; + +template +struct Else : public Else {}; + +template +struct Else(Arg)> { + template + static Result orElse(const Result& result, Func func) + { + static_assert( + std::is_same::value || std::is_convertible::value, + "Incompatible types detected"); + + if (result.isErr()) { + auto res = func(result.storage().template get()); + return res; + } + + return types::Ok(result.storage().template get()); + } + + template + static Result orElse(const Result& result, Func func) + { + if (result.isErr()) { + auto res = func(result.storage().template get()); + return res; + } + + return types::Ok(); + } +}; + +template +struct Else(void)> { + template + static Result orElse(const Result& result, Func func) + { + static_assert(std::is_same::value, + "Can not call a void-callback on a non-void Result"); + + if (result.isErr()) { + auto res = func(); + return res; + } + + return types::Ok(result.storage().template get()); + } + + template + static Result orElse(const Result& result, Func func) + { + if (result.isErr()) { + auto res = func(); + return res; + } + + return types::Ok(); + } +}; + +} // namespace impl + +template +struct Else : public impl::Else {}; + +template +struct Else : public impl::Else {}; + +template +struct Else : public impl::Else {}; + +template +struct Else : public impl::Else {}; + +} // namespace Or + +namespace Other { + +namespace impl { + +template +struct Wise; + +template +struct Wise : public Wise {}; + +template +struct Wise : public Wise {}; + +template +struct Wise : public Wise {}; + +template +struct Wise { + template + static Result otherwise(const Result& result, Func func) + { + static_assert( + std::is_same::value || std::is_convertible::value, + "Incompatible types detected"); + + static_assert( + std::is_same::value, + "callback should not return anything, use mapError() for that"); + + if (result.isErr()) { + func(result.storage().template get()); + } + return result; + } +}; + +} // namespace impl + +template +struct Wise : public impl::Wise {}; + +template +struct Wise : public impl::Wise {}; + +template +struct Wise : public impl::Wise {}; + +template +struct Wise : public impl::Wise {}; + +} // namespace Other + +template +decltype(auto) +map(const Result& result, Func func) +{ + return ok::Map::map(result, func); +} + +template::type>::type>> +Ret +mapError(const Result& result, Func func) +{ + return err::Map::map(result, func); +} + +template +Result +then(const Result& result, Func func) +{ + return And::Then::then(result, func); +} + +template +Result +otherwise(const Result& result, Func func) +{ + return Other::Wise::otherwise(result, func); +} + +template::type>::type>> +Ret +orElse(const Result& result, Func func) +{ + return Or::Else::orElse(result, func); +} + +struct ok_tag {}; +struct err_tag {}; + +template +struct Storage { + static constexpr size_t Size = sizeof(T) > sizeof(E) ? sizeof(T) + : sizeof(E); + static constexpr size_t Align = sizeof(T) > sizeof(E) ? alignof(T) + : alignof(E); + + typedef typename std::aligned_storage::type type; + + Storage() : initialized_(false) {} + + void construct(types::Ok ok) + { + new (&storage_) T(std::move(ok.val)); + initialized_ = true; + } + void construct(types::Err err) + { + new (&storage_) E(err.val); + initialized_ = true; + } + + template + void rawConstruct(U&& val) + { + typedef typename std::decay::type CleanU; + + new (&storage_) CleanU(std::forward(val)); + initialized_ = true; + } + + template + const U& get() const + { + return *reinterpret_cast(&storage_); + } + + template + U& get() + { + return *reinterpret_cast(&storage_); + } + + void destroy(ok_tag) + { + if (initialized_) { + get().~T(); + initialized_ = false; + } + } + + void destroy(err_tag) + { + if (initialized_) { + get().~E(); + initialized_ = false; + } + } + + type storage_; + bool initialized_; +}; + +template +struct Storage { + typedef typename std::aligned_storage::type type; + + void construct(types::Ok) { initialized_ = true; } + + void construct(types::Err err) + { + new (&storage_) E(err.val); + initialized_ = true; + } + + template + void rawConstruct(U&& val) + { + typedef typename std::decay::type CleanU; + + new (&storage_) CleanU(std::forward(val)); + initialized_ = true; + } + + void destroy(ok_tag) { initialized_ = false; } + void destroy(err_tag) + { + if (initialized_) { + get().~E(); + initialized_ = false; + } + } + + template::value>> + const U& get() const + { + return *reinterpret_cast(&storage_); + } + + template::value>> + typename std::add_lvalue_reference::type get() + { + return *reinterpret_cast(&storage_); + } + + template::value>> + void get() + { + } + + type storage_; + bool initialized_; +}; + +template +struct Constructor { + static void move(Storage&& src, Storage& dst, ok_tag) + { + dst.rawConstruct(std::move(src.template get())); + src.destroy(ok_tag()); + } + + static void copy(const Storage& src, Storage& dst, ok_tag) + { + dst.rawConstruct(src.template get()); + } + + static void move(Storage&& src, Storage& dst, err_tag) + { + dst.rawConstruct(std::move(src.template get())); + src.destroy(err_tag()); + } + + static void copy(const Storage& src, Storage& dst, err_tag) + { + dst.rawConstruct(src.template get()); + } +}; + +template +struct Constructor { + static void move(Storage&& src, Storage& dst, ok_tag) {} + + static void copy(const Storage& src, Storage& dst, ok_tag) + { + } + + static void move(Storage&& src, Storage& dst, err_tag) + { + dst.rawConstruct(std::move(src.template get())); + src.destroy(err_tag()); + } + + static void copy(const Storage& src, + Storage& dst, + err_tag) + { + dst.rawConstruct(src.template get()); + } +}; + +} // namespace details + +namespace local_concept { + +template +struct EqualityComparable : std::false_type {}; + +template +struct EqualityComparable< + T, + typename std::enable_if< + true, + typename details::void_t() + == std::declval())>::type>::type> + : std::true_type {}; + +} // namespace local_concept + +template +struct Result { + static_assert(!std::is_same::value, + "void error type is not allowed"); + + typedef details::Storage storage_type; + + Result(types::Ok ok) : ok_(true) { storage_.construct(std::move(ok)); } + + Result(types::Err err) : ok_(false) + { + storage_.construct(std::move(err)); + } + + Result(Result&& other) + { + if (other.isOk()) { + details::Constructor::move( + std::move(other.storage_), storage_, details::ok_tag()); + ok_ = true; + } else { + details::Constructor::move( + std::move(other.storage_), storage_, details::err_tag()); + ok_ = false; + } + } + + Result(const Result& other) + { + if (other.isOk()) { + details::Constructor::copy( + other.storage_, storage_, details::ok_tag()); + ok_ = true; + } else { + details::Constructor::copy( + other.storage_, storage_, details::err_tag()); + ok_ = false; + } + } + + ~Result() + { + if (ok_) + storage_.destroy(details::ok_tag()); + else + storage_.destroy(details::err_tag()); + } + + bool isOk() const { return ok_; } + + bool isErr() const { return !ok_; } + + T expect(const char* str) + { + if (!isOk()) { + ::fprintf(stderr, "%s\n", str); + abort(); + } + return expect_impl(std::is_same()); + } + + template + auto map(Func func) + { + using return_type = decltype(func(T{})); + + if (this->isOk()) { + auto value = std::move(this->storage().template get()); + auto res = func(std::move(value)); + return Result( + types::Ok(std::move(res))); + } + + return Result( + types::Err(this->storage().template get())); + } + + template::type>::type>> + Ret mapError(Func func) const + { + return details::mapError(*this, func); + } + + template + Result then(Func func) + { + if (this->isOk()) { + func(std::move(this->storage().template get())); + + return Ok(); + } + + return Err(std::move(this->storage().template get())); + } + + template + Result::type, E> then(Func func) + { + if (this->isOk()) { + return Ok(func(std::move(this->storage().template get()))); + } + + return Err(std::move(this->storage().template get())); + } + + template + void otherwise(Func func) + { + if (this->isOk()) { + return; + } + + func(std::move(this->storage().template get())); + } + + template::type>::type>> + Ret orElse(Func func) const + { + return details::orElse(*this, func); + } + + storage_type& storage() { return storage_; } + + const storage_type& storage() const { return storage_; } + + template + typename std::enable_if::value, T>::type unwrapOr( + const U& defaultValue) const + { + if (isOk()) { + return storage().template get(); + } + return defaultValue; + } + + template + auto unwrapOrElse(Func func) const + { + if (isOk()) { + return storage().template get(); + } + return func(this->storage().template get()); + } + + template + typename std::enable_if::value, U>::type unwrap() + const + { + if (isOk()) { + return std::move(storage().template get()); + } + + ::fprintf(stderr, "Attempting to unwrap an error Result\n"); + abort(); + } + + template + typename std::enable_if::value, U>::type unwrap() + { + if (isOk()) { + return std::move(storage().template get()); + } + + ::fprintf(stderr, "Attempting to unwrap an error Result\n"); + abort(); + } + + template + typename std::enable_if::value, U>::type unwrap() + const + { + if (isOk()) { + return; + } + + ::fprintf(stderr, "Attempting to unwrap an error Result\n"); + abort(); + } + + E unwrapErr() const + { + if (isErr()) { + return storage().template get(); + } + + ::fprintf(stderr, "Attempting to unwrapErr an ok Result\n"); + abort(); + } + +private: + T expect_impl(std::true_type) const {} + T expect_impl(std::false_type) + { + return std::move(storage_.template get()); + } + + bool ok_; + storage_type storage_; +}; + +template +bool +operator==(const Result& lhs, const Result& rhs) +{ + static_assert(local_concept::EqualityComparable::value, + "T must be EqualityComparable for Result to be comparable"); + static_assert(local_concept::EqualityComparable::value, + "E must be EqualityComparable for Result to be comparable"); + + if (lhs.isOk() && rhs.isOk()) { + return lhs.storage().template get() + == rhs.storage().template get(); + } + if (lhs.isErr() && rhs.isErr()) { + return lhs.storage().template get() + == rhs.storage().template get(); + } +} + +template +bool +operator==(const Result& lhs, types::Ok ok) +{ + static_assert(local_concept::EqualityComparable::value, + "T must be EqualityComparable for Result to be comparable"); + + if (!lhs.isOk()) + return false; + + return lhs.storage().template get() == ok.val; +} + +template +bool +operator==(const Result& lhs, types::Ok) +{ + return lhs.isOk(); +} + +template +bool +operator==(const Result& lhs, types::Err err) +{ + static_assert(local_concept::EqualityComparable::value, + "E must be EqualityComparable for Result to be comparable"); + if (!lhs.isErr()) + return false; + + return lhs.storage().template get() == err.val; +} + +#define TRY(...) \ + ({ \ + auto res = __VA_ARGS__; \ + if (!res.isOk()) { \ + typedef typename ::details::ResultErrType::type E; \ + return ::types::Err(res.storage().template get()); \ + } \ + res.unwrap(); \ + }) diff --git a/src/base/snippet_highlighters.cc b/src/base/snippet_highlighters.cc new file mode 100644 index 0000000..058fa41 --- /dev/null +++ b/src/base/snippet_highlighters.cc @@ -0,0 +1,344 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "snippet_highlighters.hh" + +#include "attr_line.builder.hh" +#include "pcrepp/pcre2pp.hh" +#include "view_curses.hh" + +namespace lnav { +namespace snippets { + +static bool +is_bracket(const std::string& str, int index, bool is_lit) +{ + if (index == 0) { + return true; + } + + if (is_lit && str[index - 1] == '\\') { + return true; + } + if (!is_lit && str[index - 1] != '\\') { + return true; + } + return false; +} + +static void +find_matching_bracket( + attr_line_t& al, int x, line_range sub, char left, char right) +{ + bool is_lit = (left == 'Q'); + attr_line_builder alb(al); + const auto& line = al.get_string(); + int depth = 0; + + if (x < sub.lr_start || x > sub.lr_end) { + return; + } + + if (line[x] == right && is_bracket(line, x, is_lit)) { + for (int lpc = x - 1; lpc >= sub.lr_start; lpc--) { + if (line[lpc] == right && is_bracket(line, lpc, is_lit)) { + depth += 1; + } else if (line[lpc] == left && is_bracket(line, lpc, is_lit)) { + if (depth == 0) { + alb.overlay_attr_for_char( + lpc, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE})); + alb.overlay_attr_for_char(lpc, + VC_ROLE.value(role_t::VCR_OK)); + break; + } + depth -= 1; + } + } + } + + if (line[x] == left && is_bracket(line, x, is_lit)) { + for (int lpc = x + 1; lpc < sub.lr_end; lpc++) { + if (line[lpc] == left && is_bracket(line, lpc, is_lit)) { + depth += 1; + } else if (line[lpc] == right && is_bracket(line, lpc, is_lit)) { + if (depth == 0) { + alb.overlay_attr_for_char( + lpc, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE})); + alb.overlay_attr_for_char(lpc, + VC_ROLE.value(role_t::VCR_OK)); + break; + } + depth -= 1; + } + } + } + + nonstd::optional first_left; + + depth = 0; + + for (auto lpc = sub.lr_start; lpc < sub.lr_end; lpc++) { + if (line[lpc] == left && is_bracket(line, lpc, is_lit)) { + depth += 1; + if (!first_left) { + first_left = lpc; + } + } else if (line[lpc] == right && is_bracket(line, lpc, is_lit)) { + if (depth > 0) { + depth -= 1; + } else { + auto lr = line_range(is_lit ? lpc - 1 : lpc, lpc + 1); + alb.overlay_attr( + lr, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE})); + alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_ERROR)); + } + } + } + + if (depth > 0) { + auto lr + = line_range(is_lit ? first_left.value() - 1 : first_left.value(), + first_left.value() + 1); + alb.overlay_attr(lr, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE})); + alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_ERROR)); + } +} + +static bool +check_re_prev(const std::string& line, int x) +{ + bool retval = false; + + if ((x > 0 && line[x - 1] != ')' && line[x - 1] != ']' && line[x - 1] != '*' + && line[x - 1] != '?' && line[x - 1] != '+') + && (x < 2 || line[x - 2] != '\\')) + { + retval = true; + } + + return retval; +} + +static char +safe_read(const std::string& str, std::string::size_type index) +{ + if (index < str.length()) { + return str.at(index); + } + + return 0; +} + +void +regex_highlighter(attr_line_t& al, int x, line_range sub) +{ + static const char* brackets[] = { + "[]", + "{}", + "()", + "QE", + + nullptr, + }; + + const auto& line = al.get_string(); + attr_line_builder alb(al); + bool backslash_is_quoted = false; + + for (auto lpc = sub.lr_start; lpc < sub.lr_end; lpc++) { + if (lpc == 0 || line[lpc - 1] != '\\') { + switch (line[lpc]) { + case '^': + case '$': + case '*': + case '+': + case '|': + case '.': + alb.overlay_attr_for_char( + lpc, VC_ROLE.value(role_t::VCR_RE_SPECIAL)); + + if ((line[lpc] == '*' || line[lpc] == '+') + && check_re_prev(line, lpc)) + { + alb.overlay_attr_for_char( + lpc - 1, VC_ROLE.value(role_t::VCR_RE_REPEAT)); + } + break; + case '?': { + struct line_range lr(lpc, lpc + 1); + + if (lpc == sub.lr_start || (lpc - sub.lr_start) == 0) { + alb.overlay_attr_for_char( + lpc, + VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE})); + alb.overlay_attr_for_char( + lpc, VC_ROLE.value(role_t::VCR_ERROR)); + } else if (line[lpc - 1] == '(') { + switch (line[lpc + 1]) { + case ':': + case '!': + case '#': + lr.lr_end += 1; + break; + } + alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_OK)); + if (line[lpc + 1] == '<') { + alb.overlay_attr( + line_range(lpc + 1, lpc + 2), + VC_ROLE.value(role_t::VCR_RE_SPECIAL)); + } + } else { + alb.overlay_attr(lr, + VC_ROLE.value(role_t::VCR_RE_SPECIAL)); + + if (check_re_prev(line, lpc)) { + alb.overlay_attr_for_char( + lpc - 1, VC_ROLE.value(role_t::VCR_RE_REPEAT)); + } + } + break; + } + case '>': { + static const auto CAP_RE + = lnav::pcre2pp::code::from_const(R"(\(\?\<\w+$)"); + + auto capture_start + = string_fragment::from_str_range( + line, sub.lr_start, lpc) + .find_left_boundary(lpc - sub.lr_start - 1, + string_fragment::tag1{'('}); + + auto cap_find_res + = CAP_RE.find_in(capture_start).ignore_error(); + + if (cap_find_res) { + alb.overlay_attr( + line_range(capture_start.sf_begin + + cap_find_res->f_all.sf_begin + 3, + capture_start.sf_begin + + cap_find_res->f_all.sf_end), + VC_ROLE.value(role_t::VCR_IDENTIFIER)); + alb.overlay_attr(line_range(lpc, lpc + 1), + VC_ROLE.value(role_t::VCR_RE_SPECIAL)); + } + break; + } + + case '(': + case ')': + case '{': + case '}': + case '[': + case ']': + alb.overlay_attr_for_char(lpc, + VC_ROLE.value(role_t::VCR_OK)); + break; + } + } + if (lpc > 0 && line[lpc - 1] == '\\') { + if (backslash_is_quoted) { + backslash_is_quoted = false; + continue; + } + switch (line[lpc]) { + case '\\': + backslash_is_quoted = true; + alb.overlay_attr(line_range(lpc - 1, lpc + 1), + VC_ROLE.value(role_t::VCR_RE_SPECIAL)); + break; + case 'd': + case 'D': + case 'h': + case 'H': + case 'N': + case 'R': + case 's': + case 'S': + case 'v': + case 'V': + case 'w': + case 'W': + case 'X': + + case 'A': + case 'b': + case 'B': + case 'G': + case 'Z': + case 'z': + alb.overlay_attr(line_range(lpc - 1, lpc + 1), + VC_ROLE.value(role_t::VCR_SYMBOL)); + break; + case ' ': + alb.overlay_attr( + line_range(lpc - 1, lpc + 1), + VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE})); + alb.overlay_attr(line_range(lpc - 1, lpc + 1), + VC_ROLE.value(role_t::VCR_ERROR)); + break; + case '0': + case 'x': + if (safe_read(line, lpc + 1) == '{') { + alb.overlay_attr(line_range(lpc - 1, lpc + 1), + VC_ROLE.value(role_t::VCR_RE_SPECIAL)); + } else if (isdigit(safe_read(line, lpc + 1)) + && isdigit(safe_read(line, lpc + 2))) + { + alb.overlay_attr(line_range(lpc - 1, lpc + 3), + VC_ROLE.value(role_t::VCR_RE_SPECIAL)); + } else { + alb.overlay_attr( + line_range(lpc - 1, lpc + 1), + VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE})); + alb.overlay_attr(line_range(lpc - 1, lpc + 1), + VC_ROLE.value(role_t::VCR_ERROR)); + } + break; + case 'Q': + case 'E': + alb.overlay_attr(line_range(lpc - 1, lpc + 1), + VC_ROLE.value(role_t::VCR_OK)); + break; + default: + if (isdigit(line[lpc])) { + alb.overlay_attr(line_range(lpc - 1, lpc + 1), + VC_ROLE.value(role_t::VCR_RE_SPECIAL)); + } + break; + } + } + } + + for (int lpc = 0; brackets[lpc]; lpc++) { + find_matching_bracket(al, x, sub, brackets[lpc][0], brackets[lpc][1]); + } +} + +} // namespace snippets +} // namespace lnav diff --git a/src/base/snippet_highlighters.hh b/src/base/snippet_highlighters.hh new file mode 100644 index 0000000..0da6291 --- /dev/null +++ b/src/base/snippet_highlighters.hh @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2022, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_snippet_highlighters_hh +#define lnav_snippet_highlighters_hh + +#include "attr_line.hh" + +namespace lnav { +namespace snippets { + +void regex_highlighter(attr_line_t& al, int x, line_range sub); + +} // namespace snippets +} // namespace lnav + +#endif diff --git a/src/base/string_attr_type.cc b/src/base/string_attr_type.cc new file mode 100644 index 0000000..9a3950b --- /dev/null +++ b/src/base/string_attr_type.cc @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "string_attr_type.hh" + +#include "config.h" + +string_attr_type SA_ORIGINAL_LINE("original_line"); +string_attr_type SA_BODY("body"); +string_attr_type SA_HIDDEN("hidden"); +string_attr_type SA_FORMAT("format"); +string_attr_type SA_REMOVED("removed"); +string_attr_type SA_PREFORMATTED("preformatted"); +string_attr_type SA_INVALID("invalid"); +string_attr_type SA_ERROR("error"); +string_attr_type SA_LEVEL("level"); +string_attr_type SA_ORIGIN("origin"); +string_attr_type SA_ORIGIN_OFFSET("origin-offset"); + +string_attr_type VC_ROLE("role"); +string_attr_type VC_ROLE_FG("role-fg"); +string_attr_type VC_STYLE("style"); +string_attr_type VC_GRAPHIC("graphic"); +string_attr_type VC_FOREGROUND("foreground"); +string_attr_type VC_BACKGROUND("background"); diff --git a/src/base/string_attr_type.hh b/src/base/string_attr_type.hh new file mode 100644 index 0000000..5bff345 --- /dev/null +++ b/src/base/string_attr_type.hh @@ -0,0 +1,679 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_string_attr_type_hh +#define lnav_string_attr_type_hh + +#include +#include + +#include + +#include "base/intern_string.hh" +#include "mapbox/variant.hpp" + +class logfile; +struct bookmark_metadata; + +/** Roles that can be mapped to curses attributes using attrs_for_role() */ +enum class role_t : int32_t { + VCR_NONE = -1, + + VCR_TEXT, /*< Raw text. */ + VCR_IDENTIFIER, + VCR_SEARCH, /*< A search hit. */ + VCR_OK, + VCR_INFO, + VCR_ERROR, /*< An error message. */ + VCR_WARNING, /*< A warning message. */ + VCR_ALT_ROW, /*< Highlight for alternating rows in a list */ + VCR_HIDDEN, + VCR_CURSOR_LINE, + VCR_ADJUSTED_TIME, + VCR_SKEWED_TIME, + VCR_OFFSET_TIME, + VCR_INVALID_MSG, + VCR_STATUS, /*< Normal status line text. */ + VCR_WARN_STATUS, + VCR_ALERT_STATUS, /*< Alert status line text. */ + VCR_ACTIVE_STATUS, /*< */ + VCR_ACTIVE_STATUS2, /*< */ + VCR_STATUS_TITLE, + VCR_STATUS_SUBTITLE, + VCR_STATUS_INFO, + VCR_STATUS_STITCH_TITLE_TO_SUB, + VCR_STATUS_STITCH_SUB_TO_TITLE, + VCR_STATUS_STITCH_SUB_TO_NORMAL, + VCR_STATUS_STITCH_NORMAL_TO_SUB, + VCR_STATUS_STITCH_TITLE_TO_NORMAL, + VCR_STATUS_STITCH_NORMAL_TO_TITLE, + VCR_STATUS_TITLE_HOTKEY, + VCR_STATUS_DISABLED_TITLE, + VCR_STATUS_HOTKEY, + VCR_INACTIVE_STATUS, + VCR_INACTIVE_ALERT_STATUS, + VCR_SCROLLBAR, + VCR_SCROLLBAR_ERROR, + VCR_SCROLLBAR_WARNING, + VCR_FOCUSED, + VCR_DISABLED_FOCUSED, + VCR_POPUP, + VCR_COLOR_HINT, + + VCR_QUOTED_CODE, + VCR_CODE_BORDER, + VCR_KEYWORD, + VCR_STRING, + VCR_COMMENT, + VCR_DOC_DIRECTIVE, + VCR_VARIABLE, + VCR_SYMBOL, + VCR_NUMBER, + VCR_RE_SPECIAL, + VCR_RE_REPEAT, + VCR_FILE, + + VCR_DIFF_DELETE, /*< Deleted line in a diff. */ + VCR_DIFF_ADD, /*< Added line in a diff. */ + VCR_DIFF_SECTION, /*< Section marker in a diff. */ + + VCR_LOW_THRESHOLD, + VCR_MED_THRESHOLD, + VCR_HIGH_THRESHOLD, + + VCR_H1, + VCR_H2, + VCR_H3, + VCR_H4, + VCR_H5, + VCR_H6, + + VCR_HR, + VCR_HYPERLINK, + VCR_LIST_GLYPH, + VCR_BREADCRUMB, + VCR_TABLE_BORDER, + VCR_TABLE_HEADER, + VCR_QUOTE_BORDER, + VCR_QUOTED_TEXT, + VCR_FOOTNOTE_BORDER, + VCR_FOOTNOTE_TEXT, + VCR_SNIPPET_BORDER, + + VCR__MAX +}; + +struct text_attrs { + bool empty() const + { + return this->ta_attrs == 0 && !this->ta_fg_color && !this->ta_bg_color; + } + + text_attrs operator|(const text_attrs& other) const + { + return text_attrs{ + this->ta_attrs | other.ta_attrs, + this->ta_fg_color ? this->ta_fg_color : other.ta_fg_color, + this->ta_bg_color ? this->ta_bg_color : other.ta_bg_color, + }; + } + + bool operator==(const text_attrs& other) const + { + return this->ta_attrs == other.ta_attrs + && this->ta_fg_color == other.ta_fg_color + && this->ta_bg_color == other.ta_bg_color; + } + + int32_t ta_attrs{0}; + nonstd::optional ta_fg_color; + nonstd::optional ta_bg_color; +}; + +using string_attr_value = mapbox::util::variant, + bookmark_metadata*, + timespec, + string_fragment>; + +class string_attr_type_base { +public: + explicit string_attr_type_base(const char* name) noexcept : sat_name(name) + { + } + + const char* const sat_name; +}; + +using string_attr_pair + = std::pair; + +template +class string_attr_type : public string_attr_type_base { +public: + using value_type = T; + + explicit string_attr_type(const char* name) noexcept + : string_attr_type_base(name) + { + } + + template + std::enable_if_t::value, string_attr_pair> value( + const U& val) const + { + return std::make_pair(this, val); + } + + template + std::enable_if_t::value, string_attr_pair> value() const + { + return std::make_pair(this, string_attr_value{}); + } +}; + +extern string_attr_type SA_ORIGINAL_LINE; +extern string_attr_type SA_BODY; +extern string_attr_type SA_HIDDEN; +extern string_attr_type SA_FORMAT; +extern string_attr_type SA_REMOVED; +extern string_attr_type SA_PREFORMATTED; +extern string_attr_type SA_INVALID; +extern string_attr_type SA_ERROR; +extern string_attr_type SA_LEVEL; +extern string_attr_type SA_ORIGIN; +extern string_attr_type SA_ORIGIN_OFFSET; + +extern string_attr_type VC_ROLE; +extern string_attr_type VC_ROLE_FG; +extern string_attr_type VC_STYLE; +extern string_attr_type VC_GRAPHIC; +extern string_attr_type VC_FOREGROUND; +extern string_attr_type VC_BACKGROUND; + +namespace lnav { + +namespace string { +namespace attrs { + +template +inline std::pair +preformatted(S str) +{ + return std::make_pair(std::move(str), SA_PREFORMATTED.template value()); +} + +} // namespace attrs +} // namespace string + +namespace roles { + +template +inline std::pair +error(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_ERROR)); +} + +template +inline std::pair +warning(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_WARNING)); +} + +template +inline std::pair +status(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_STATUS)); +} + +template +inline std::pair +inactive_status(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_INACTIVE_STATUS)); +} + +template +inline std::pair +status_title(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_STATUS_TITLE)); +} + +template +inline std::pair +ok(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_OK)); +} + +template +inline std::pair +file(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_FILE)); +} + +template +inline std::pair +symbol(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_SYMBOL)); +} + +template +inline std::pair +keyword(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_KEYWORD)); +} + +template +inline std::pair +variable(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_VARIABLE)); +} + +template +inline std::pair +number(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_NUMBER)); +} + +template +inline std::pair +comment(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_COMMENT)); +} + +template +inline std::pair +identifier(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_IDENTIFIER)); +} + +template +inline std::pair +hr(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_HR)); +} + +template +inline std::pair +hyperlink(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_HYPERLINK)); +} + +template +inline std::pair +list_glyph(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_LIST_GLYPH)); +} + +template +inline std::pair +breadcrumb(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_BREADCRUMB)); +} + +template +inline std::pair +quoted_code(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_QUOTED_CODE)); +} + +template +inline std::pair +code_border(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_CODE_BORDER)); +} + +template +inline std::pair +snippet_border(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_SNIPPET_BORDER)); +} + +template +inline std::pair +table_border(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_TABLE_BORDER)); +} + +template +inline std::pair +table_header(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_TABLE_HEADER)); +} + +template +inline std::pair +quote_border(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_QUOTE_BORDER)); +} + +template +inline std::pair +quoted_text(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_QUOTED_TEXT)); +} + +template +inline std::pair +footnote_border(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_FOOTNOTE_BORDER)); +} + +template +inline std::pair +footnote_text(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_FOOTNOTE_TEXT)); +} + +template +inline std::pair +h1(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_H1)); +} + +template +inline std::pair +h2(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_H2)); +} + +template +inline std::pair +h3(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_H3)); +} + +template +inline std::pair +h4(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_H4)); +} + +template +inline std::pair +h5(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_H5)); +} + +template +inline std::pair +h6(S str) +{ + return std::make_pair(std::move(str), + VC_ROLE.template value(role_t::VCR_H6)); +} + +namespace literals { + +inline std::pair operator"" _ok(const char* str, + std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_OK)); +} + +inline std::pair operator"" _error( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_ERROR)); +} + +inline std::pair operator"" _info( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_INFO)); +} + +inline std::pair operator"" _symbol( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_SYMBOL)); +} + +inline std::pair operator"" _keyword( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_KEYWORD)); +} + +inline std::pair operator"" _variable( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_VARIABLE)); +} + +inline std::pair operator"" _comment( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_COMMENT)); +} + +inline std::pair operator"" _hotkey( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_STATUS_HOTKEY)); +} + +inline std::pair operator"" _h1(const char* str, + std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_H1)); +} + +inline std::pair operator"" _h2(const char* str, + std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_H2)); +} + +inline std::pair operator"" _h3(const char* str, + std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_H3)); +} + +inline std::pair operator"" _h4(const char* str, + std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_H4)); +} + +inline std::pair operator"" _h5(const char* str, + std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_H5)); +} + +inline std::pair operator"" _hr(const char* str, + std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_HR)); +} + +inline std::pair operator"" _hyperlink( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_HYPERLINK)); +} + +inline std::pair operator"" _list_glyph( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_LIST_GLYPH)); +} + +inline std::pair operator"" _breadcrumb( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_BREADCRUMB)); +} + +inline std::pair operator"" _quoted_code( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_QUOTED_CODE)); +} + +inline std::pair operator"" _code_border( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_CODE_BORDER)); +} + +inline std::pair operator"" _table_border( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_TABLE_BORDER)); +} + +inline std::pair operator"" _quote_border( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_QUOTE_BORDER)); +} + +inline std::pair operator"" _quoted_text( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_QUOTED_TEXT)); +} + +inline std::pair operator"" _footnote_border( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_FOOTNOTE_BORDER)); +} + +inline std::pair operator"" _footnote_text( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_FOOTNOTE_BORDER)); +} + +inline std::pair operator"" _snippet_border( + const char* str, std::size_t len) +{ + return std::make_pair(std::string(str, len), + VC_ROLE.template value(role_t::VCR_SNIPPET_BORDER)); +} + +} // namespace literals + +} // namespace roles +} // namespace lnav + +#endif diff --git a/src/base/string_util.cc b/src/base/string_util.cc new file mode 100644 index 0000000..8af686c --- /dev/null +++ b/src/base/string_util.cc @@ -0,0 +1,304 @@ +/** + * Copyright (c) 2019, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include +#include +#include + +#include "string_util.hh" + +#include "config.h" +#include "is_utf8.hh" +#include "lnav_log.hh" + +void +scrub_to_utf8(char* buffer, size_t length) +{ + while (true) { + auto frag = string_fragment::from_bytes(buffer, length); + auto scan_res = is_utf8(frag); + + if (scan_res.is_valid()) { + break; + } + for (size_t lpc = 0; lpc < scan_res.usr_faulty_bytes; lpc++) { + buffer[scan_res.usr_valid_frag.sf_end + lpc] = '?'; + } + } +} + +void +quote_content(auto_buffer& buf, const string_fragment& sf, char quote_char) +{ + for (char ch : sf) { + if (ch == quote_char) { + buf.push_back('\\').push_back(ch); + continue; + } + switch (ch) { + case '\\': + buf.push_back('\\').push_back('\\'); + break; + case '\n': + buf.push_back('\\').push_back('n'); + break; + case '\t': + buf.push_back('\\').push_back('t'); + break; + case '\r': + buf.push_back('\\').push_back('r'); + break; + case '\a': + buf.push_back('\\').push_back('a'); + break; + case '\b': + buf.push_back('\\').push_back('b'); + break; + default: + buf.push_back(ch); + break; + } + } +} + +size_t +unquote_content(char* dst, const char* str, size_t len, char quote_char) +{ + size_t index = 0; + + for (size_t lpc = 0; lpc < len; lpc++, index++) { + dst[index] = str[lpc]; + if (str[lpc] == quote_char) { + lpc += 1; + } else if (str[lpc] == '\\' && (lpc + 1) < len) { + switch (str[lpc + 1]) { + case 'n': + dst[index] = '\n'; + break; + case 'r': + dst[index] = '\r'; + break; + case 't': + dst[index] = '\t'; + break; + default: + dst[index] = str[lpc + 1]; + break; + } + lpc += 1; + } + } + dst[index] = '\0'; + + return index; +} + +size_t +unquote(char* dst, const char* str, size_t len) +{ + if (str[0] == 'r' || str[0] == 'u') { + str += 1; + len -= 1; + } + char quote_char = str[0]; + + require(str[0] == '\'' || str[0] == '"'); + + return unquote_content(dst, &str[1], len - 2, quote_char); +} + +size_t +unquote_w3c(char* dst, const char* str, size_t len) +{ + size_t index = 0; + + require(str[0] == '\'' || str[0] == '"'); + + for (size_t lpc = 1; lpc < (len - 1); lpc++, index++) { + dst[index] = str[lpc]; + if (str[lpc] == '"') { + lpc += 1; + } + } + dst[index] = '\0'; + + return index; +} + +void +truncate_to(std::string& str, size_t max_char_len) +{ + static const std::string ELLIPSIS = "\u22ef"; + + if (str.length() < max_char_len) { + return; + } + + auto str_char_len_res = utf8_string_length(str); + + if (str_char_len_res.isErr()) { + // XXX + return; + } + + auto str_char_len = str_char_len_res.unwrap(); + if (str_char_len <= max_char_len) { + return; + } + + if (max_char_len < 3) { + str = ELLIPSIS; + return; + } + + auto chars_to_remove = (str_char_len - max_char_len) + 1; + auto midpoint = str_char_len / 2; + auto chars_to_keep_at_front = midpoint - (chars_to_remove / 2); + auto bytes_to_keep_at_front + = utf8_char_to_byte_index(str, chars_to_keep_at_front); + auto remove_up_to_bytes = utf8_char_to_byte_index( + str, chars_to_keep_at_front + chars_to_remove); + auto bytes_to_remove = remove_up_to_bytes - bytes_to_keep_at_front; + str.erase(bytes_to_keep_at_front, bytes_to_remove); + str.insert(bytes_to_keep_at_front, ELLIPSIS); +} + +bool +is_url(const std::string& fn) +{ + static const auto url_re = std::regex("^(file|https?|ftps?|scp|sftp):.*"); + + return std::regex_match(fn, url_re); +} + +size_t +abbreviate_str(char* str, size_t len, size_t max_len) +{ + size_t last_start = 1; + + if (len < max_len) { + return len; + } + + for (size_t index = 0; index < len; index++) { + switch (str[index]) { + case '.': + case '-': + case '/': + case ':': + memmove(&str[last_start], &str[index], len - index); + len -= (index - last_start); + index = last_start + 1; + last_start = index + 1; + + if (len < max_len) { + return len; + } + break; + } + } + + return len; +} + +void +split_ws(const std::string& str, std::vector& toks_out) +{ + std::stringstream ss(str); + std::string buf; + + while (ss >> buf) { + toks_out.push_back(buf); + } +} + +std::string +repeat(const std::string& input, size_t num) +{ + std::ostringstream os; + std::fill_n(std::ostream_iterator(os), num, input); + return os.str(); +} + +std::string +center_str(const std::string& subject, size_t width) +{ + std::string retval = subject; + + truncate_to(retval, width); + + auto retval_length = utf8_string_length(retval).unwrapOr(retval.length()); + auto total_fill = width - retval_length; + auto before = total_fill / 2; + auto after = total_fill - before; + + retval.insert(0, before, ' '); + retval.append(after, ' '); + + return retval; +} + +bool +is_blank(const std::string& str) +{ + return std::all_of( + str.begin(), str.end(), [](const auto ch) { return isspace(ch); }); +} + +std::string +scrub_ws(const char* in) +{ + static const std::string TAB_SYMBOL = "\u21e5"; + static const std::string LF_SYMBOL = "\u240a"; + static const std::string CR_SYMBOL = "\u240d"; + + std::string retval; + + for (size_t lpc = 0; in[lpc]; lpc++) { + auto ch = in[lpc]; + + switch (ch) { + case '\t': + retval.append(TAB_SYMBOL); + break; + case '\n': + retval.append(LF_SYMBOL); + break; + case '\r': + retval.append(CR_SYMBOL); + break; + default: + retval.append(1, ch); + break; + } + } + + return retval; +} diff --git a/src/base/string_util.hh b/src/base/string_util.hh new file mode 100644 index 0000000..73a8b87 --- /dev/null +++ b/src/base/string_util.hh @@ -0,0 +1,232 @@ +/** + * Copyright (c) 2019, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_string_util_hh +#define lnav_string_util_hh + +#include +#include + +#include + +#include "auto_mem.hh" +#include "intern_string.hh" +#include "ww898/cp_utf8.hpp" + +void scrub_to_utf8(char* buffer, size_t length); + +inline bool +is_line_ending(char ch) +{ + return ch == '\r' || ch == '\n'; +} + +void quote_content(auto_buffer& buf, + const string_fragment& sf, + char quote_char); + +size_t unquote_content(char* dst, const char* str, size_t len, char quote_char); + +size_t unquote(char* dst, const char* str, size_t len); + +size_t unquote_w3c(char* dst, const char* str, size_t len); + +inline bool +startswith(const char* str, const char* prefix) +{ + return strncmp(str, prefix, strlen(prefix)) == 0; +} + +inline bool +startswith(const std::string& str, const char* prefix) +{ + return startswith(str.c_str(), prefix); +} + +inline bool +startswith(const std::string& str, const std::string& prefix) +{ + return startswith(str.c_str(), prefix.c_str()); +} + +inline bool +endswith(const char* str, const char* suffix) +{ + size_t len = strlen(str), suffix_len = strlen(suffix); + + if (suffix_len > len) { + return false; + } + + return strcmp(&str[len - suffix_len], suffix) == 0; +} + +template +inline bool +endswith(const std::string& str, const char (&suffix)[N]) +{ + if (N - 1 > str.length()) { + return false; + } + + return strcmp(&str[str.size() - (N - 1)], suffix) == 0; +} + +void truncate_to(std::string& str, size_t max_char_len); + +std::string scrub_ws(const char* in); + +inline std::string +trim(const std::string& str) +{ + std::string::size_type start, end; + + for (start = 0; start < str.size() && isspace(str[start]); start++) + ; + for (end = str.size(); end > 0 && isspace(str[end - 1]); end--) + ; + + return str.substr(start, end - start); +} + +inline std::string +rtrim(const std::string& str) +{ + std::string::size_type end; + + for (end = str.size(); end > 0 && isspace(str[end - 1]); end--) + ; + + return str.substr(0, end); +} + +inline std::string +tolower(const char* str) +{ + std::string retval; + + for (int lpc = 0; str[lpc]; lpc++) { + retval.push_back(::tolower(str[lpc])); + } + + return retval; +} + +inline std::string +tolower(const std::string& str) +{ + return tolower(str.c_str()); +} + +inline std::string +toupper(const char* str) +{ + std::string retval; + + for (int lpc = 0; str[lpc]; lpc++) { + retval.push_back(::toupper(str[lpc])); + } + + return retval; +} + +inline std::string +toupper(const std::string& str) +{ + return toupper(str.c_str()); +} + +inline ssize_t +utf8_char_to_byte_index(const std::string& str, ssize_t ch_index) +{ + ssize_t retval = 0; + + while (ch_index > 0) { + auto ch_len + = ww898::utf::utf8::char_size([&str, retval]() { + return std::make_pair(str[retval], str.length() - retval - 1); + }).unwrapOr(1); + + retval += ch_len; + ch_index -= 1; + } + + return retval; +} + +inline Result +utf8_string_length(const char* str, ssize_t len = -1) +{ + size_t retval = 0; + + if (len == -1) { + len = strlen(str); + } + + for (ssize_t byte_index = 0; byte_index < len;) { + auto ch_size + = TRY(ww898::utf::utf8::char_size([str, len, byte_index]() { + return std::make_pair(str[byte_index], len - byte_index); + })); + byte_index += ch_size; + retval += 1; + } + + return Ok(retval); +} + +inline Result +utf8_string_length(const std::string& str) +{ + return utf8_string_length(str.c_str(), str.length()); +} + +bool is_url(const std::string& fn); + +bool is_blank(const std::string& str); + +size_t abbreviate_str(char* str, size_t len, size_t max_len); + +void split_ws(const std::string& str, std::vector& toks_out); + +std::string repeat(const std::string& input, size_t num); + +std::string center_str(const std::string& subject, size_t width); + +inline std::string +on_blank(const std::string& str, const std::string& def) +{ + if (is_blank(str)) { + return def; + } + + return str; +} + +#endif diff --git a/src/base/string_util.tests.cc b/src/base/string_util.tests.cc new file mode 100644 index 0000000..98cf5c8 --- /dev/null +++ b/src/base/string_util.tests.cc @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "base/string_util.hh" + +#include "base/strnatcmp.h" +#include "config.h" +#include "doctest/doctest.h" + +TEST_CASE("endswith") +{ + std::string hw("hello"); + + CHECK(endswith(hw, "f") == false); + CHECK(endswith(hw, "lo") == true); +} + +TEST_CASE("truncate_to") +{ + const std::string orig = "0123456789abcdefghijklmnopqrstuvwxyz"; + std::string str; + + truncate_to(str, 10); + CHECK(str == ""); + str = "abc"; + truncate_to(str, 10); + CHECK(str == "abc"); + str = orig; + truncate_to(str, 10); + CHECK(str == "01234\u22efwxyz"); + str = orig; + truncate_to(str, 1); + CHECK(str == "\u22ef"); + str = orig; + truncate_to(str, 2); + CHECK(str == "\u22ef"); + str = orig; + truncate_to(str, 3); + CHECK(str == "0\u22efz"); + str = orig; + truncate_to(str, 4); + CHECK(str == "01\u22efz"); + str = orig; + truncate_to(str, 5); + CHECK(str == "01\u22efyz"); +} + +TEST_CASE("strnatcmp") +{ + { + constexpr const char* n1 = "010"; + constexpr const char* n2 = "020"; + + CHECK(strnatcmp(strlen(n1), n1, strlen(n2), n2) < 0); + } + { + constexpr const char* n1 = "2"; + constexpr const char* n2 = "10"; + + CHECK(strnatcmp(strlen(n1), n1, strlen(n2), n2) < 0); + } +} diff --git a/src/base/strnatcmp.c b/src/base/strnatcmp.c new file mode 100644 index 0000000..6773164 --- /dev/null +++ b/src/base/strnatcmp.c @@ -0,0 +1,302 @@ +/* -*- mode: c; c-file-style: "k&r" -*- + + strnatcmp.c -- Perform 'natural order' comparisons of strings in C. + Copyright (C) 2000, 2004 by Martin Pool + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + + +/* partial change history: + * + * 2004-10-10 mbp: Lift out character type dependencies into macros. + * + * Eric Sosman pointed out that ctype functions take a parameter whose + * value must be that of an unsigned int, even on platforms that have + * negative chars in their default char type. + */ + +#include +#include + +#include "strnatcmp.h" + + +/* These are defined as macros to make it easier to adapt this code to + * different characters types or comparison functions. */ +static inline int +nat_isdigit(nat_char a) +{ + return isdigit((unsigned char) a); +} + + +static inline int +nat_isspace(nat_char a) +{ + return isspace((unsigned char) a); +} + + +static inline nat_char +nat_toupper(nat_char a) +{ + return toupper((unsigned char) a); +} + + + +static int +compare_right(int a_len, nat_char const *a, int b_len, nat_char const *b, int *len_out) +{ + int bias = 0; + + /* The longest run of digits wins. That aside, the greatest + value wins, but we can't know that it will until we've scanned + both numbers to know that they have the same magnitude, so we + remember it in BIAS. */ + for (;; a++, b++, a_len--, b_len--, (*len_out)++) { + if (a_len == 0 && b_len == 0) + return bias; + if (a_len == 0) + return -1; + if (b_len == 0) + return 1; + if (!nat_isdigit(*a) && !nat_isdigit(*b)) + return bias; + else if (!nat_isdigit(*a)) + return -1; + else if (!nat_isdigit(*b)) + return +1; + else if (*a < *b) { + if (!bias) + bias = -1; + } else if (*a > *b) { + if (!bias) + bias = +1; + } else if (!*a && !*b) + return bias; + } + + return 0; +} + +static int +compare_left(int a_len, nat_char const *a, int b_len, nat_char const *b, int *len_out) +{ + /* Compare two left-aligned numbers: the first to have a + different value wins. */ + for (;; a++, b++, a_len--, b_len--, (*len_out)++) { + if (a_len == 0 && b_len == 0) + return 0; + if (a_len == 0) + return -1; + if (b_len == 0) + return 1; + if (!nat_isdigit(*a) && !nat_isdigit(*b)) + return 0; + else if (!nat_isdigit(*a)) + return -1; + else if (!nat_isdigit(*b)) + return +1; + else if (*a < *b) + return -1; + else if (*a > *b) + return +1; + } + + return 0; +} + +static int strnatcmp0(int a_len, nat_char const *a, + int b_len, nat_char const *b, + int fold_case) +{ + int ai, bi; + nat_char ca, cb; + int fractional, result; + + assert(a && b); + ai = bi = 0; + while (1) { + if (ai >= a_len) + ca = 0; + else + ca = a[ai]; + if (bi >= b_len) + cb = 0; + else + cb = b[bi]; + + /* skip over leading spaces or zeros */ + while (nat_isspace(ca)) { + ai += 1; + if (ai >= a_len) + ca = 0; + else + ca = a[ai]; + } + + while (nat_isspace(cb)) { + bi += 1; + if (bi >= b_len) + cb = 0; + else + cb = b[bi]; + } + + /* process run of digits */ + if (nat_isdigit(ca) && nat_isdigit(cb)) { + int num_len = 0; + + fractional = (ca == '0' || cb == '0'); + + if (fractional) { + if ((result = compare_left(a_len - ai, a + ai, b_len - bi, + b + bi, &num_len)) != 0) { + return result; + } + } else { + if ((result = compare_right(a_len - ai, a + ai, b_len - bi, + b + bi, &num_len)) != 0) { + return result; + } + } + + ai += num_len; + bi += num_len; + continue; + } + + if (!ca && !cb) { + /* The strings compare the same. Perhaps the caller + will want to call strcmp to break the tie. */ + return 0; + } + + if (fold_case) { + ca = nat_toupper(ca); + cb = nat_toupper(cb); + } + + if (ca < cb) + return -1; + else if (ca > cb) + return +1; + + ++ai; + ++bi; + } +} + +int ipv4cmp(int a_len, nat_char const *a, + int b_len, nat_char const *b, + int *res_out) +{ + int ai, bi; + nat_char ca, cb; + int fractional, result = 0; + + assert(a && b); + ai = bi = 0; + while (result == 0) { + if (ai >= a_len) + ca = 0; + else + ca = a[ai]; + if (bi >= b_len) + cb = 0; + else + cb = b[bi]; + + /* skip over leading spaces or zeros */ + while (nat_isspace(ca)) { + ai += 1; + if (ai >= a_len) + ca = 0; + else + ca = a[ai]; + } + + while (nat_isspace(cb)) { + bi += 1; + if (bi >= b_len) + cb = 0; + else + cb = b[bi]; + } + + /* process run of digits */ + if (nat_isdigit(ca) && nat_isdigit(cb)) { + int num_len = 0; + + fractional = (ca == '0' || cb == '0'); + + if (fractional) { + result = compare_left(a_len - ai, a + ai, b_len - bi, + b + bi, &num_len); + } else { + result = compare_right(a_len - ai, a + ai, b_len - bi, + b + bi, &num_len); + } + + ai += num_len; + bi += num_len; + continue; + } + + if (!ca && !cb) { + /* The strings compare the same. Perhaps the caller + will want to call strcmp to break the tie. */ + *res_out = result; + return 1; + } + + if (ca != '.' || cb != '.') { + return 0; + } + + ++ai; + ++bi; + } + + for (; ai < a_len; ai++) { + if (!isdigit((unsigned char)a[ai]) || a[ai] != '.') { + return 0; + } + } + + for (; bi < b_len; bi++) { + if (!isdigit((unsigned char)b[bi]) || b[bi] != '.') { + return 0; + } + } + + *res_out = result; + return 1; +} + +int strnatcmp(int a_len, nat_char const *a, int b_len, nat_char const *b) +{ + return strnatcmp0(a_len, a, b_len, b, 0); +} + +/* Compare, recognizing numeric string and ignoring case. */ +int strnatcasecmp(int a_len, nat_char const *a, int b_len, nat_char const *b) +{ + return strnatcmp0(a_len, a, b_len, b, 1); +} diff --git a/src/base/strnatcmp.h b/src/base/strnatcmp.h new file mode 100644 index 0000000..24b0c53 --- /dev/null +++ b/src/base/strnatcmp.h @@ -0,0 +1,41 @@ +/* -*- mode: c; c-file-style: "k&r" -*- + + strnatcmp.c -- Perform 'natural order' comparisons of strings in C. + Copyright (C) 2000, 2004 by Martin Pool + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. +*/ + +#ifdef __cplusplus +extern "C" { +#endif + +/* CUSTOMIZATION SECTION + * + * You can change this typedef, but must then also change the inline + * functions in strnatcmp.c */ +typedef char nat_char; + +int strnatcmp(int a_len, nat_char const *a, int b_len, nat_char const *b); + +int strnatcasecmp(int a_len, nat_char const *a, int b_len, nat_char const *b); + +int ipv4cmp(int a_len, nat_char const *a, int b_len, nat_char const *b, int *res_out); + +#ifdef __cplusplus +} +#endif diff --git a/src/base/test_base.cc b/src/base/test_base.cc new file mode 100644 index 0000000..654cd57 --- /dev/null +++ b/src/base/test_base.cc @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2021, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include "doctest/doctest.h" diff --git a/src/base/time_util.cc b/src/base/time_util.cc new file mode 100644 index 0000000..0d46107 --- /dev/null +++ b/src/base/time_util.cc @@ -0,0 +1,239 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file time_util.cc + */ + +#include + +#include "time_util.hh" + +#include "config.h" + +namespace lnav { + +ssize_t +strftime_rfc3339( + char* buffer, size_t buffer_size, lnav::time64_t tim, int millis, char sep) +{ + struct tm gmtm; + int year, month, index = 0; + + secs2tm(tim, &gmtm); + year = gmtm.tm_year + 1900; + month = gmtm.tm_mon + 1; + buffer[index++] = '0' + ((year / 1000) % 10); + buffer[index++] = '0' + ((year / 100) % 10); + buffer[index++] = '0' + ((year / 10) % 10); + buffer[index++] = '0' + ((year / 1) % 10); + buffer[index++] = '-'; + buffer[index++] = '0' + ((month / 10) % 10); + buffer[index++] = '0' + ((month / 1) % 10); + buffer[index++] = '-'; + buffer[index++] = '0' + ((gmtm.tm_mday / 10) % 10); + buffer[index++] = '0' + ((gmtm.tm_mday / 1) % 10); + buffer[index++] = sep; + buffer[index++] = '0' + ((gmtm.tm_hour / 10) % 10); + buffer[index++] = '0' + ((gmtm.tm_hour / 1) % 10); + buffer[index++] = ':'; + buffer[index++] = '0' + ((gmtm.tm_min / 10) % 10); + buffer[index++] = '0' + ((gmtm.tm_min / 1) % 10); + buffer[index++] = ':'; + buffer[index++] = '0' + ((gmtm.tm_sec / 10) % 10); + buffer[index++] = '0' + ((gmtm.tm_sec / 1) % 10); + buffer[index++] = '.'; + buffer[index++] = '0' + ((millis / 100) % 10); + buffer[index++] = '0' + ((millis / 10) % 10); + buffer[index++] = '0' + ((millis / 1) % 10); + buffer[index] = '\0'; + + return index; +} + +} + +static time_t BAD_DATE = -1; + +time_t +tm2sec(const struct tm* t) +{ + int year; + time_t days, secs; + const int dayoffset[12] + = {306, 337, 0, 31, 61, 92, 122, 153, 184, 214, 245, 275}; + + year = t->tm_year; + + if (year < 70) { + return BAD_DATE; + } + if ((sizeof(time_t) <= 4) && (year >= 138)) { + year = 137; + } + + /* shift new year to 1st March in order to make leap year calc easy */ + + if (t->tm_mon < 2) { + year--; + } + + /* Find number of days since 1st March 1900 (in the Gregorian calendar). */ + + days = year * 365 + year / 4 - year / 100 + (year / 100 + 3) / 4; + days += dayoffset[t->tm_mon] + t->tm_mday - 1; + days -= 25508; /* 1 jan 1970 is 25508 days since 1 mar 1900 */ + + secs = ((days * 24 + t->tm_hour) * 60 + t->tm_min) * 60 + t->tm_sec; + + if (secs < 0) { + return BAD_DATE; + } /* must have overflowed */ + else + { +#ifdef HAVE_STRUCT_TM_TM_ZONE + if (t->tm_zone) { + secs -= t->tm_gmtoff; + } +#endif + return secs; + } /* must be a valid time */ +} + +static const int SECSPERMIN = 60; +static const int SECSPERHOUR = 60 * SECSPERMIN; +static const int SECSPERDAY = 24 * SECSPERHOUR; +static const int YEAR_BASE = 1900; +static const int EPOCH_WDAY = 4; +static const int DAYSPERWEEK = 7; +static const int EPOCH_YEAR = 1970; + +#define isleap(y) ((((y) % 4) == 0 && ((y) % 100) != 0) || ((y) % 400) == 0) + +static const int year_lengths[2] = {365, 366}; + +const unsigned short int mon_yday[2][13] = { + /* Normal years. */ + {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}, + /* Leap years. */ + {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}}; + +void +secs2wday(const struct timeval& tv, struct tm* res) +{ + long days, rem; + time_t lcltime; + + /* base decision about std/dst time on current time */ + lcltime = tv.tv_sec; + + days = ((long) lcltime) / SECSPERDAY; + rem = ((long) lcltime) % SECSPERDAY; + while (rem < 0) { + rem += SECSPERDAY; + --days; + } + + /* compute day of week */ + if ((res->tm_wday = ((EPOCH_WDAY + days) % DAYSPERWEEK)) < 0) { + res->tm_wday += DAYSPERWEEK; + } +} + +struct tm* +secs2tm(lnav::time64_t tim, struct tm* res) +{ + long days, rem; + lnav::time64_t lcltime; + int y; + int yleap; + const unsigned short int* ip; + + /* base decision about std/dst time on current time */ + lcltime = tim; + + days = ((long) lcltime) / SECSPERDAY; + rem = ((long) lcltime) % SECSPERDAY; + while (rem < 0) { + rem += SECSPERDAY; + --days; + } + + /* compute hour, min, and sec */ + res->tm_hour = (int) (rem / SECSPERHOUR); + rem %= SECSPERHOUR; + res->tm_min = (int) (rem / SECSPERMIN); + res->tm_sec = (int) (rem % SECSPERMIN); + + /* compute day of week */ + if ((res->tm_wday = ((EPOCH_WDAY + days) % DAYSPERWEEK)) < 0) + res->tm_wday += DAYSPERWEEK; + + /* compute year & day of year */ + y = EPOCH_YEAR; + if (days >= 0) { + for (;;) { + yleap = isleap(y); + if (days < year_lengths[yleap]) + break; + y++; + days -= year_lengths[yleap]; + } + } else { + do { + --y; + yleap = isleap(y); + days += year_lengths[yleap]; + } while (days < 0); + } + + res->tm_year = y - YEAR_BASE; + res->tm_yday = days; + ip = mon_yday[isleap(y)]; + for (y = 11; days < (long int) ip[y]; --y) + continue; + days -= ip[y]; + res->tm_mon = y; + res->tm_mday = days + 1; + + res->tm_isdst = 0; + + return (res); +} + +struct timeval +exttm::to_timeval() const +{ + struct timeval retval; + + retval.tv_sec = tm2sec(&this->et_tm); + retval.tv_usec = std::chrono::duration_cast( + std::chrono::nanoseconds(this->et_nsec)) + .count(); + + return retval; +} diff --git a/src/base/time_util.hh b/src/base/time_util.hh new file mode 100644 index 0000000..ef9687f --- /dev/null +++ b/src/base/time_util.hh @@ -0,0 +1,206 @@ +/** + * Copyright (c) 2020, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef lnav_time_util_hh +#define lnav_time_util_hh + +#include +#include +#include +#include +#include + +#include "config.h" + +namespace lnav { + +using time64_t = uint64_t; + +ssize_t strftime_rfc3339(char* buffer, + size_t buffer_size, + lnav::time64_t tim, + int millis, + char sep = ' '); + +} // namespace lnav + +struct tm* secs2tm(lnav::time64_t tim, struct tm* res); +/** + * Convert the time stored in a 'tm' struct into epoch time. + * + * @param t The 'tm' structure to convert to epoch time. + * @return The given time in seconds since the epoch. + */ +time_t tm2sec(const struct tm* t); +void secs2wday(const struct timeval& tv, struct tm* res); + +inline time_t +convert_log_time_to_local(time_t value) +{ + struct tm tm; + + localtime_r(&value, &tm); +#ifdef HAVE_STRUCT_TM_TM_ZONE + tm.tm_zone = NULL; +#endif + tm.tm_isdst = 0; + return tm2sec(&tm); +} + +constexpr lnav::time64_t MAX_TIME_T = 4000000000LL; + +enum exttm_bits_t { + ETB_YEAR_SET, + ETB_MONTH_SET, + ETB_DAY_SET, + ETB_HOUR_SET, + ETB_MINUTE_SET, + ETB_SECOND_SET, + ETB_MACHINE_ORIENTED, + ETB_EPOCH_TIME, + ETB_MILLIS_SET, + ETB_MICROS_SET, + ETB_NANOS_SET, +}; + +enum exttm_flags_t { + ETF_YEAR_SET = (1UL << ETB_YEAR_SET), + ETF_MONTH_SET = (1UL << ETB_MONTH_SET), + ETF_DAY_SET = (1UL << ETB_DAY_SET), + ETF_HOUR_SET = (1UL << ETB_HOUR_SET), + ETF_MINUTE_SET = (1UL << ETB_MINUTE_SET), + ETF_SECOND_SET = (1UL << ETB_SECOND_SET), + ETF_MACHINE_ORIENTED = (1UL << ETB_MACHINE_ORIENTED), + ETF_EPOCH_TIME = (1UL << ETB_EPOCH_TIME), + ETF_MILLIS_SET = (1UL << ETB_MILLIS_SET), + ETF_MICROS_SET = (1UL << ETB_MICROS_SET), + ETF_NANOS_SET = (1UL << ETB_NANOS_SET), +}; + +struct exttm { + struct tm et_tm {}; + int32_t et_nsec{0}; + unsigned int et_flags{0}; + long et_gmtoff{0}; + + exttm() { memset(&this->et_tm, 0, sizeof(this->et_tm)); } + + bool operator==(const exttm& other) const + { + return memcmp(this, &other, sizeof(exttm)) == 0; + } + + struct timeval to_timeval() const; +}; + +inline bool +operator<(const struct timeval& left, time_t right) +{ + return left.tv_sec < right; +} + +inline bool +operator<(time_t left, const struct timeval& right) +{ + return left < right.tv_sec; +} + +inline bool +operator<(const struct timeval& left, const struct timeval& right) +{ + return left.tv_sec < right.tv_sec + || ((left.tv_sec == right.tv_sec) && (left.tv_usec < right.tv_usec)); +} + +inline bool +operator!=(const struct timeval& left, const struct timeval& right) +{ + return left.tv_sec != right.tv_sec || left.tv_usec != right.tv_usec; +} + +inline bool +operator==(const struct timeval& left, const struct timeval& right) +{ + return left.tv_sec == right.tv_sec || left.tv_usec == right.tv_usec; +} + +inline struct timeval +operator-(const struct timeval& lhs, const struct timeval& rhs) +{ + struct timeval diff; + + timersub(&lhs, &rhs, &diff); + return diff; +} + +typedef int64_t mstime_t; + +inline mstime_t +getmstime() +{ + struct timeval tv; + + gettimeofday(&tv, nullptr); + + return tv.tv_sec * 1000ULL + tv.tv_usec / 1000ULL; +} + +inline struct timeval +current_timeval() +{ + struct timeval retval; + + gettimeofday(&retval, nullptr); + + return retval; +} + +inline struct timespec +current_timespec() +{ + struct timespec retval; + + clock_gettime(CLOCK_REALTIME, &retval); + + return retval; +} + +inline time_t +day_num(time_t ti) +{ + return ti / (24 * 60 * 60); +} + +inline time_t +hour_num(time_t ti) +{ + return ti / (60 * 60); +} + +#endif -- cgit v1.2.3