diff --git a/src/config/config.cpp b/src/config/config.cpp index d4c6ae0..fdce910 100644 --- a/src/config/config.cpp +++ b/src/config/config.cpp @@ -10,10 +10,10 @@ #include // toml::table #include // getuid -#include "src/core/util/defs.hpp" -#include "src/core/util/helpers.hpp" -#include "src/core/util/logging.hpp" -#include "src/core/util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/helpers.hpp" +#include "src/util/logging.hpp" +#include "src/util/types.hpp" namespace fs = std::filesystem; diff --git a/src/config/config.hpp b/src/config/config.hpp index 85d04ee..7cd42cc 100644 --- a/src/config/config.hpp +++ b/src/config/config.hpp @@ -12,13 +12,13 @@ #include // getpwuid, passwd #include // getuid - #include "src/core/util/helpers.hpp" + #include "src/util/helpers.hpp" #endif -#include "src/core/util/defs.hpp" -#include "src/core/util/error.hpp" -#include "src/core/util/logging.hpp" -#include "src/core/util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/logging.hpp" +#include "src/util/types.hpp" #include "weather.hpp" diff --git a/src/config/weather.cpp b/src/config/weather.cpp index e2bf73a..e8fe108 100644 --- a/src/config/weather.cpp +++ b/src/config/weather.cpp @@ -15,14 +15,14 @@ #include // NOLINT(misc-include-cleaner) - glaze/json/read.hpp is needed for glz::read #include // std::ios::{binary, trunc} #include // std::istreambuf_iterator -#include // std::error_code #include // std::move #include // std::{get, holds_alternative} -#include "src/core/util/defs.hpp" -#include "src/core/util/error.hpp" -#include "src/core/util/logging.hpp" -#include "src/core/util/types.hpp" +#include "src/util/cache.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/logging.hpp" +#include "src/util/types.hpp" #include "config.hpp" @@ -35,109 +35,10 @@ namespace { using util::error::DracError, util::error::DracErrorCode; using util::types::usize, util::types::Err, util::types::Exception; using weather::Coords; + using namespace util::cache; constexpr opts glaze_opts = { .error_on_unknown_keys = false }; - fn GetCachePath() -> Result { - std::error_code errc; - fs::path cachePath = fs::temp_directory_path(errc); - - if (errc) - return Err("Failed to get temp directory: " + errc.message()); - - cachePath /= "weather_cache.beve"; - return cachePath; - } - - fn ReadCacheFromFile() -> Result { - Result cachePath = GetCachePath(); - - if (!cachePath) - return Err(cachePath.error()); - - std::ifstream ifs(*cachePath, std::ios::binary); - - if (!ifs.is_open()) - return Err("Cache file not found: " + cachePath->string()); - - debug_log("Reading from cache file..."); - - try { - const String content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); - ifs.close(); - - if (content.empty()) - return Err(std::format("BEVE cache file is empty: {}", cachePath->string())); - - Output result; - - if (const error_ctx glazeErr = read_beve(result, content); glazeErr.ec != error_code::none) - return Err(std::format( - "BEVE parse error reading cache (code {}): {}", static_cast(glazeErr.ec), cachePath->string() - )); - - debug_log("Successfully read from cache file."); - return result; - } catch (const Exception& e) { return Err(std::format("Error reading cache: {}", e.what())); } - } - - fn WriteCacheToFile(const Output& data) -> Result { - using util::types::isize; - - Result cachePath = GetCachePath(); - - if (!cachePath) - return Err(cachePath.error()); - - debug_log("Writing to cache file..."); - fs::path tempPath = *cachePath; - tempPath += ".tmp"; - - try { - String binaryBuffer; - - if (const error_ctx glazeErr = write_beve(data, binaryBuffer); glazeErr) - return Err(std::format("BEVE serialization error writing cache (code {})", static_cast(glazeErr.ec))); - - { - std::ofstream ofs(tempPath, std::ios::binary | std::ios::trunc); - if (!ofs.is_open()) - return Err("Failed to open temp file: " + tempPath.string()); - - ofs.write(binaryBuffer.data(), static_cast(binaryBuffer.size())); - if (!ofs) { - std::error_code removeEc; - fs::remove(tempPath, removeEc); - return Err("Failed to write to temp BEVE cache file"); - } - } - - std::error_code errc; - fs::rename(tempPath, *cachePath, errc); - if (errc) { - if (!fs::remove(tempPath, errc)) - debug_log("Failed to remove temp file: {}", errc.message()); - - return Err(std::format("Failed to replace cache file: {}", errc.message())); - } - - debug_log("Successfully wrote to cache file."); - return {}; - } catch (const std::ios_base::failure& e) { - std::error_code removeEc; - fs::remove(tempPath, removeEc); - return Err(std::format("Filesystem error writing BEVE cache file {}: {}", tempPath.string(), e.what())); - } catch (const Exception& e) { - std::error_code removeEc; - fs::remove(tempPath, removeEc); - return Err(std::format("File operation error during BEVE cache write: {}", e.what())); - } catch (...) { - std::error_code removeEc; - fs::remove(tempPath, removeEc); - return Err(std::format("Unknown error writing BEVE cache file: {}", tempPath.string())); - } - } - fn WriteCallback(void* contents, const usize size, const usize nmemb, String* str) -> usize { const usize totalSize = size * nmemb; str->append(static_cast(contents), totalSize); @@ -177,7 +78,7 @@ fn Weather::getWeatherInfo() const -> Result { using namespace std::chrono; using util::types::i32; - if (Result data = ReadCacheFromFile()) { + if (Result data = ReadCache("weather")) { const Output& dataVal = *data; if (const duration cacheAge = system_clock::now() - system_clock::time_point(seconds(dataVal.dt)); @@ -188,15 +89,15 @@ fn Weather::getWeatherInfo() const -> Result { debug_log("Cache expired"); } else { - debug_log("Cache error: {}", data.error()); + error_at(data.error()); } fn handleApiResult = [](const Result& result) -> Result { if (!result) return Err(DracError(DracErrorCode::ApiUnavailable, result.error())); - if (Result writeResult = WriteCacheToFile(*result); !writeResult) - error_log("Failed to write cache: {}", writeResult.error()); + if (Result writeResult = WriteCache("weather", *result); !writeResult) + error_at(writeResult.error()); return *result; }; diff --git a/src/config/weather.hpp b/src/config/weather.hpp index 935d1e2..747173d 100644 --- a/src/config/weather.hpp +++ b/src/config/weather.hpp @@ -3,7 +3,7 @@ #include // object #include // Object -#include "src/core/util/types.hpp" +#include "src/util/types.hpp" namespace weather { using glz::detail::Object, glz::object; diff --git a/src/core/package.hpp b/src/core/package.hpp new file mode 100644 index 0000000..88caf00 --- /dev/null +++ b/src/core/package.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include // std::filesystem::path +#include // std::future +#include // glz::object +#include // glz::detail::Object +#include // std::vector + +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/types.hpp" + +namespace packages { + + namespace fs = std::filesystem; + using util::error::DracError; + using util::types::Result, util::types::u64, util::types::i64, util::types::String, util::types::Vec, + util::types::Future; + + /** + * @struct PkgCountCacheData + * @brief Structure for caching package count results along with a timestamp. + * + * Used to avoid redundant lookups in package manager databases or directories + * if the underlying data source hasn't changed recently. + */ + struct PkgCountCacheData { + u64 count {}; ///< The cached package count. + i64 timestampEpochSeconds {}; ///< The UNIX timestamp (seconds since epoch) when the count was cached. + + // NOLINTBEGIN(readability-identifier-naming) + struct [[maybe_unused]] glaze { + using T = PkgCountCacheData; + static constexpr glz::detail::Object value = + glz::object("count", &T::count, "timestamp", &T::timestampEpochSeconds); + }; + // NOLINTEND(readability-identifier-naming) + }; + + /** + * @struct PackageManagerInfo + * @brief Holds information needed to query a database-backed package manager. + */ + struct PackageManagerInfo { + String id; ///< Unique identifier for the package manager (used for cache key). + fs::path dbPath; ///< Filesystem path to the package manager's database. + String countQuery; ///< SQL query string to count the packages. + }; + + /** + * @brief Gets the total package count by querying all relevant package managers for the current OS. + * @details This function orchestrates the process: + * 1. Determines the set of relevant package managers (platform-specific + shared). + * 2. Launches asynchronous tasks to query each manager. + * 3. Aggregates the results, summing counts and logging errors. + * @return Result containing the total package count (u64) on success, + * or a DracError if the aggregation fails (though individual manager errors are logged). + */ + fn GetTotalCount() -> Result; + + fn GetCountFromDb(const PackageManagerInfo& pmInfo) -> Result; + + fn GetCountFromDirectory( + const String& pmId, + const fs::path& dirPath, + const String& fileExtensionFilter = "", + bool subtractOne = false + ) -> Result; + +#ifdef __linux__ + fn GetDpkgCount() -> Result; + fn GetPacmanCount() -> Result; + fn GetMossCount() -> Result; + fn GetRpmCount() -> Result; + fn GetZypperCount() -> Result; + fn GetPortageCount() -> Result; + fn GetApkCount() -> Result; +#elif defined(__APPLE__) + fn GetHomebrewCount() -> Result; + fn GetMacPortsCount() -> Result; +#elif defined(_WIN32) + fn GetWinRTCount() -> Result; + fn GetChocolateyCount() -> Result; + fn GetScoopCount() -> Result; +#endif + +#ifndef _WIN32 + fn GetNixCount() -> Result; +#endif + fn GetCargoCount() -> Result; +} // namespace packages \ No newline at end of file diff --git a/src/core/system_data.cpp b/src/core/system_data.cpp index 818e062..971961d 100644 --- a/src/core/system_data.cpp +++ b/src/core/system_data.cpp @@ -7,10 +7,10 @@ #include "src/config/config.hpp" #include "src/config/weather.hpp" -#include "src/core/util/defs.hpp" -#include "src/core/util/error.hpp" -#include "src/core/util/types.hpp" #include "src/os/os.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/types.hpp" using util::error::DracError, util::error::DracErrorCode; diff --git a/src/core/system_data.hpp b/src/core/system_data.hpp index b354bb7..37194c6 100644 --- a/src/core/system_data.hpp +++ b/src/core/system_data.hpp @@ -4,9 +4,8 @@ #include "src/config/config.hpp" // Config #include "src/config/weather.hpp" // weather::Output - -#include "util/defs.hpp" -#include "util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/types.hpp" struct Config; diff --git a/src/main.cpp b/src/main.cpp index 0802fb3..b187418 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,9 +10,9 @@ #include "src/config/config.hpp" #include "src/config/weather.hpp" #include "src/core/system_data.hpp" -#include "src/core/util/defs.hpp" -#include "src/core/util/logging.hpp" -#include "src/core/util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/logging.hpp" +#include "src/util/types.hpp" namespace ui { using ftxui::Color; diff --git a/src/os/linux.cpp b/src/os/linux.cpp index 903acae..1085c66 100644 --- a/src/os/linux.cpp +++ b/src/os/linux.cpp @@ -18,11 +18,11 @@ #include // readlink #include // std::move -#include "src/core/util/defs.hpp" -#include "src/core/util/error.hpp" -#include "src/core/util/helpers.hpp" -#include "src/core/util/logging.hpp" -#include "src/core/util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/helpers.hpp" +#include "src/util/logging.hpp" +#include "src/util/types.hpp" #include "src/wrappers/dbus.hpp" #include "src/wrappers/wayland.hpp" #include "src/wrappers/xcb.hpp" diff --git a/src/os/linux/pkg_count.cpp b/src/os/linux/pkg_count.cpp index ab9b79e..5f28285 100644 --- a/src/os/linux/pkg_count.cpp +++ b/src/os/linux/pkg_count.cpp @@ -15,13 +15,13 @@ #include // std::istreambuf_iterator #include // glz::read_beve #include // glz::write_beve -#include // glz::{context, error_code, error_ctx} #include // std::error_code -#include "src/core/util/defs.hpp" -#include "src/core/util/error.hpp" -#include "src/core/util/logging.hpp" -#include "src/core/util/types.hpp" +#include "src/util/cache.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/logging.hpp" +#include "src/util/types.hpp" // clang-format on using util::error::DracError, util::error::DracErrorCode; @@ -31,6 +31,7 @@ using util::types::u64, util::types::i64, util::types::Result, util::types::Err, namespace { namespace fs = std::filesystem; using namespace std::chrono; + using namespace util::cache; using os::linux::PkgCountCacheData, os::linux::PackageManagerInfo; fn GetPackageCountInternalDir( @@ -111,6 +112,68 @@ namespace { return count; } + + fn GetPackageCountInternalDb(const PackageManagerInfo& pmInfo) -> Result { + const auto& [pmId, dbPath, countQuery] = pmInfo; + + if (Result cachedDataResult = ReadCache(pmId)) { + const auto& [count, timestamp] = *cachedDataResult; + std::error_code errc; + const std::filesystem::file_time_type dbModTime = fs::last_write_time(dbPath, errc); + + if (errc) { + warn_log( + "Could not get modification time for '{}': {}. Invalidating {} cache.", dbPath.string(), errc.message(), pmId + ); + } else { + if (const system_clock::time_point cacheTimePoint = system_clock::time_point(seconds(timestamp)); + cacheTimePoint.time_since_epoch() >= dbModTime.time_since_epoch()) { + debug_log( + "Using valid {} package count cache (DB file unchanged since {}).", + pmId, + std::format("{:%F %T %Z}", floor(cacheTimePoint)) + ); + return count; + } + debug_log("{} package count cache stale (DB file modified).", pmId); + } + } else { + if (cachedDataResult.error().code != DracErrorCode::NotFound) + debug_at(cachedDataResult.error()); + debug_log("{} package count cache not found or unreadable.", pmId); + } + + debug_log("Fetching fresh {} package count from database: {}", pmId, dbPath.string()); + u64 count = 0; + + try { + const SQLite::Database database(dbPath.string(), SQLite::OPEN_READONLY); + if (SQLite::Statement query(database, countQuery); query.executeStep()) { + const i64 countInt64 = query.getColumn(0).getInt64(); + if (countInt64 < 0) + return Err( + DracError(DracErrorCode::ParseError, std::format("Negative count returned by {} DB COUNT query.", pmId)) + ); + count = static_cast(countInt64); + } else { + return Err(DracError(DracErrorCode::ParseError, std::format("No rows returned by {} DB COUNT query.", pmId))); + } + } catch (const SQLite::Exception& e) { + return Err(DracError( + DracErrorCode::ApiUnavailable, std::format("SQLite error occurred accessing {} DB: {}", pmId, e.what()) + )); + } catch (const Exception& e) { return Err(DracError(DracErrorCode::InternalError, e.what())); } catch (...) { + return Err(DracError(DracErrorCode::Other, std::format("Unknown error occurred accessing {} DB", pmId))); + } + + const i64 nowEpochSeconds = duration_cast(system_clock::now().time_since_epoch()).count(); + const PkgCountCacheData dataToCache = { .count = count, .timestampEpochSeconds = nowEpochSeconds }; + + if (Result writeResult = WriteCache(pmId, dataToCache); !writeResult) + error_at(writeResult.error()); + + return count; + } } // namespace namespace os::linux { @@ -159,10 +222,9 @@ namespace os::linux { fn GetTotalPackageCount() -> Result { using util::types::Array, util::types::Future; - Array>, 4> futures = { + Array>, 3> futures = { std::async(std::launch::async, GetDpkgPackageCount), std::async(std::launch::async, GetMossPackageCount), - std::async(std::launch::async, GetNixPackageCount), std::async(std::launch::async, GetPacmanPackageCount), }; diff --git a/src/os/linux/pkg_count.hpp b/src/os/linux/pkg_count.hpp index 39d3124..89fc7f9 100644 --- a/src/os/linux/pkg_count.hpp +++ b/src/os/linux/pkg_count.hpp @@ -7,9 +7,9 @@ #include // glz::object #include // glz::detail::Object -#include "src/core/util/defs.hpp" -#include "src/core/util/error.hpp" -#include "src/core/util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/types.hpp" // clang-format on namespace os::linux { diff --git a/src/os/macos.cpp b/src/os/macos.cpp index 6d90e08..232a896 100644 --- a/src/os/macos.cpp +++ b/src/os/macos.cpp @@ -7,9 +7,9 @@ #include "macos/bridge.hpp" #include "os.hpp" -#include "src/core/util/defs.hpp" -#include "src/core/util/error.hpp" -#include "src/core/util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/types.hpp" // clang-format on using namespace util::types; diff --git a/src/os/macos/bridge.hpp b/src/os/macos/bridge.hpp index 13d2248..14d2e2f 100644 --- a/src/os/macos/bridge.hpp +++ b/src/os/macos/bridge.hpp @@ -3,9 +3,9 @@ #ifdef __APPLE__ // clang-format off -#include "src/core/util/defs.hpp" -#include "src/core/util/error.hpp" -#include "src/core/util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/types.hpp" // clang-format on using util::error::DracError; using util::types::MediaInfo, util::types::String, util::types::Result; diff --git a/src/os/macos/bridge.mm b/src/os/macos/bridge.mm index 4bdd8eb..8a26b41 100644 --- a/src/os/macos/bridge.mm +++ b/src/os/macos/bridge.mm @@ -11,7 +11,7 @@ #include #include -#include "src/core/util/error.hpp" +#include "src/util/error.hpp" // clang-format on using util::error::DracErrorCode; diff --git a/src/os/os.hpp b/src/os/os.hpp index 7bc81d1..237e407 100644 --- a/src/os/os.hpp +++ b/src/os/os.hpp @@ -1,8 +1,8 @@ #pragma once -#include "src/core/util/defs.hpp" -#include "src/core/util/error.hpp" -#include "src/core/util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/types.hpp" /** * @namespace os diff --git a/src/os/shared.cpp b/src/os/shared.cpp index 6186979..e435bf1 100644 --- a/src/os/shared.cpp +++ b/src/os/shared.cpp @@ -1,24 +1,24 @@ -#include // SQLite::{Database, OPEN_READONLY} -#include // SQLite::Exception -#include // SQLite::Statement -#include // std::chrono -#include // std::filesystem -#include // std::format -#include // std::{ifstream, ofstream} -#include // glz::read_beve -#include // glz::write_beve -#include // glz::object -#include // glz::{context, error_code, error_ctx} -#include // glz::detail::Object -#include // std::ios::{binary, trunc}, std::ios_base -#include // std::istreambuf_iterator -#include // std::error_code +#include // SQLite::{Database, OPEN_READONLY} +#include // SQLite::Exception +#include // SQLite::Statement +#include // std::chrono +#include // std::filesystem +#include // std::format +#include // std::{ifstream, ofstream} +#include // glz::read_beve +#include // glz::write_beve +#include // glz::object +#include // glz::detail::Object +#include // std::ios::{binary, trunc}, std::ios_base +#include // std::istreambuf_iterator +#include // std::error_code -#include "src/core/util/defs.hpp" -#include "src/core/util/error.hpp" -#include "src/core/util/helpers.hpp" -#include "src/core/util/logging.hpp" -#include "src/core/util/types.hpp" +#include "src/util/cache.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/helpers.hpp" +#include "src/util/logging.hpp" +#include "src/util/types.hpp" #include "os.hpp" @@ -30,6 +30,7 @@ namespace fs = std::filesystem; namespace { using namespace std::chrono; + using namespace util::cache; struct PackageManagerInfo { String id; @@ -51,136 +52,10 @@ namespace { // NOLINTEND(readability-identifier-naming) }; - constexpr StringView ALLOWED_PMID_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"; - - fn GetPkgCountCachePath(const String& pmId) -> Result { - std::error_code errc; - const fs::path cacheDir = fs::temp_directory_path(errc); - - if (errc) - return Err(DracError(DracErrorCode::IoError, "Failed to get temp directory: " + errc.message())); - - if (pmId.empty() || pmId.find_first_not_of(ALLOWED_PMID_CHARS) != String::npos) - return Err(DracError(DracErrorCode::ParseError, "Invalid package manager ID for cache path: " + pmId)); - - return cacheDir / (pmId + "_pkg_count_cache.beve"); - } - - fn ReadPkgCountCache(const String& pmId) -> Result { - Result cachePathResult = GetPkgCountCachePath(pmId); - - if (!cachePathResult) - return Err(cachePathResult.error()); - - const fs::path& cachePath = *cachePathResult; - - if (!fs::exists(cachePath)) - return Err(DracError(DracErrorCode::NotFound, "Cache file not found: " + cachePath.string())); - - std::ifstream ifs(cachePath, std::ios::binary); - if (!ifs.is_open()) - return Err(DracError(DracErrorCode::IoError, "Failed to open cache file for reading: " + cachePath.string())); - - try { - const String content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); - ifs.close(); - - if (content.empty()) - return Err(DracError(DracErrorCode::ParseError, "BEVE cache file is empty: " + cachePath.string())); - - PkgCountCacheData result; - const glz::context ctx {}; - - if (glz::error_ctx glazeResult = glz::read_beve(result, content); glazeResult.ec != glz::error_code::none) - return Err(DracError( - DracErrorCode::ParseError, - std::format( - "BEVE parse error reading cache (code {}): {}", static_cast(glazeResult.ec), cachePath.string() - ) - )); - - return result; - } catch (const std::ios_base::failure& e) { - return Err(DracError( - DracErrorCode::IoError, std::format("Filesystem error reading cache file {}: {}", cachePath.string(), e.what()) - )); - } catch (const Exception& e) { - return Err(DracError(DracErrorCode::InternalError, std::format("Error reading package count cache: {}", e.what())) - ); - } - } - - fn WritePkgCountCache(const String& pmId, const PkgCountCacheData& data) -> Result { - using util::types::isize; - - Result cachePathResult = GetPkgCountCachePath(pmId); - - if (!cachePathResult) - return Err(cachePathResult.error()); - - const fs::path& cachePath = *cachePathResult; - fs::path tempPath = cachePath; - tempPath += ".tmp"; - - try { - String binaryBuffer; - - PkgCountCacheData mutableData = data; - - if (glz::error_ctx glazeErr = glz::write_beve(mutableData, binaryBuffer); glazeErr) - return Err(DracError( - DracErrorCode::ParseError, - std::format("BEVE serialization error writing cache (code {})", static_cast(glazeErr.ec)) - )); - - { - std::ofstream ofs(tempPath, std::ios::binary | std::ios::trunc); - if (!ofs.is_open()) - return Err(DracError(DracErrorCode::IoError, "Failed to open temp cache file: " + tempPath.string())); - - ofs.write(binaryBuffer.data(), static_cast(binaryBuffer.size())); - - if (!ofs) { - std::error_code removeEc; - fs::remove(tempPath, removeEc); - return Err(DracError(DracErrorCode::IoError, "Failed to write to temp cache file: " + tempPath.string())); - } - } - - std::error_code errc; - fs::rename(tempPath, cachePath, errc); - if (errc) { - fs::remove(tempPath, errc); - return Err(DracError( - DracErrorCode::IoError, - std::format("Failed to replace cache file '{}': {}", cachePath.string(), errc.message()) - )); - } - - return {}; - } catch (const std::ios_base::failure& e) { - std::error_code removeEc; - fs::remove(tempPath, removeEc); - return Err(DracError( - DracErrorCode::IoError, std::format("Filesystem error writing cache file {}: {}", tempPath.string(), e.what()) - )); - } catch (const Exception& e) { - std::error_code removeEc; - fs::remove(tempPath, removeEc); - return Err(DracError(DracErrorCode::InternalError, std::format("Error writing package count cache: {}", e.what())) - ); - } catch (...) { - std::error_code removeEc; - fs::remove(tempPath, removeEc); - return Err(DracError(DracErrorCode::Other, std::format("Unknown error writing cache file: {}", tempPath.string())) - ); - } - } - fn GetPackageCountInternalDb(const PackageManagerInfo& pmInfo) -> Result { const auto& [pmId, dbPath, countQuery] = pmInfo; - if (Result cachedDataResult = ReadPkgCountCache(pmId)) { + if (Result cachedDataResult = ReadCache(pmId)) { const auto& [count, timestamp] = *cachedDataResult; std::error_code errc; const std::filesystem::file_time_type dbModTime = fs::last_write_time(dbPath, errc); @@ -233,12 +108,13 @@ namespace { const i64 nowEpochSeconds = duration_cast(system_clock::now().time_since_epoch()).count(); const PkgCountCacheData dataToCache = { .count = count, .timestampEpochSeconds = nowEpochSeconds }; - if (Result writeResult = WritePkgCountCache(pmId, dataToCache); !writeResult) + if (Result writeResult = WriteCache(pmId, dataToCache); !writeResult) error_at(writeResult.error()); return count; } +#ifndef _WIN32 fn GetNixPackageCount() -> Result { debug_log("Attempting to get Nix package count."); @@ -259,6 +135,7 @@ namespace { return GetPackageCountInternalDb(nixInfo); } +#endif fn GetCargoPackageCount() -> Result { using util::helpers::GetEnv; @@ -288,12 +165,15 @@ namespace { namespace os::shared { fn GetPackageCount() -> Result { u64 count = 0; - if (Result pkgCount = GetNixPackageCount()) + +#ifndef _WIN32 + if (const Result pkgCount = GetNixPackageCount()) count += *pkgCount; else debug_at(pkgCount.error()); +#endif - if (Result pkgCount = GetCargoPackageCount()) + if (const Result pkgCount = GetCargoPackageCount()) count += *pkgCount; else debug_at(pkgCount.error()); diff --git a/src/os/windows.cpp b/src/os/windows.cpp index e26760c..ca2c202 100644 --- a/src/os/windows.cpp +++ b/src/os/windows.cpp @@ -16,10 +16,10 @@ #include #include -#include "src/core/util/error.hpp" -#include "src/core/util/helpers.hpp" -#include "src/core/util/logging.hpp" -#include "src/core/util/types.hpp" +#include "src/util/error.hpp" +#include "src/util/helpers.hpp" +#include "src/util/logging.hpp" +#include "src/util/types.hpp" #include "os.hpp" // clang-format on diff --git a/src/util/cache.hpp b/src/util/cache.hpp new file mode 100644 index 0000000..ac8454c --- /dev/null +++ b/src/util/cache.hpp @@ -0,0 +1,216 @@ +#pragma once + +#include // std::filesystem +#include // std::{ifstream, ofstream} +#include // glz::read_beve +#include // glz::write_beve +#include // glz::{context, error_code, error_ctx} +#include // std::istreambuf_iterator +#include // std::string +#include // std::error_code +#include // std::decay_t + +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/logging.hpp" +#include "src/util/types.hpp" + +namespace util::cache { + namespace fs = std::filesystem; + using error::DracError, error::DracErrorCode; + using types::Err, types::Exception, types::Result, types::String, types::isize; + + /** + * @brief Gets the full path for a cache file based on a unique key. + * @param cache_key A unique identifier for the cache (e.g., "weather", "pkg_count_pacman"). + * Should ideally only contain filesystem-safe characters. + * @return Result containing the filesystem path on success, or a DracError on failure. + */ + inline fn GetCachePath(const String& cache_key) -> Result { + if (cache_key.empty()) + return Err(DracError(DracErrorCode::InvalidArgument, "Cache key cannot be empty.")); + + // Basic check for potentially problematic characters in the key for filename safety. + // You might want to expand this or implement more robust sanitization if needed. + if (cache_key.find_first_of("/\\:*?\"<>|") != String::npos) + return Err( + DracError(DracErrorCode::InvalidArgument, std::format("Cache key '{}' contains invalid characters.", cache_key)) + ); + + std::error_code errc; + const fs::path cacheDir = fs::temp_directory_path(errc); + + if (errc) + return Err(DracError(DracErrorCode::IoError, "Failed to get system temporary directory: " + errc.message())); + + // Use a consistent naming scheme + return cacheDir / (cache_key + "_cache.beve"); + } + + /** + * @brief Reads and deserializes data from a BEVE cache file. + * @tparam T The type of the object to deserialize from the cache. Must be Glaze-compatible. + * @param cache_key The unique identifier for the cache. + * @return Result containing the deserialized object of type T on success, or a DracError on failure. + */ + template + fn ReadCache(const String& cache_key) -> Result { + Result cachePathResult = GetCachePath(cache_key); + if (!cachePathResult) + return Err(cachePathResult.error()); + + const fs::path& cachePath = *cachePathResult; + + if (std::error_code exists_ec; !fs::exists(cachePath, exists_ec) || exists_ec) { + if (exists_ec) { + // Log if there was an error checking existence, but still return NotFound + warn_log("Error checking existence of cache file '{}': {}", cachePath.string(), exists_ec.message()); + } + return Err(DracError(DracErrorCode::NotFound, "Cache file not found: " + cachePath.string())); + } + + std::ifstream ifs(cachePath, std::ios::binary); + if (!ifs.is_open()) + return Err(DracError(DracErrorCode::IoError, "Failed to open cache file for reading: " + cachePath.string())); + + debug_log("Reading cache for key '{}' from: {}", cache_key, cachePath.string()); + + try { + // Read the entire file content + const String content((std::istreambuf_iterator(ifs)), std::istreambuf_iterator()); + ifs.close(); // Close the file as soon as content is read + + if (content.empty()) + return Err(DracError(DracErrorCode::ParseError, "BEVE cache file is empty: " + cachePath.string())); + + // Ensure T is default constructible for Glaze + static_assert(std::is_default_constructible_v, "Cache type T must be default constructible for Glaze."); + T result {}; + + if (glz::error_ctx glazeErr = glz::read_beve(result, content); glazeErr.ec != glz::error_code::none) { + return Err(DracError( + DracErrorCode::ParseError, + std::format( + "BEVE parse error reading cache '{}' (code {}): {}", + cachePath.string(), + static_cast(glazeErr.ec), + glz::format_error(glazeErr, content) + ) + )); + } + + debug_log("Successfully read cache for key '{}'.", cache_key); + return result; + } catch (const std::ios_base::failure& e) { + return Err(DracError( + DracErrorCode::IoError, std::format("Filesystem error reading cache file {}: {}", cachePath.string(), e.what()) + )); + } catch (const Exception& e) { + // Catching std::exception or a project-specific base exception + return Err(DracError( + DracErrorCode::InternalError, + std::format("Standard exception reading cache file {}: {}", cachePath.string(), e.what()) + )); + } catch (...) { + return Err(DracError(DracErrorCode::Other, "Unknown error reading cache file: " + cachePath.string())); + } + } + + /** + * @brief Serializes and writes data to a BEVE cache file safely. + * @tparam T The type of the object to serialize. Must be Glaze-compatible. + * @param cache_key The unique identifier for the cache. + * @param data The data object of type T to write to the cache. + * @return Result containing void on success, or a DracError on failure. + */ + template + fn WriteCache(const String& cache_key, const T& data) -> Result { + Result cachePathResult = GetCachePath(cache_key); + if (!cachePathResult) + return Err(cachePathResult.error()); + + const fs::path& cachePath = *cachePathResult; + fs::path tempPath = cachePath; + tempPath += ".tmp"; // Use a temporary file for atomic write + + debug_log("Writing cache for key '{}' to: {}", cache_key, cachePath.string()); + + try { + String binaryBuffer; + + // Use Glaze to serialize + // Need to decay T in case it's a reference type from the caller + using DecayedT = std::decay_t; + DecayedT dataToSerialize = data; // Make a copy if needed for non-const Glaze operations + + if (glz::error_ctx glazeErr = glz::write_beve(dataToSerialize, binaryBuffer); glazeErr) { + return Err(DracError( + DracErrorCode::ParseError, + std::format( + "BEVE serialization error writing cache for key '{}' (code {}): {}", + cache_key, + static_cast(glazeErr.ec), + glz::format_error(glazeErr, binaryBuffer) + ) + )); + } + + // Scope for ofstream to ensure it's closed before rename + { + std::ofstream ofs(tempPath, std::ios::binary | std::ios::trunc); + if (!ofs.is_open()) + return Err(DracError(DracErrorCode::IoError, "Failed to open temporary cache file: " + tempPath.string())); + + ofs.write(binaryBuffer.data(), static_cast(binaryBuffer.size())); + + if (!ofs) { + // Attempt cleanup before returning error + std::error_code removeEc; + fs::remove(tempPath, removeEc); // Ignore error on cleanup attempt + return Err(DracError(DracErrorCode::IoError, "Failed to write to temporary cache file: " + tempPath.string()) + ); + } + // ofstream automatically closed here + } + + // Attempt to atomically replace the old cache file + std::error_code rename_ec; + fs::rename(tempPath, cachePath, rename_ec); + if (rename_ec) { + // If rename failed, attempt to clean up the temp file + std::error_code removeEc; + fs::remove(tempPath, removeEc); // Ignore error on cleanup attempt + return Err(DracError( + DracErrorCode::IoError, + std::format( + "Failed to replace cache file '{}' with temporary file '{}': {}", + cachePath.string(), + tempPath.string(), + rename_ec.message() + ) + )); + } + + debug_log("Successfully wrote cache for key '{}'.", cache_key); + return {}; // Success + } catch (const std::ios_base::failure& e) { + std::error_code removeEc; + fs::remove(tempPath, removeEc); // Cleanup attempt + return Err(DracError( + DracErrorCode::IoError, std::format("Filesystem error writing cache file {}: {}", tempPath.string(), e.what()) + )); + } catch (const Exception& e) { + std::error_code removeEc; + fs::remove(tempPath, removeEc); // Cleanup attempt + return Err(DracError( + DracErrorCode::InternalError, + std::format("Standard exception writing cache file {}: {}", tempPath.string(), e.what()) + )); + } catch (...) { + std::error_code removeEc; + fs::remove(tempPath, removeEc); // Cleanup attempt + return Err(DracError(DracErrorCode::Other, "Unknown error writing cache file: " + tempPath.string())); + } + } + +} // namespace util::cache diff --git a/src/core/util/defs.hpp b/src/util/defs.hpp similarity index 100% rename from src/core/util/defs.hpp rename to src/util/defs.hpp diff --git a/src/core/util/error.hpp b/src/util/error.hpp similarity index 99% rename from src/core/util/error.hpp rename to src/util/error.hpp index 8625b3e..ed82589 100644 --- a/src/core/util/error.hpp +++ b/src/util/error.hpp @@ -9,7 +9,7 @@ #include // winrt::hresult_error #endif -#include "src/core/util/types.hpp" +#include "src/util/types.hpp" namespace util::error { using types::u8, types::i32, types::String, types::StringView, types::Exception; diff --git a/src/core/util/helpers.hpp b/src/util/helpers.hpp similarity index 93% rename from src/core/util/helpers.hpp rename to src/util/helpers.hpp index 3e8b1fc..d7c939a 100644 --- a/src/core/util/helpers.hpp +++ b/src/util/helpers.hpp @@ -1,8 +1,8 @@ #pragma once -#include "defs.hpp" -#include "error.hpp" -#include "types.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/types.hpp" namespace util::helpers { using error::DracError, error::DracErrorCode; diff --git a/src/core/util/logging.hpp b/src/util/logging.hpp similarity index 91% rename from src/core/util/logging.hpp rename to src/util/logging.hpp index 442831b..2d9e84a 100644 --- a/src/core/util/logging.hpp +++ b/src/util/logging.hpp @@ -1,9 +1,8 @@ #pragma once -#include // std::chrono::{days, floor, seconds, system_clock} -#include // For time_t, tm, localtime_s, localtime_r, strftime (needed for cross-platform local time) -#include // std::filesystem::path -#include // std::format +#include // std::chrono::{days, floor, seconds, system_clock} +#include // std::filesystem::path +#include // std::format #include // ftxui::Color #include // std::print #include // std::forward @@ -12,9 +11,9 @@ #include // std::source_location #endif -#include "src/core/util/defs.hpp" -#include "src/core/util/error.hpp" -#include "src/core/util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/types.hpp" namespace util::logging { using types::usize, types::u8, types::i32, types::i64, types::CStr, types::String, types::StringView, types::Array, @@ -74,10 +73,8 @@ namespace util::logging { * @param color The FTXUI color * @return Styled string with ANSI codes */ - inline fn Colorize(StringView text, const ftxui::Color::Palette16& color) -> String { - std::ostringstream oss; - oss << LogLevelConst::COLOR_CODE_LITERALS.at(static_cast(color)) << text << LogLevelConst::RESET_CODE; - return oss.str(); + inline fn Colorize(const StringView text, const ftxui::Color::Palette16& color) -> String { + return std::format("{}{}{}", LogLevelConst::COLOR_CODE_LITERALS.at(color), text, LogLevelConst::RESET_CODE); } /** @@ -86,7 +83,7 @@ namespace util::logging { * @return Bold text */ inline fn Bold(const StringView text) -> String { - return String(LogLevelConst::BOLD_START) + String(text) + String(LogLevelConst::BOLD_END); + return std::format("{}{}{}", LogLevelConst::BOLD_START, text, LogLevelConst::BOLD_END); } /** @@ -95,7 +92,7 @@ namespace util::logging { * @return Italic text */ inline fn Italic(const StringView text) -> String { - return String(LogLevelConst::ITALIC_START) + String(text) + String(LogLevelConst::ITALIC_END); + return std::format("{}{}{}", LogLevelConst::ITALIC_START, text, LogLevelConst::ITALIC_END); } /** @@ -143,11 +140,14 @@ namespace util::logging { } } + // ReSharper disable once CppDoxygenUnresolvedReference /** * @brief Logs a message with the specified log level, source location, and format string. * @tparam Args Parameter pack for format arguments. * @param level The log level (DEBUG, INFO, WARN, ERROR). + * \ifnot NDEBUG * @param loc The source location of the log message (only in Debug builds). + * \endif * @param fmt The format string. * @param args The arguments for the format string. */ @@ -163,8 +163,6 @@ namespace util::logging { using namespace std::chrono; using std::filesystem::path; - using Buffer = Array; - const auto nowTp = system_clock::now(); const std::time_t nowTt = system_clock::to_time_t(nowTp); std::tm localTm; @@ -176,7 +174,7 @@ namespace util::logging { #else if (localtime_r(&nowTt, &localTm) != nullptr) { #endif - Array timeBuffer; + Array timeBuffer {}; if (std::strftime(timeBuffer.data(), sizeof(timeBuffer), LogLevelConst::TIMESTAMP_FORMAT, &localTm) > 0) timestamp = timeBuffer.data(); @@ -187,7 +185,7 @@ namespace util::logging { const String message = std::format(fmt, std::forward(args)...); - Buffer buffer {}; + Array buffer {}; // Use the locally formatted timestamp string here auto* iter = std::format_to( diff --git a/src/core/util/types.hpp b/src/util/types.hpp similarity index 100% rename from src/core/util/types.hpp rename to src/util/types.hpp diff --git a/src/wrappers/dbus.hpp b/src/wrappers/dbus.hpp index 5be8e68..f98e5a1 100644 --- a/src/wrappers/dbus.hpp +++ b/src/wrappers/dbus.hpp @@ -9,9 +9,9 @@ #include // std::format #include // std::is_convertible_v -#include "src/core/util/defs.hpp" -#include "src/core/util/error.hpp" -#include "src/core/util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/error.hpp" +#include "src/util/types.hpp" // clang-format on namespace dbus { diff --git a/src/wrappers/wayland.hpp b/src/wrappers/wayland.hpp index 4b9cf75..05e4fb7 100644 --- a/src/wrappers/wayland.hpp +++ b/src/wrappers/wayland.hpp @@ -5,8 +5,8 @@ // clang-format off #include // Wayland client library -#include "src/core/util/defs.hpp" -#include "src/core/util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/types.hpp" // clang-format on struct wl_display; diff --git a/src/wrappers/xcb.hpp b/src/wrappers/xcb.hpp index 8eedffd..bdc171f 100644 --- a/src/wrappers/xcb.hpp +++ b/src/wrappers/xcb.hpp @@ -5,8 +5,8 @@ // clang-format off #include // XCB library -#include "src/core/util/defs.hpp" -#include "src/core/util/types.hpp" +#include "src/util/defs.hpp" +#include "src/util/types.hpp" // clang-format on namespace xcb {