diff --git a/src/applib/CMakeLists.txt b/src/applib/CMakeLists.txt index 11d332b..04376d8 100644 --- a/src/applib/CMakeLists.txt +++ b/src/applib/CMakeLists.txt @@ -35,6 +35,9 @@ target_sources(applib PRIVATE smartctl_executor.cpp smartctl_executor_gui.h smartctl_executor.h + smartctl_output_type.h + smartctl_version_parser.cpp + smartctl_version_parser.h storage_detector.cpp storage_detector.h storage_detector_helpers.h diff --git a/src/applib/smartctl_ata_text_parser.cpp b/src/applib/smartctl_ata_text_parser.cpp index 37dca04..3e5431f 100644 --- a/src/applib/smartctl_ata_text_parser.cpp +++ b/src/applib/smartctl_ata_text_parser.cpp @@ -23,6 +23,7 @@ Copyright: #include "smartctl_ata_text_parser.h" #include "ata_storage_property_descr.h" #include "warning_colors.h" +#include "smartctl_version_parser.h" #include "build_config.h" @@ -208,7 +209,7 @@ bool SmartctlAtaTextParser::parse_full(const std::string& full, AtaStorageAttrib // version info std::string version, version_full; - if (!parse_version(s, version, version_full)) { + if (!SmartctlVersionParser::parse_version(s, version, version_full)) { set_error_msg("Cannot extract smartctl version information."); debug_out_warn("app", DBG_FUNC_MSG << "Cannot extract version information. Returning.\n"); return false; @@ -231,7 +232,7 @@ bool SmartctlAtaTextParser::parse_full(const std::string& full, AtaStorageAttrib add_property(p); } - if (!check_parsed_version(version, version_full)) { + if (!SmartctlVersionParser::check_parsed_version(SmartctlOutputParserType::Text, version)) { set_error_msg("Incompatible smartctl version."); debug_out_warn("app", DBG_FUNC_MSG << "Incompatible smartctl version. Returning.\n"); return false; @@ -275,48 +276,6 @@ bool SmartctlAtaTextParser::parse_full(const std::string& full, AtaStorageAttrib -// Supply output of "smartctl --version" here. -// returns false on failure. Non-unix newlines in s are ok. -bool SmartctlAtaTextParser::parse_version(const std::string& s, std::string& version, std::string& version_full) -{ - // e.g. - // "smartctl version 5.37" - // "smartctl 5.39" - // "smartctl 5.39 2009-06-03 20:10" (cvs versions) - // "smartctl 5.39 2009-08-08 r2873" (svn versions) - if (!app_pcre_match(R"(/^smartctl (?:version )?(([0-9][^ \t\n\r]+)(?: [0-9 r:-]+)?)/mi)", s, &version_full, &version)) { - debug_out_error("app", DBG_FUNC_MSG << "No smartctl version information found in supplied string.\n"); - return false; - } - hz::string_trim(version_full); - - return true; -} - - - - -// check that the version of smartctl output can be parsed with this parser. -bool SmartctlAtaTextParser::check_parsed_version(const std::string& version_str, [[maybe_unused]] const std::string& version_full_str) -{ - // tested with 5.1-xx versions (1 - 18), and 5.[20 - 38]. - // note: 5.1-11 (maybe others too) with scsi disk gives non-parsable output (why?). - - // 5.0-24, 5.0-36, 5.0-49 tested with data only, from smartmontool site. - // can't fully test 5.0-xx, they don't support sata and I have only sata. - const double minimum_req_version = 5.0; - - double version = 0; - if (hz::string_is_numeric_nolocale(version_str, version, false)) { - if (version >= minimum_req_version) - return true; - } - - return false; -} - - - // convert e.g. "1,000,204,886,016 bytes" to 1.00 TB [931.51 GiB, 1000204886016 bytes]. // Note: this property is present since 5.33. std::string SmartctlAtaTextParser::parse_byte_size(const std::string& str, int64_t& bytes, bool extended) diff --git a/src/applib/smartctl_ata_text_parser.h b/src/applib/smartctl_ata_text_parser.h index 63b003b..a3c6325 100644 --- a/src/applib/smartctl_ata_text_parser.h +++ b/src/applib/smartctl_ata_text_parser.h @@ -28,16 +28,11 @@ class SmartctlAtaTextParser { /// Parse full "smartctl -x" output bool parse_full(const std::string& full, AtaStorageAttribute::DiskType disk_type); - - /// Supply any output of smartctl here, the smartctl version will be retrieved. - static bool parse_version(const std::string& s, std::string& version, std::string& version_full); - - - /// Check that the version of smartctl output can be parsed with this parser. - static bool check_parsed_version(const std::string& version_str, const std::string& version_full_str); - - - /// Convert e.g. "1,000,204,886,016 bytes" to 1.00 TiB [931.51 GB, 1000204886016 bytes] + /// Convert e.g. "1,000,204,886,016 bytes" to "1.00 TiB [931.51 GB, 1000204886016 bytes]" + /// \param str String to parse + /// \param bytes Number of bytes + /// \param extended Return size in other units as well + /// \return Size as a displayable string static std::string parse_byte_size(const std::string& str, int64_t& bytes, bool extended); diff --git a/src/applib/smartctl_output_type.h b/src/applib/smartctl_output_type.h new file mode 100644 index 0000000..bda0ba3 --- /dev/null +++ b/src/applib/smartctl_output_type.h @@ -0,0 +1,53 @@ +/****************************************************************************** +License: GNU General Public License v3.0 only +Copyright: + (C) 2022 Alexander Shaduri +******************************************************************************/ +/// \file +/// \author Alexander Shaduri +/// \ingroup applib +/// \weakgroup applib +/// @{ + +#ifndef SMARTCTL_OUTPUT_TYPE_H +#define SMARTCTL_OUTPUT_TYPE_H + +#include "local_glibmm.h" + +#include "hz/enum_helper.h" + + +enum class SmartctlOutputParserType { + Auto, + Json, + Text, +}; + + + +/// Helper structure for enum-related functions +struct SmartctlOutputParserTypeExt + : public hz::EnumHelper< + SmartctlOutputParserType, + SmartctlOutputParserTypeExt, + Glib::ustring> +{ + static constexpr inline SmartctlOutputParserType default_value = SmartctlOutputParserType::Auto; + + static std::unordered_map> build_enum_map() + { + return { + {SmartctlOutputParserType::Auto, {"auto", _("Automatic")}}, + {SmartctlOutputParserType::Json, {"json", _("JSON")}}, + {SmartctlOutputParserType::Text, {"text", _("Text")}}, + }; + } + +}; + + + + +#endif + +/// @} diff --git a/src/applib/smartctl_version_parser.cpp b/src/applib/smartctl_version_parser.cpp new file mode 100644 index 0000000..9311d6c --- /dev/null +++ b/src/applib/smartctl_version_parser.cpp @@ -0,0 +1,88 @@ +/****************************************************************************** +License: GNU General Public License v3.0 only +Copyright: + (C) 2008 - 2022 Alexander Shaduri +******************************************************************************/ +/// \file +/// \author Alexander Shaduri +/// \ingroup applib +/// \weakgroup applib +/// @{ + +// #include "local_glibmm.h" + +#include "hz/string_algo.h" // string_* +#include "hz/string_num.h" // string_is_numeric, number_to_string +#include "hz/debug.h" // debug_* + +#include "app_pcrecpp.h" +#include "smartctl_version_parser.h" + + + + +bool SmartctlVersionParser::parse_version(const std::string& s, std::string& version_only, std::string& version_full) +{ + // e.g. + // "smartctl version 5.37" + // "smartctl 5.39" + // "smartctl 5.39 2009-06-03 20:10" (cvs versions) + // "smartctl 5.39 2009-08-08 r2873" (svn versions) + // "smartctl 7.3 (build date Feb 11 2022)" (git versions) + if (!app_pcre_match(R"(/^smartctl (?:version )?(([0-9][^ \t\n\r]+)(?: [0-9 r:-]+)?)/mi)", s, &version_full, &version_only)) { + debug_out_error("app", DBG_FUNC_MSG << "No smartctl version information found in supplied string.\n"); + return false; + } + + hz::string_trim(version_only); + hz::string_trim(version_full); + + return true; +} + + + +std::optional SmartctlVersionParser::get_numeric_version(const std::string& version_only) +{ + double numeric_version = 0; + if (!hz::string_is_numeric_nolocale(version_only, numeric_version, false)) { + return std::nullopt; + } + return numeric_version; +} + + + +bool SmartctlVersionParser::check_parsed_version(SmartctlOutputParserType parser_type, const std::string& version_only) +{ + if (auto numeric_version = get_numeric_version(version_only); numeric_version.has_value()) { + switch(parser_type) { + case SmartctlOutputParserType::Auto: + return numeric_version.value() >= minimum_req_text_version || numeric_version.value() >= minimum_req_json_version; + case SmartctlOutputParserType::Json: + return numeric_version.value() >= minimum_req_json_version; + case SmartctlOutputParserType::Text: + return numeric_version.value() >= minimum_req_text_version; + } + } + return false; +} + + + +std::optional SmartctlVersionParser::detect_supported_parser_type(const std::string& version_only) +{ + if (check_parsed_version(SmartctlOutputParserType::Json, version_only)) { + return SmartctlOutputParserType::Json; + } + if (check_parsed_version(SmartctlOutputParserType::Text, version_only)) { + return SmartctlOutputParserType::Text; + } + return std::nullopt; +} + + + + + +/// @} diff --git a/src/applib/smartctl_version_parser.h b/src/applib/smartctl_version_parser.h new file mode 100644 index 0000000..5c1b29c --- /dev/null +++ b/src/applib/smartctl_version_parser.h @@ -0,0 +1,74 @@ +/****************************************************************************** +License: GNU General Public License v3.0 only +Copyright: + (C) 2008 - 2022 Alexander Shaduri +******************************************************************************/ +/// \file +/// \author Alexander Shaduri +/// \ingroup applib +/// \weakgroup applib +/// @{ + +#ifndef SMARTCTL_VERSION_PARSER_H +#define SMARTCTL_VERSION_PARSER_H + +#include "local_glibmm.h" + +#include +#include +#include + +#include "smartctl_output_type.h" + + + +/// Smartctl version parser. +class SmartctlVersionParser { + public: + + /// Supply any text (not JSON) output of smartctl here, the smartctl version will be retrieved. + /// The text does not have to be in Unix newline format. + /// \param command_output "smartctl -V" command output. + /// \param[out] version_only A string similar to "7.2" + /// \param[out] version_full A string similar to "smartctl 7.2 2020-12-30 r5155" + /// \return false if the version could not be parsed. + static bool parse_version(const std::string& command_output, std::string& version_only, std::string& version_full); + + + /// Get numeric version as a double from a parsed version. + /// \param version_only A string similar to "7.2", as parsed by parse_version(). + /// \return Numeric version as a double, e.g. 7.2. std::nullopt if the version could not be parsed. + static std::optional get_numeric_version(const std::string& version_only); + + + /// Check that the version of smartctl output can be parsed with a parser. + static bool check_parsed_version(SmartctlOutputParserType parser_type, const std::string& version_only); + + + /// Detect smartctl parser type based on smartctl version + static std::optional detect_supported_parser_type(const std::string& version_only); + + + private: + + // Text Parser: + // Tested with 5.1-xx versions (1 - 18), and 5.[20 - 38]. + // Note: 5.1-11 (maybe others too) with scsi disk gives non-parsable output (why?). + // 5.0-24, 5.0-36, 5.0-49 tested with data only, from smartmontools site. + // Can't fully test 5.0-xx, they don't support sata, and I have only sata. + static constexpr double minimum_req_text_version = 5.0; + + // JSON in 7.2 is still experimental, but we have to have some cut-off point. + static constexpr double minimum_req_json_version = 7.2; + + +}; + + + + + + +#endif + +/// @} diff --git a/src/applib/storage_device.cpp b/src/applib/storage_device.cpp index ff26f3b..1499950 100644 --- a/src/applib/storage_device.cpp +++ b/src/applib/storage_device.cpp @@ -22,7 +22,7 @@ Copyright: #include "smartctl_ata_text_parser.h" #include "storage_settings.h" #include "smartctl_executor.h" - +#include "smartctl_version_parser.h" @@ -144,7 +144,7 @@ std::string StorageDevice::parse_basic_data(bool do_set_properties, bool emit_si } std::string version, version_full; - if (!SmartctlAtaTextParser::parse_version(this->info_output_, version, version_full)) // is this smartctl data at all? + if (!SmartctlVersionParser::parse_version(this->info_output_, version, version_full)) // is this smartctl data at all? return _("Cannot get smartctl version information."); // Detect type. note: we can't distinguish between sata and scsi (on linux, for -d ata switch). diff --git a/src/applib/tests/CMakeLists.txt b/src/applib/tests/CMakeLists.txt index 983b79b..dbc4f46 100644 --- a/src/applib/tests/CMakeLists.txt +++ b/src/applib/tests/CMakeLists.txt @@ -15,6 +15,7 @@ endif() add_library(applib_tests OBJECT) target_sources(applib_tests PRIVATE test_app_pcrecpp.cpp + test_smartctl_version_parser.cpp ) target_link_libraries(applib_tests PRIVATE applib diff --git a/src/applib/tests/test_smartctl_version_parser.cpp b/src/applib/tests/test_smartctl_version_parser.cpp new file mode 100644 index 0000000..25ea673 --- /dev/null +++ b/src/applib/tests/test_smartctl_version_parser.cpp @@ -0,0 +1,105 @@ +/****************************************************************************** +License: BSD Zero Clause License +Copyright: + (C) 2022 Alexander Shaduri +******************************************************************************/ +/// \file +/// \author Alexander Shaduri +/// \ingroup applib_tests +/// \weakgroup applib_tests +/// @{ + +// Catch2 v3 +//#include "catch2/catch_test_macros.hpp" + +// Catch2 v2 +#include "catch2/catch.hpp" + +#include "applib/smartctl_version_parser.h" + + + +TEST_CASE("SmartctlVersionParser", "[app][parser]") +{ + std::string version_only, version_full; + + SECTION("Parse with version keyword") { + SmartctlVersionParser::parse_version("smartctl version 5.37", version_only, version_full); + REQUIRE(version_only == "5.37"); + REQUIRE(version_full == "5.37"); + } + + SECTION("Parse without version keyword") { + SmartctlVersionParser::parse_version("smartctl 5.39", version_only, version_full); + REQUIRE(version_only == "5.39"); + REQUIRE(version_full == "5.39"); + } + + SECTION("Parse with date (CVS)") { + SmartctlVersionParser::parse_version("smartctl 5.39 2009-06-03 20:10", version_only, version_full); + REQUIRE(version_only == "5.39"); + REQUIRE(version_full == "5.39 2009-06-03 20:10"); + } + + SECTION("Parse with date (SVN)") { + SmartctlVersionParser::parse_version("smartctl 5.39 2009-08-08 r2873", version_only, version_full); + REQUIRE(version_only == "5.39"); + REQUIRE(version_full == "5.39 2009-08-08 r2873"); + } + + SECTION("Parse old 5.0") { + SmartctlVersionParser::parse_version("smartctl version 5.0-49", version_only, version_full); + REQUIRE(version_only == "5.0-49"); + REQUIRE(version_full == "5.0-49"); + } + + SECTION("Parse full output (SVN)") { + std::string output = +R"(smartctl 7.2 2020-12-30 r5155 [x86_64-linux-5.3.18-lp152.66-default] (SUSE RPM) +Copyright (C) 2002-20, Bruce Allen, Christian Franke, www.smartmontools.org + +smartctl comes with ABSOLUTELY NO WARRANTY. This is free +software, and you are welcome to redistribute it under +the terms of the GNU General Public License; either +version 2, or (at your option) any later version. +See http://www.gnu.org for further details. + +smartmontools release 7.2 dated 2020-12-30 at 16:48:30 UTC +smartmontools SVN rev 5155 dated 2020-12-30 at 16:49:18 +smartmontools build host: x86_64-suse-linux-gnu +smartmontools build with: C++14, GCC 7.5.0 +smartmontools configure arguments: '--host=x86_64-suse-linux-gnu' '--build=x86_64-suse-linux-gnu' '--program-prefix=' '--prefix=/usr' '--exec-prefix=/usr' '--bindir=/usr/bin' '--sbindir=/usr/sbin' '--sysconfdir=/etc' '--datadir=/usr/share' '--includedir=/usr/include' '--libdir=/usr/lib64' '--libexecdir=/usr/lib' '--localstatedir=/var' '--sharedstatedir=/var/lib' '--mandir=/usr/share/man' '--infodir=/usr/share/info' '--disable-dependency-tracking' '--docdir=/usr/share/doc/packages/smartmontools' '--with-selinux' '--with-libsystemd' '--with-systemdsystemunitdir=/usr/lib/systemd/system' '--with-savestates' '--with-attributelog' '--with-nvme-devicescan' 'build_alias=x86_64-suse-linux-gnu' 'host_alias=x86_64-suse-linux-gnu' 'CXXFLAGS=-O2 -g -m64 -fmessage-length=0 -D_FORTIFY_SOURCE=2 -fstack-protector -funwind-tables -fasynchronous-unwind-tables -fPIE ' 'LDFLAGS=-pie' 'CFLAGS=-O2 -g -m64 -fmessage-length=0 -D_FORTIFY_SOURCE=2 -fstack-protector -funwind-tables -fasynchronous-unwind-tables -fPIE' 'PKG_CONFIG_PATH=:/usr/lib64/pkgconfig:/usr/share/pkgconfig' +)"; + SmartctlVersionParser::parse_version(output, version_only, version_full); + REQUIRE(version_only == "7.2"); + REQUIRE(version_full == "7.2 2020-12-30 r5155"); + } + + SECTION("Parse full output (git)") { + std::string output = +R"(smartctl 7.3 (build date Feb 11 2022) [x86_64-linux-5.3.18-lp152.66-default] (local build) +Copyright (C) 2002-22, Bruce Allen, Christian Franke, www.smartmontools.org + +smartctl comes with ABSOLUTELY NO WARRANTY. This is free +software, and you are welcome to redistribute it under +the terms of the GNU General Public License; either +version 2, or (at your option) any later version. +See https://www.gnu.org for further details. + +smartmontools release 7.3 dated 2020-12-30 at 16:48:30 UTC +smartmontools SVN rev is unknown +smartmontools build host: x86_64-pc-linux-gnu +smartmontools build with: C++11, GCC 7.5.0 +smartmontools configure arguments: [no arguments given] + +)"; + SmartctlVersionParser::parse_version(output, version_only, version_full); + REQUIRE(version_only == "7.3"); + REQUIRE(version_full == "7.3"); + } + +} + + + +/// @} diff --git a/src/gsc_main_window.cpp b/src/gsc_main_window.cpp index 960fcd8..652dc4e 100644 --- a/src/gsc_main_window.cpp +++ b/src/gsc_main_window.cpp @@ -21,13 +21,13 @@ Copyright: #include "hz/fs.h" #include "rconfig/rconfig.h" #include "applib/storage_detector.h" -#include "applib/smartctl_ata_text_parser.h" #include "applib/gui_utils.h" // gui_show_error_dialog #include "applib/smartctl_executor.h" // get_smartctl_binary() #include "applib/smartctl_executor_gui.h" #include "applib/app_gtkmm_tools.h" // app_gtkmm_* #include "applib/warning_colors.h" // app_property_get_label_highlight_color #include "applib/app_pcrecpp.h" // app_pcre_match +#include "applib/smartctl_version_parser.h" #include "gsc_init.h" // app_quit() #include "gsc_about_dialog.h" @@ -118,7 +118,7 @@ GscMainWindow::GscMainWindow(BaseObjectType* gtkcobj, Glib::RefPtr } std::string version, version_full; - if (!SmartctlAtaTextParser::parse_version(output, version, version_full)) { + if (!SmartctlVersionParser::parse_version(output, version, version_full)) { error_msg = _("Smartctl returned invalid output."); break; } diff --git a/src/hz/CMakeLists.txt b/src/hz/CMakeLists.txt index aeda7d0..02f6ea3 100644 --- a/src/hz/CMakeLists.txt +++ b/src/hz/CMakeLists.txt @@ -11,6 +11,7 @@ target_sources(hz INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/bad_cast_exception.h ${CMAKE_CURRENT_SOURCE_DIR}/data_file.h ${CMAKE_CURRENT_SOURCE_DIR}/debug.h + ${CMAKE_CURRENT_SOURCE_DIR}/enum_helper.h ${CMAKE_CURRENT_SOURCE_DIR}/env_tools.h ${CMAKE_CURRENT_SOURCE_DIR}/error.h ${CMAKE_CURRENT_SOURCE_DIR}/error_holder.h diff --git a/src/hz/enum_helper.h b/src/hz/enum_helper.h new file mode 100644 index 0000000..30d5bc5 --- /dev/null +++ b/src/hz/enum_helper.h @@ -0,0 +1,129 @@ +/****************************************************************************** +License: GNU General Public License v3.0 only +Copyright: + (C) 2022 Alexander Shaduri +******************************************************************************/ +/// \file +/// \author Alexander Shaduri +/// \ingroup hz +/// \weakgroup hz +/// @{ + +#ifndef ENUM_HELPER_H +#define ENUM_HELPER_H + +#include +#include +#include +#include +#include + + + +namespace hz { + + +/// Helper class for defining enum-related functions. +/// In EnumExtClass it expects the following (accessible) members: +/// - static inline EnumType default_value = ...; +/// - static const std::unordered_map>& get_enum_static_map(); +template +class EnumHelper { + public: + + using EnumType = Enum; + using EnumMapType = std::unordered_map>; + + + /// Return storable name of an enum member + static std::string get_storable_name(EnumType enum_value) + { + const auto& m = get_enum_static_map(); + // Iterator: enum -> pair{storable, displayable} + auto iter = m.find(enum_value); + return iter != m.end() ? std::string(iter->second.first) : std::string(); + } + + + /// Return an enum member by its storable name + static EnumType get_by_storable_name(const std::string& storable_name, + EnumType default_value = EnumExtClass::default_value) + { + const auto& m = get_storable_enum_static_map(); + // Iterator: storable_name -> enum + auto iter = m.find(storable_name); + return iter != m.end() ? iter->second : default_value; + } + + + /// Return displayable name of an enum member + static DisplayableStringType get_displayable_name(EnumType enum_value) + { + const auto& m = get_enum_static_map(); + // Iterator: enum -> pair{storable, displayable} + auto iter = m.find(enum_value); + return iter != m.end() ? DisplayableStringType(iter->second.second) : DisplayableStringType(); + } + + + /// Return all possible members of an enum + static std::vector getAllValues() + { + static const auto v = build_enum_value_list(); + return v; + } + + + private: + + /// Get a static map of storable names to enum values. + static const EnumMapType& get_enum_static_map() + { + static const auto m = EnumExtClass::build_enum_map(); + return m; + } + + + /// Get a static map of storable names to enum values. + static const std::unordered_map& get_storable_enum_static_map() + { + static const auto m = build_storable_enum_map(); + return m; + } + + + /// Build a map of storable names to enum values. + static std::unordered_map build_storable_enum_map() + { + std::unordered_map m; + for (const auto& [data, enum_value] : EnumExtClass::get_enum_static_map()) { + m.emplace(data.first, enum_value); + } + return m; + } + + + /// Build a list of enum values from get_enum_static_map() + static std::vector build_enum_value_list() + { + const auto& m = EnumExtClass::get_enum_static_map(); + std::vector v; + v.reserve(m.size()); + for (const auto& p : m) { + v.push_back(p.first); + } + std::sort(v.begin(), v.end()); + return v; + } + +}; + + + +} + + + +#endif + +/// @}