diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index a0250d0..b7c8822 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -11,7 +11,7 @@ permissions: contents: read env: GITHUB_ACTIONS: true - VCPKG_ROOT: ${{github.workspace}}/vcpkg + VCPKG_ROOT: ${{ github.workspace }}/vcpkg jobs: build: name: "Build on Linux" diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 28d956e..777c4cd 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -11,7 +11,7 @@ permissions: contents: read env: GITHUB_ACTIONS: true - VCPKG_ROOT: ${{github.workspace}}/vcpkg + VCPKG_ROOT: ${{ github.workspace }}/vcpkg jobs: build: name: "Build on macOS" diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 071e962..48cc32c 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -11,7 +11,7 @@ permissions: contents: read env: GITHUB_ACTIONS: true - VCPKG_ROOT: ${{github.workspace}}/vcpkg + VCPKG_ROOT: ${{ github.workspace }}/vcpkg jobs: build: name: "Build on Windows" @@ -19,7 +19,7 @@ jobs: matrix: variant: - arch: x64 - runner: windows-latest + runner: windows-2025 triplet: x64-windows #- arch: arm64 # runner: windows-11-arm diff --git a/CHANGELOG.md b/CHANGELOG.md index a509e16..d24aef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 2025.9.4 +### Breaking Changes +None +### New APIs +#### Database +- Added `reset` method to `SqliteStatement` +### Fixes +#### Database +- Fixed an issue where the sqlite database crashed on Windows +#### Update +- Fixed parsing of development versions + ## 2025.9.3 ### Breaking Changes None diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b76f75..9b38ef7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,7 @@ endif() set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake") #libnick Definition -project ("libnick" LANGUAGES C CXX VERSION 2025.9.3 DESCRIPTION "A cross-platform base for native Nickvision applications.") +project ("libnick" LANGUAGES C CXX VERSION 2025.9.4 DESCRIPTION "A cross-platform base for native Nickvision applications.") include(CMakePackageConfigHelpers) include(GNUInstallDirs) include(CTest) diff --git a/Doxyfile b/Doxyfile index 19089a2..d134d6d 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = "libnick" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = "2025.9.3" +PROJECT_NUMBER = "2025.9.4" # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/include/database/sqlitestatement.h b/include/database/sqlitestatement.h index 8cb1234..78b2ed9 100644 --- a/include/database/sqlitestatement.h +++ b/include/database/sqlitestatement.h @@ -56,6 +56,10 @@ namespace Nickvision::Database * @returns SqliteStepResult */ SqliteStepResult step() noexcept; + /** + * @brief Resets the statement to its initial state, clearing any bindings as well. + */ + void reset() noexcept; /** * @brief Binds a value to a sqlite parameter. * @tparam T The type of the value to bind diff --git a/include/update/updater.h b/include/update/updater.h index 67a1cff..f6561d7 100644 --- a/include/update/updater.h +++ b/include/update/updater.h @@ -56,6 +56,7 @@ namespace Nickvision::Update /** * @brief Gets the latest version of the provided type from the GitHub repo. * @brief This method looks for tags in the format major.minor.build-dev or major.minor.build.dev for preview versions and major.minor.build for stable versions. + * @brief If the VersionType is Preview, the updater may return a stable version if it is greater than the latest preview. * @param versionType The type of the version to get * @return The current version of the provided type if available, else empty Version */ diff --git a/manual/README.md b/manual/README.md index a3eab18..42e8307 100644 --- a/manual/README.md +++ b/manual/README.md @@ -6,14 +6,17 @@ libnick provides Nickvision apps with a common set of cross-platform APIs for managing system and desktop app functionality such as network management, taskbar icons, translations, app updates, and more. -## 2025.9.3 +## 2025.9.4 ### Breaking Changes None ### New APIs -None +#### Database +- Added `reset` method to `SqliteStatement` ### Fixes #### Database -- Fixed an issue where the sqlite database did not encrypt and decrypt correctly +- Fixed an issue where the sqlite database crashed on Windows +#### Update +- Fixed parsing of development versions ## Dependencies The following are a list of dependencies used by libnick. diff --git a/src/database/sqlitedatabase.cpp b/src/database/sqlitedatabase.cpp index ab898ee..3e262e1 100644 --- a/src/database/sqlitedatabase.cpp +++ b/src/database/sqlitedatabase.cpp @@ -10,7 +10,7 @@ namespace Nickvision::Database m_isUnlocked{ true }, m_database{ nullptr } { - if (sqlite3_open_v2(m_path.string().c_str(), &m_database, m_flags, nullptr) != SQLITE_OK) + if(sqlite3_open_v2(m_path.string().c_str(), &m_database, m_flags, nullptr) != SQLITE_OK) { throw std::runtime_error("Unable to open sql database."); } @@ -102,33 +102,38 @@ namespace Nickvision::Database return true; } //If empty database, can use sqlite3_key - SqliteStatement statement{ m_database, "SELECT count(*) FROM sqlite_master;" }; - int tableCount{ -1 }; - if(statement.step() == SqliteStepResult::Row) { - tableCount = statement.getColumn(0); - } - if(tableCount == 0) - { - if(sqlite3_key(m_database, password.c_str(), static_cast(password.size())) != SQLITE_OK) + SqliteStatement statement{ m_database, "SELECT count(*) FROM sqlite_master;" }; + int tableCount{ -1 }; + if(statement.step() == SqliteStepResult::Row) { - return false; + tableCount = statement.getColumn(0); } - else + if(tableCount == 0) { - m_isUnlocked = sqlite3_exec(m_database, "SELECT count(*) FROM sqlite_master;", nullptr, nullptr, nullptr) == SQLITE_OK; - m_isEncrypted = true; - return true; + if(sqlite3_key(m_database, password.c_str(), static_cast(password.size())) != SQLITE_OK) + { + return false; + } + else + { + m_isUnlocked = sqlite3_exec(m_database, "SELECT count(*) FROM sqlite_master;", nullptr, nullptr, nullptr) == SQLITE_OK; + m_isEncrypted = true; + return true; + } } } //Create temp encrypted database std::filesystem::path tempPath{ (m_path.string() + ".encrypt") }; - std::string cmd{ "ATTACH DATABASE '" + tempPath.string() + "' AS encrypted KEY '" + password + "'" }; + std::string cmd{ "ATTACH DATABASE '" + tempPath.string() + "' AS encrypted KEY '" + password + "';" }; sqlite3_exec(m_database, cmd.c_str(), nullptr, nullptr, nullptr); - sqlite3_exec(m_database, "SELECT sqlcipher_export('encrypted')", nullptr, nullptr, nullptr); - sqlite3_exec(m_database, "DETACH DATABASE encrypted", nullptr, nullptr, nullptr); + sqlite3_exec(m_database, "SELECT sqlcipher_export('encrypted');", nullptr, nullptr, nullptr); + sqlite3_exec(m_database, "DETACH DATABASE encrypted;", nullptr, nullptr, nullptr); //Remove old encrypted database - sqlite3_close(m_database); + if(sqlite3_close(m_database) != SQLITE_OK) + { + throw std::runtime_error("Unable to close old sql database."); + } std::filesystem::remove(m_path); std::filesystem::rename(tempPath, m_path); //Open new encrypted database @@ -154,12 +159,15 @@ namespace Nickvision::Database { //Create temporary decrypted database std::filesystem::path tempPath{ (m_path.string() + ".decrypt") }; - std::string cmd{ "ATTACH DATABASE '" + tempPath.string() + "' AS plaintext KEY ''" }; + std::string cmd{ "ATTACH DATABASE '" + tempPath.string() + "' AS plaintext KEY '';" }; sqlite3_exec(m_database, cmd.c_str(), nullptr, nullptr, nullptr); - sqlite3_exec(m_database, "SELECT sqlcipher_export('plaintext')", nullptr, nullptr, nullptr); - sqlite3_exec(m_database, "DETACH DATABASE plaintext", nullptr, nullptr, nullptr); + sqlite3_exec(m_database, "SELECT sqlcipher_export('plaintext');", nullptr, nullptr, nullptr); + sqlite3_exec(m_database, "DETACH DATABASE plaintext;", nullptr, nullptr, nullptr); //Remove old encrypted database - sqlite3_close(m_database); + if(sqlite3_close(m_database) != SQLITE_OK) + { + throw std::runtime_error("Unable to close old sql database."); + } std::filesystem::remove(m_path); std::filesystem::rename(tempPath, m_path); //Open new decrypted database diff --git a/src/database/sqlitestatement.cpp b/src/database/sqlitestatement.cpp index 28ef474..3663a94 100644 --- a/src/database/sqlitestatement.cpp +++ b/src/database/sqlitestatement.cpp @@ -44,6 +44,15 @@ namespace Nickvision::Database return SqliteStepResult::Error; } + void SqliteStatement::reset() noexcept + { + if(m_statement) + { + sqlite3_reset(m_statement); + sqlite3_clear_bindings(m_statement); + } + } + SqliteStatement& SqliteStatement::operator=(SqliteStatement&& other) noexcept { if(this != &other) diff --git a/src/update/updater.cpp b/src/update/updater.cpp index dd659ae..cd083ae 100644 --- a/src/update/updater.cpp +++ b/src/update/updater.cpp @@ -63,8 +63,8 @@ namespace Nickvision::Update { return {}; } - std::string stableVersion; - std::string previewVersion; + Version stable; + Version preview; for (const boost::json::value& release : root.as_array()) { if(!release.is_object()) @@ -72,38 +72,42 @@ namespace Nickvision::Update continue; } boost::json::object releaseObject = release.as_object(); - const boost::json::value& tagNameValue{ releaseObject["tag_name"] }; - if (!tagNameValue.is_string()) + const boost::json::value& id{ releaseObject["id"] }; + const boost::json::value& tagName{ releaseObject["tag_name"] }; + const boost::json::value& prerelease{ releaseObject["prerelease"] }; + if (!id.is_int64() || !tagName.is_string() || !prerelease.is_bool()) { - return {}; + continue; } - std::string version{ tagNameValue.as_string() }; - size_t splitCount{ StringHelpers::split(version, ".", false).size() }; - const boost::json::value& id{ releaseObject["id"] }; - if (stableVersion.empty() && versionType == VersionType::Stable && version.find('-') == std::string::npos && splitCount == 3) + if (prerelease.as_bool() && preview.empty()) { - m_latestStableReleaseId = id.is_int64() ? static_cast(id.as_int64()) : -1; - stableVersion = version; + preview = Version(tagName.as_string().c_str()); + m_latestPreviewReleaseId = id.as_int64(); } - if (previewVersion.empty() && versionType == VersionType::Preview && (version.find('-') != std::string::npos || splitCount == 4)) + else if(stable.empty()) { - m_latestPreviewReleaseId = id.is_int64() ? static_cast(id.as_int64()) : -1; - previewVersion = version; + stable = Version(tagName.as_string().c_str()); + m_latestStableReleaseId = id.as_int64(); } - if (!stableVersion.empty() && !previewVersion.empty()) + if(!stable.empty() && !preview.empty()) { break; } } + if(stable > preview) + { + preview = stable; + m_latestPreviewReleaseId = m_latestStableReleaseId; + } if(versionType == VersionType::Stable) { - return stableVersion.empty() ? Version() : Version(stableVersion); + return stable; } else if(versionType == VersionType::Preview) { - return previewVersion.empty() ? Version() : Version(previewVersion); + return preview; } - return Version(); + return {}; } bool Updater::downloadUpdate(VersionType versionType, const std::filesystem::path& path, const std::string& assetName, bool exactMatch, const cpr::ProgressCallback& progress) noexcept diff --git a/src/update/version.cpp b/src/update/version.cpp index c7c7718..694f565 100644 --- a/src/update/version.cpp +++ b/src/update/version.cpp @@ -1,6 +1,8 @@ #include "update/version.h" #include -#include +#include "helpers/stringhelpers.h" + +using namespace Nickvision::Helpers; namespace Nickvision::Update { @@ -40,42 +42,45 @@ namespace Nickvision::Update m_minor{ 0 }, m_build{ 0 } { - std::string s{ version }; - size_t pos{ 0 }; - int i{ 0 }; - while ((pos = s.find('.')) != std::string::npos) + + std::vector splits{ StringHelpers::split(version, ".", false) }; + if(splits.size() < 3) { - std::string token{ s.substr(0, pos) }; - if (i == 0) - { - m_major = std::stoi(token); - } - else if (i == 1) + throw std::invalid_argument("Ill-formated version string."); + } + m_major = std::stoi(splits[0]); + m_minor = std::stoi(splits[1]); + if(splits.size() == 3) + { + if(splits[2].find("-") == std::string::npos) { - m_minor = std::stoi(token); + m_build = std::stoi(splits[2]); } else { - throw std::invalid_argument("Ill-formated version string."); + std::vector devSplits{ StringHelpers::split(splits[2], "-", false) }; + if(devSplits.size() != 2) + { + throw std::invalid_argument("Ill-formated version string."); + } + m_build = std::stoi(devSplits[0]); + m_dev = devSplits[1]; } - s.erase(0, pos + 1); - i++; } - if (i != 2) + else if(splits.size() == 4) { - throw std::invalid_argument("Ill-formated version string."); + m_build = std::stoi(splits[2]); + m_dev = splits[3]; } - size_t dashIndex{ s.find('-') }; - if(dashIndex == std::string::npos) + else { - dashIndex = s.find('.'); + throw std::invalid_argument("Ill-formated version string."); } - s.erase(0, dashIndex); - if (!s.empty() && (s[0] == '-' || s[0] == '.')) //dev version + m_str = std::to_string(m_major) + "." + std::to_string(m_minor) + "." + std::to_string(m_build); + if(!m_dev.empty()) { - m_dev = s; + m_str += "-" + m_dev; } - m_str = std::to_string(m_major) + "." + std::to_string(m_minor) + "." + std::to_string(m_build) + m_dev; } int Version::getMajor() const noexcept @@ -153,6 +158,10 @@ namespace Nickvision::Update { return true; } + else + { + return m_dev < compare.m_dev; + } } } } @@ -204,6 +213,10 @@ namespace Nickvision::Update { return false; } + else + { + return m_dev > compare.m_dev; + } } } } @@ -224,4 +237,4 @@ namespace Nickvision::Update { return !(operator==(compare)); } -} \ No newline at end of file +} diff --git a/tests/versiontests.cpp b/tests/versiontests.cpp index fe4cf02..bedeaf1 100644 --- a/tests/versiontests.cpp +++ b/tests/versiontests.cpp @@ -9,10 +9,46 @@ TEST(VersionTests, EmptyVersion) ASSERT_EQ(v.empty(), true); } +TEST(VersionTests, StableVersion1) +{ + Version v{ "2025.9.0" }; + ASSERT_EQ(v.getVersionType(), VersionType::Stable); + ASSERT_EQ(v.str(), "2025.9.0"); +} + +TEST(VersionTests, StableVersion2) +{ + Version v{ "0.2.0" }; + ASSERT_EQ(v.getVersionType(), VersionType::Stable); + ASSERT_EQ(v.str(), "0.2.0"); +} + +TEST(VersionTests, StableVersion3) +{ + Version v{ "1.2.4" }; + ASSERT_EQ(v.getVersionType(), VersionType::Stable); + ASSERT_EQ(v.str(), "1.2.4"); +} + TEST(VersionTests, BetaVersion1) { Version v{ "2010.5.0-beta1" }; ASSERT_EQ(v.getVersionType(), VersionType::Preview); + ASSERT_EQ(v.str(), "2010.5.0-beta1"); +} + +TEST(VersionTests, BetaVersion2) +{ + Version v{ "2025.9.1-next" }; + ASSERT_EQ(v.getVersionType(), VersionType::Preview); + ASSERT_EQ(v.str(), "2025.9.1-next"); +} + +TEST(VersionTests, BetaVersion3) +{ + Version v{ "2025.9.1.beta1" }; + ASSERT_EQ(v.getVersionType(), VersionType::Preview); + ASSERT_EQ(v.str(), "2025.9.1-beta1"); } TEST(VersionTests, BadVersion1) @@ -59,9 +95,18 @@ TEST(VersionTests, CompareVersion3) TEST(VersionTests, CompareVersion4) { - Version v1{ "2024.6.0" }; - Version v2{ "2024.6.0" }; - ASSERT_FALSE(v1 > v2); - ASSERT_FALSE(v2 < v1); - ASSERT_TRUE(v1 == v2); -} \ No newline at end of file + Version v1{ "2025.9.0-beta1" }; + Version v2{ "2025.8.0" }; + ASSERT_TRUE(v1 > v2); + ASSERT_TRUE(v2 < v1); + ASSERT_FALSE(v1 == v2); +} + +TEST(VersionTests, CompareVersion5) +{ + Version v1{ "2025.9.0-beta2" }; + Version v2{ "2025.9.0-beta1" }; + ASSERT_TRUE(v1 > v2); + ASSERT_TRUE(v2 < v1); + ASSERT_FALSE(v1 == v2); +}