diff --git a/CHANGELOG.md b/CHANGELOG.md index 044754c4c..a000a900f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - Bugfix: Fixed scrollbar highlight colors when changing message history limit. (#4288) - Bugfix: Fixed the split "Search" menu action not opening the correct search window. (#4305) - Bugfix: Fixed an issue on Windows when opening links in incognito mode that contained forward slashes in hash (#4307) +- Bugfix: Fixed an issue where beta versions wouldn't update to stable versions correctly. (#4329) - Dev: Remove protocol from QApplication's Organization Domain (so changed from `https://www.chatterino.com` to `chatterino.com`). (#4256) - Dev: Ignore `WM_SHOWWINDOW` hide events, causing fewer attempted rescales. (#4198) - Dev: Migrated to C++ 20 (#4252, #4257) diff --git a/lib/semver/README.md b/lib/semver/README.md new file mode 100644 index 000000000..792300807 --- /dev/null +++ b/lib/semver/README.md @@ -0,0 +1,3 @@ +From https://github.com/Neargye/semver + +Downloaded 2023-01-25 from commit hash [eae828abf579836ba2ecc72a8604ad1b6fb10d86](https://github.com/Neargye/semver/commit/eae828abf579836ba2ecc72a8604ad1b6fb10d86) diff --git a/lib/semver/include/semver/semver.hpp b/lib/semver/include/semver/semver.hpp new file mode 100644 index 000000000..b132c52e4 --- /dev/null +++ b/lib/semver/include/semver/semver.hpp @@ -0,0 +1,858 @@ +// _____ _ _ +// / ____| | | (_) +// | (___ ___ _ __ ___ __ _ _ __ | |_ _ ___ +// \___ \ / _ \ '_ ` _ \ / _` | '_ \| __| |/ __| +// ____) | __/ | | | | | (_| | | | | |_| | (__ +// |_____/ \___|_| |_| |_|\__,_|_| |_|\__|_|\___| +// __ __ _ _ _____ +// \ \ / / (_) (_) / ____|_ _ +// \ \ / /__ _ __ ___ _ ___ _ __ _ _ __ __ _ | | _| |_ _| |_ +// \ \/ / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | | | |_ _|_ _| +// \ / __/ | \__ \ | (_) | | | | | | | | (_| | | |____|_| |_| +// \/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | \_____| +// https://github.com/Neargye/semver __/ | +// version 0.3.0 |___/ +// +// Licensed under the MIT License . +// SPDX-License-Identifier: MIT +// Copyright (c) 2018 - 2021 Daniil Goncharov . +// Copyright (c) 2020 - 2021 Alexander Gorbunov . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef NEARGYE_SEMANTIC_VERSIONING_HPP +#define NEARGYE_SEMANTIC_VERSIONING_HPP + +#define SEMVER_VERSION_MAJOR 0 +#define SEMVER_VERSION_MINOR 3 +#define SEMVER_VERSION_PATCH 0 + +#include +#include +#include +#include +#include +#include +#include +#if __has_include() +#include +#else +#include +#endif + +#if defined(SEMVER_CONFIG_FILE) +#include SEMVER_CONFIG_FILE +#endif + +#if defined(SEMVER_THROW) +// define SEMVER_THROW(msg) to override semver throw behavior. +#elif defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND) +# include +# define SEMVER_THROW(msg) (throw std::invalid_argument{msg}) +#else +# include +# include +# define SEMVER_THROW(msg) (assert(!msg), std::abort()) +#endif + +#if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wmissing-braces" // Ignore warning: suggest braces around initialization of subobject 'return {first, std::errc::invalid_argument};'. +#endif + +namespace semver { + +enum struct prerelease : std::uint8_t { + alpha = 0, + beta = 1, + rc = 2, + none = 3 +}; + +#if __has_include() +struct from_chars_result : std::from_chars_result { + [[nodiscard]] constexpr operator bool() const noexcept { return ec == std::errc{}; } +}; + +struct to_chars_result : std::to_chars_result { + [[nodiscard]] constexpr operator bool() const noexcept { return ec == std::errc{}; } +}; +#else +struct from_chars_result { + const char* ptr; + std::errc ec; + + [[nodiscard]] constexpr operator bool() const noexcept { return ec == std::errc{}; } +}; + +struct to_chars_result { + char* ptr; + std::errc ec; + + [[nodiscard]] constexpr operator bool() const noexcept { return ec == std::errc{}; } +}; +#endif + +// Max version string length = 3() + 1(.) + 3() + 1(.) + 3() + 1(-) + 5() + 1(.) + 3() = 21. +inline constexpr auto max_version_string_length = std::size_t{21}; + +namespace detail { + +inline constexpr auto alpha = std::string_view{"alpha", 5}; +inline constexpr auto beta = std::string_view{"beta", 4}; +inline constexpr auto rc = std::string_view{"rc", 2}; + +// Min version string length = 1() + 1(.) + 1() + 1(.) + 1() = 5. +inline constexpr auto min_version_string_length = 5; + +constexpr char to_lower(char c) noexcept { + return (c >= 'A' && c <= 'Z') ? static_cast(c + ('a' - 'A')) : c; +} + +constexpr bool is_digit(char c) noexcept { + return c >= '0' && c <= '9'; +} + +constexpr bool is_space(char c) noexcept { + return c == ' '; +} + +constexpr bool is_operator(char c) noexcept { + return c == '<' || c == '>' || c == '='; +} + +constexpr bool is_dot(char c) noexcept { + return c == '.'; +} + +constexpr bool is_logical_or(char c) noexcept { + return c == '|'; +} + +constexpr bool is_hyphen(char c) noexcept { + return c == '-'; +} + +constexpr bool is_letter(char c) noexcept { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); +} + +constexpr std::uint8_t to_digit(char c) noexcept { + return static_cast(c - '0'); +} + +constexpr std::uint8_t length(std::uint8_t x) noexcept { + return x < 10 ? 1 : (x < 100 ? 2 : 3); +} + +constexpr std::uint8_t length(prerelease t) noexcept { + if (t == prerelease::alpha) { + return static_cast(alpha.length()); + } else if (t == prerelease::beta) { + return static_cast(beta.length()); + } else if (t == prerelease::rc) { + return static_cast(rc.length()); + } + + return 0; +} + +constexpr bool equals(const char* first, const char* last, std::string_view str) noexcept { + for (std::size_t i = 0; first != last && i < str.length(); ++i, ++first) { + if (to_lower(*first) != to_lower(str[i])) { + return false; + } + } + + return true; +} + +constexpr char* to_chars(char* str, std::uint8_t x, bool dot = true) noexcept { + do { + *(--str) = static_cast('0' + (x % 10)); + x /= 10; + } while (x != 0); + + if (dot) { + *(--str) = '.'; + } + + return str; +} + +constexpr char* to_chars(char* str, prerelease t) noexcept { + const auto p = t == prerelease::alpha + ? alpha + : t == prerelease::beta + ? beta + : t == prerelease::rc + ? rc + : std::string_view{}; + + if (p.size() > 0) { + for (auto it = p.rbegin(); it != p.rend(); ++it) { + *(--str) = *it; + } + *(--str) = '-'; + } + + return str; +} + +constexpr const char* from_chars(const char* first, const char* last, std::uint8_t& d) noexcept { + if (first != last && is_digit(*first)) { + std::int32_t t = 0; + for (; first != last && is_digit(*first); ++first) { + t = t * 10 + to_digit(*first); + } + if (t <= (std::numeric_limits::max)()) { + d = static_cast(t); + return first; + } + } + + return nullptr; +} + +constexpr const char* from_chars(const char* first, const char* last, std::optional& d) noexcept { + if (first != last && is_digit(*first)) { + std::int32_t t = 0; + for (; first != last && is_digit(*first); ++first) { + t = t * 10 + to_digit(*first); + } + if (t <= (std::numeric_limits::max)()) { + d = static_cast(t); + return first; + } + } + + return nullptr; +} + +constexpr const char* from_chars(const char* first, const char* last, prerelease& p) noexcept { + if (is_hyphen(*first)) { + ++first; + } + + if (equals(first, last, alpha)) { + p = prerelease::alpha; + return first + alpha.length(); + } else if (equals(first, last, beta)) { + p = prerelease::beta; + return first + beta.length(); + } else if (equals(first, last, rc)) { + p = prerelease::rc; + return first + rc.length(); + } + + return nullptr; +} + +constexpr bool check_delimiter(const char* first, const char* last, char d) noexcept { + return first != last && first != nullptr && *first == d; +} + +template +struct resize_uninitialized { + static auto resize(T& str, std::size_t size) -> std::void_t { + str.resize(size); + } +}; + +template +struct resize_uninitialized().__resize_default_init(42))>> { + static void resize(T& str, std::size_t size) { + str.__resize_default_init(size); + } +}; + +} // namespace semver::detail + +struct version { + std::uint8_t major = 0; + std::uint8_t minor = 1; + std::uint8_t patch = 0; + prerelease prerelease_type = prerelease::none; + std::optional prerelease_number = std::nullopt; + + constexpr version(std::uint8_t mj, + std::uint8_t mn, + std::uint8_t pt, + prerelease prt = prerelease::none, + std::optional prn = std::nullopt) noexcept + : major{mj}, + minor{mn}, + patch{pt}, + prerelease_type{prt}, + prerelease_number{prt == prerelease::none ? std::nullopt : prn} { + } + + + explicit constexpr version(std::string_view str) : version(0, 0, 0, prerelease::none, 0) { + from_string(str); + } + + constexpr version() = default; // https://semver.org/#how-should-i-deal-with-revisions-in-the-0yz-initial-development-phase + + constexpr version(const version&) = default; + + constexpr version(version&&) = default; + + ~version() = default; + + version& operator=(const version&) = default; + + version& operator=(version&&) = default; + + [[nodiscard]] constexpr from_chars_result from_chars(const char* first, const char* last) noexcept { + if (first == nullptr || last == nullptr || (last - first) < detail::min_version_string_length) { + return {first, std::errc::invalid_argument}; + } + + auto next = first; + if (next = detail::from_chars(next, last, major); detail::check_delimiter(next, last, '.')) { + if (next = detail::from_chars(++next, last, minor); detail::check_delimiter(next, last, '.')) { + if (next = detail::from_chars(++next, last, patch); next == last) { + prerelease_type = prerelease::none; + prerelease_number = {}; + return {next, std::errc{}}; + } else if (detail::check_delimiter(next, last, '-')) { + if (next = detail::from_chars(next, last, prerelease_type); next == last) { + prerelease_number = {}; + return {next, std::errc{}}; + } else if (detail::check_delimiter(next, last, '.')) { + if (next = detail::from_chars(++next, last, prerelease_number); next == last) { + return {next, std::errc{}}; + } + } + } + } + } + + return {first, std::errc::invalid_argument}; + } + + [[nodiscard]] constexpr to_chars_result to_chars(char* first, char* last) const noexcept { + const auto length = string_length(); + if (first == nullptr || last == nullptr || (last - first) < length) { + return {last, std::errc::value_too_large}; + } + + auto next = first + length; + if (prerelease_type != prerelease::none) { + if (prerelease_number.has_value()) { + next = detail::to_chars(next, prerelease_number.value()); + } + next = detail::to_chars(next, prerelease_type); + } + next = detail::to_chars(next, patch); + next = detail::to_chars(next, minor); + next = detail::to_chars(next, major, false); + + return {first + length, std::errc{}}; + } + + [[nodiscard]] constexpr bool from_string_noexcept(std::string_view str) noexcept { + return from_chars(str.data(), str.data() + str.length()); + } + + constexpr version& from_string(std::string_view str) { + if (!from_string_noexcept(str)) { + SEMVER_THROW("semver::version::from_string invalid version."); + } + + return *this; + } + + [[nodiscard]] std::string to_string() const { + auto str = std::string{}; + detail::resize_uninitialized::resize(str, string_length()); + if (!to_chars(str.data(), str.data() + str.length())) { + SEMVER_THROW("semver::version::to_string invalid version."); + } + + return str; + } + + [[nodiscard]] constexpr std::uint8_t string_length() const noexcept { + // () + 1(.) + () + 1(.) + () + auto length = detail::length(major) + detail::length(minor) + detail::length(patch) + 2; + if (prerelease_type != prerelease::none) { + // + 1(-) + () + length += detail::length(prerelease_type) + 1; + if (prerelease_number.has_value()) { + // + 1(.) + () + length += detail::length(prerelease_number.value()) + 1; + } + } + + return static_cast(length); + } + + [[nodiscard]] constexpr int compare(const version& other) const noexcept { + if (major != other.major) { + return major - other.major; + } + + if (minor != other.minor) { + return minor - other.minor; + } + + if (patch != other.patch) { + return patch - other.patch; + } + + if (prerelease_type != other.prerelease_type) { + return static_cast(prerelease_type) - static_cast(other.prerelease_type); + } + + if (prerelease_number.has_value()) { + if (other.prerelease_number.has_value()) { + return prerelease_number.value() - other.prerelease_number.value(); + } + return 1; + } else if (other.prerelease_number.has_value()) { + return -1; + } + + return 0; + } +}; + +[[nodiscard]] constexpr bool operator==(const version& lhs, const version& rhs) noexcept { + return lhs.compare(rhs) == 0; +} + +[[nodiscard]] constexpr bool operator!=(const version& lhs, const version& rhs) noexcept { + return lhs.compare(rhs) != 0; +} + +[[nodiscard]] constexpr bool operator>(const version& lhs, const version& rhs) noexcept { + return lhs.compare(rhs) > 0; +} + +[[nodiscard]] constexpr bool operator>=(const version& lhs, const version& rhs) noexcept { + return lhs.compare(rhs) >= 0; +} + +[[nodiscard]] constexpr bool operator<(const version& lhs, const version& rhs) noexcept { + return lhs.compare(rhs) < 0; +} + +[[nodiscard]] constexpr bool operator<=(const version& lhs, const version& rhs) noexcept { + return lhs.compare(rhs) <= 0; +} + +[[nodiscard]] constexpr version operator""_version(const char* str, std::size_t length) { + return version{std::string_view{str, length}}; +} + +[[nodiscard]] constexpr bool valid(std::string_view str) noexcept { + return version{}.from_string_noexcept(str); +} + +[[nodiscard]] constexpr from_chars_result from_chars(const char* first, const char* last, version& v) noexcept { + return v.from_chars(first, last); +} + +[[nodiscard]] constexpr to_chars_result to_chars(char* first, char* last, const version& v) noexcept { + return v.to_chars(first, last); +} + +[[nodiscard]] constexpr std::optional from_string_noexcept(std::string_view str) noexcept { + if (version v{}; v.from_string_noexcept(str)) { + return v; + } + + return std::nullopt; +} + +[[nodiscard]] constexpr version from_string(std::string_view str) { + return version{str}; +} + +[[nodiscard]] inline std::string to_string(const version& v) { + return v.to_string(); +} + +template +inline std::basic_ostream& operator<<(std::basic_ostream& os, const version& v) { + for (const auto c : v.to_string()) { + os.put(c); + } + + return os; +} + +inline namespace comparators { + +enum struct comparators_option : std::uint8_t { + exclude_prerelease, + include_prerelease +}; + +[[nodiscard]] constexpr int compare(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + if (option == comparators_option::exclude_prerelease) { + return version{lhs.major, lhs.minor, lhs.patch}.compare(version{rhs.major, rhs.minor, rhs.patch}); + } + return lhs.compare(rhs); +} + +[[nodiscard]] constexpr bool equal_to(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + return compare(lhs, rhs, option) == 0; +} + +[[nodiscard]] constexpr bool not_equal_to(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + return compare(lhs, rhs, option) != 0; +} + +[[nodiscard]] constexpr bool greater(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + return compare(lhs, rhs, option) > 0; +} + +[[nodiscard]] constexpr bool greater_equal(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + return compare(lhs, rhs, option) >= 0; +} + +[[nodiscard]] constexpr bool less(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + return compare(lhs, rhs, option) < 0; +} + +[[nodiscard]] constexpr bool less_equal(const version& lhs, const version& rhs, comparators_option option = comparators_option::include_prerelease) noexcept { + return compare(lhs, rhs, option) <= 0; +} + +} // namespace semver::comparators + +namespace range { + +namespace detail { + +using namespace semver::detail; + +class range { + public: + constexpr explicit range(std::string_view str) noexcept : parser{str} {} + + constexpr bool satisfies(const version& ver, bool include_prerelease) { + const bool has_prerelease = ver.prerelease_type != prerelease::none; + + do { + if (is_logical_or_token()) { + parser.advance_token(range_token_type::logical_or); + } + + bool contains = true; + bool allow_compare = include_prerelease; + + while (is_operator_token() || is_number_token()) { + const auto range = parser.parse_range(); + const bool equal_without_tags = equal_to(range.ver, ver, comparators_option::exclude_prerelease); + + if (has_prerelease && equal_without_tags) { + allow_compare = true; + } + + if (!range.satisfies(ver)) { + contains = false; + break; + } + } + + if (has_prerelease) { + if (allow_compare && contains) { + return true; + } + } else if (contains) { + return true; + } + + } while (is_logical_or_token()); + + return false; + } + +private: + enum struct range_operator : std::uint8_t { + less, + less_or_equal, + greater, + greater_or_equal, + equal + }; + + struct range_comparator { + range_operator op; + version ver; + + constexpr bool satisfies(const version& version) const { + switch (op) { + case range_operator::equal: + return version == ver; + case range_operator::greater: + return version > ver; + case range_operator::greater_or_equal: + return version >= ver; + case range_operator::less: + return version < ver; + case range_operator::less_or_equal: + return version <= ver; + default: + SEMVER_THROW("semver::range unexpected operator."); + } + } + }; + + enum struct range_token_type : std::uint8_t { + none, + number, + range_operator, + dot, + logical_or, + hyphen, + prerelease, + end_of_line + }; + + struct range_token { + range_token_type type = range_token_type::none; + std::uint8_t number = 0; + range_operator op = range_operator::equal; + prerelease prerelease_type = prerelease::none; + }; + + struct range_lexer { + std::string_view text; + std::size_t pos; + + constexpr explicit range_lexer(std::string_view text) noexcept : text{text}, pos{0} {} + + constexpr range_token get_next_token() noexcept { + while (!end_of_line()) { + + if (is_space(text[pos])) { + advance(1); + continue; + } + + if (is_logical_or(text[pos])) { + advance(2); + return {range_token_type::logical_or}; + } + + if (is_operator(text[pos])) { + const auto op = get_operator(); + return {range_token_type::range_operator, 0, op}; + } + + if (is_digit(text[pos])) { + const auto number = get_number(); + return {range_token_type::number, number}; + } + + if (is_dot(text[pos])) { + advance(1); + return {range_token_type::dot}; + } + + if (is_hyphen(text[pos])) { + advance(1); + return {range_token_type::hyphen}; + } + + if (is_letter(text[pos])) { + const auto prerelease = get_prerelease(); + return {range_token_type::prerelease, 0, range_operator::equal, prerelease}; + } + } + + return {range_token_type::end_of_line}; + } + + constexpr bool end_of_line() const noexcept { return pos >= text.length(); } + + constexpr void advance(std::size_t i) noexcept { + pos += i; + } + + constexpr range_operator get_operator() noexcept { + if (text[pos] == '<') { + advance(1); + if (text[pos] == '=') { + advance(1); + return range_operator::less_or_equal; + } + return range_operator::less; + } else if (text[pos] == '>') { + advance(1); + if (text[pos] == '=') { + advance(1); + return range_operator::greater_or_equal; + } + return range_operator::greater; + } else if (text[pos] == '=') { + advance(1); + return range_operator::equal; + } + + return range_operator::equal; + } + + constexpr std::uint8_t get_number() noexcept { + const auto first = text.data() + pos; + const auto last = text.data() + text.length(); + if (std::uint8_t n{}; from_chars(first, last, n) != nullptr) { + advance(length(n)); + return n; + } + + return 0; + } + + constexpr prerelease get_prerelease() noexcept { + const auto first = text.data() + pos; + const auto last = text.data() + text.length(); + if (first > last) { + advance(1); + return prerelease::none; + } + + if (prerelease p{}; from_chars(first, last, p) != nullptr) { + advance(length(p)); + return p; + } + + advance(1); + + return prerelease::none; + } + }; + + struct range_parser { + range_lexer lexer; + range_token current_token; + + constexpr explicit range_parser(std::string_view str) : lexer{str}, current_token{range_token_type::none} { + advance_token(range_token_type::none); + } + + constexpr void advance_token(range_token_type token_type) { + if (current_token.type != token_type) { + SEMVER_THROW("semver::range unexpected token."); + } + current_token = lexer.get_next_token(); + } + + constexpr range_comparator parse_range() { + if (current_token.type == range_token_type::number) { + const auto version = parse_version(); + return {range_operator::equal, version}; + } else if (current_token.type == range_token_type::range_operator) { + const auto range_operator = current_token.op; + advance_token(range_token_type::range_operator); + const auto version = parse_version(); + return {range_operator, version}; + } + + return {range_operator::equal, version{}}; + } + + constexpr version parse_version() { + const auto major = parse_number(); + + advance_token(range_token_type::dot); + const auto minor = parse_number(); + + advance_token(range_token_type::dot); + const auto patch = parse_number(); + + prerelease prerelease = prerelease::none; + std::optional prerelease_number = std::nullopt; + + if (current_token.type == range_token_type::hyphen) { + advance_token(range_token_type::hyphen); + prerelease = parse_prerelease(); + if (current_token.type == range_token_type::dot) { + advance_token(range_token_type::dot); + prerelease_number = parse_number(); + } + } + + return {major, minor, patch, prerelease, prerelease_number}; + } + + constexpr std::uint8_t parse_number() { + const auto token = current_token; + advance_token(range_token_type::number); + + return token.number; + } + + constexpr prerelease parse_prerelease() { + const auto token = current_token; + advance_token(range_token_type::prerelease); + + return token.prerelease_type; + } + }; + + [[nodiscard]] constexpr bool is_logical_or_token() const noexcept { + return parser.current_token.type == range_token_type::logical_or; + } + [[nodiscard]] constexpr bool is_operator_token() const noexcept { + return parser.current_token.type == range_token_type::range_operator; + } + + [[nodiscard]] constexpr bool is_number_token() const noexcept { + return parser.current_token.type == range_token_type::number; + } + + range_parser parser; +}; + +} // namespace semver::range::detail + +enum struct satisfies_option : std::uint8_t { + exclude_prerelease, + include_prerelease +}; + +constexpr bool satisfies(const version& ver, std::string_view str, satisfies_option option = satisfies_option::exclude_prerelease) { + switch (option) { + case satisfies_option::exclude_prerelease: + return detail::range{str}.satisfies(ver, false); + case satisfies_option::include_prerelease: + return detail::range{str}.satisfies(ver, true); + default: + SEMVER_THROW("semver::range unexpected satisfies_option."); + } +} + +} // namespace semver::range + +// Version lib semver. +inline constexpr auto semver_version = version{SEMVER_VERSION_MAJOR, SEMVER_VERSION_MINOR, SEMVER_VERSION_PATCH}; + +} // namespace semver + +#if defined(__clang__) +# pragma clang diagnostic pop +#endif + +#endif // NEARGYE_SEMANTIC_VERSIONING_HPP diff --git a/resources/licenses/semver.txt b/resources/licenses/semver.txt new file mode 100644 index 000000000..48c79ba76 --- /dev/null +++ b/resources/licenses/semver.txt @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2018 - 2021 Daniil Goncharov +Copyright (c) 2020 - 2021 Alexander Gorbunov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2244449dd..79210ab0b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -768,6 +768,9 @@ endif () target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_BINARY_DIR}/autogen/) +# semver dependency https://github.com/Neargye/semver +target_include_directories(${LIBRARY_PROJECT} PUBLIC ${CMAKE_SOURCE_DIR}/lib/semver/include) + if (WinToast_FOUND) target_link_libraries(${LIBRARY_PROJECT} PUBLIC diff --git a/src/common/Version.hpp b/src/common/Version.hpp index 68d2b22a4..222b656f7 100644 --- a/src/common/Version.hpp +++ b/src/common/Version.hpp @@ -3,6 +3,27 @@ #include #include +/** + * Valid version formats, in order of latest to oldest + * + * Stable: + * - 2.4.0 + * + * Release candidate: + * - 2.4.0-rc.3 + * - 2.4.0-rc.2 + * - 2.4.0-rc + * + * Beta: + * - 2.4.0-beta.3 + * - 2.4.0-beta.2 + * - 2.4.0-beta + * + * Alpha: + * - 2.4.0-alpha.3 + * - 2.4.0-alpha.2 + * - 2.4.0-alpha + **/ #define CHATTERINO_VERSION "2.4.0" #if defined(Q_OS_WIN) diff --git a/src/singletons/Updates.cpp b/src/singletons/Updates.cpp index 2647cfc43..44e8b492b 100644 --- a/src/singletons/Updates.cpp +++ b/src/singletons/Updates.cpp @@ -15,6 +15,7 @@ #include #include #include +#include namespace chatterino { namespace { @@ -23,37 +24,6 @@ namespace { return getSettings()->betaUpdates ? "beta" : "stable"; } - /// Checks if the online version is newer or older than the current version. - bool isDowngradeOf(const QString &online, const QString ¤t) - { - static auto matchVersion = - QRegularExpression(R"((\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?)"); - - // Versions are just strings, they don't need to follow a specific - // format so we can only assume if one version is newer than another - // one. - - // We match x.x.x.x with each version level being optional. - - auto onlineMatch = matchVersion.match(online); - auto currentMatch = matchVersion.match(current); - - for (int i = 1; i <= 4; i++) - { - if (onlineMatch.captured(i).toInt() < - currentMatch.captured(i).toInt()) - { - return true; - } - if (onlineMatch.captured(i).toInt() > - currentMatch.captured(i).toInt()) - { - break; - } - } - - return false; - } } // namespace Updates::Updates() @@ -71,6 +41,28 @@ Updates &Updates::instance() return instance; } +/// Checks if the online version is newer or older than the current version. +bool Updates::isDowngradeOf(const QString &online, const QString ¤t) +{ + semver::version onlineVersion; + if (!onlineVersion.from_string_noexcept(online.toStdString())) + { + qCWarning(chatterinoUpdate) << "Unable to parse online version" + << online << "into a proper semver string"; + return false; + } + + semver::version currentVersion; + if (!currentVersion.from_string_noexcept(current.toStdString())) + { + qCWarning(chatterinoUpdate) << "Unable to parse current version" + << current << "into a proper semver string"; + return false; + } + + return onlineVersion < currentVersion; +} + const QString &Updates::getCurrentVersion() const { return currentVersion_; @@ -313,8 +305,8 @@ void Updates::checkForUpdates() if (this->currentVersion_ != this->onlineVersion_) { this->setStatus_(UpdateAvailable); - this->isDowngrade_ = - isDowngradeOf(this->onlineVersion_, this->currentVersion_); + this->isDowngrade_ = Updates::isDowngradeOf( + this->onlineVersion_, this->currentVersion_); } else { diff --git a/src/singletons/Updates.hpp b/src/singletons/Updates.hpp index 0b35f55b5..e08b313ba 100644 --- a/src/singletons/Updates.hpp +++ b/src/singletons/Updates.hpp @@ -24,6 +24,8 @@ public: // fourtf: don't add this class to the application class static Updates &instance(); + static bool isDowngradeOf(const QString &online, const QString ¤t); + void checkForUpdates(); const QString &getCurrentVersion() const; const QString &getOnlineVersion() const; diff --git a/src/widgets/settingspages/AboutPage.cpp b/src/widgets/settingspages/AboutPage.cpp index f6f93e084..8f9bc78ae 100644 --- a/src/widgets/settingspages/AboutPage.cpp +++ b/src/widgets/settingspages/AboutPage.cpp @@ -107,6 +107,9 @@ AboutPage::AboutPage() addLicense(form.getElement(), "magic_enum", "https://github.com/Neargye/magic_enum", ":/licenses/magic_enum.txt"); + addLicense(form.getElement(), "semver", + "https://github.com/Neargye/semver", + ":/licenses/semver.txt"); } // Attributions diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 57a5fcb17..a5663126a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,6 +23,7 @@ set(test_SOURCES ${CMAKE_CURRENT_LIST_DIR}/src/BasicPubSub.cpp ${CMAKE_CURRENT_LIST_DIR}/src/SeventvEventAPI.cpp ${CMAKE_CURRENT_LIST_DIR}/src/BttvLiveUpdates.cpp + ${CMAKE_CURRENT_LIST_DIR}/src/Updates.cpp # Add your new file above this line! ) diff --git a/tests/src/Updates.cpp b/tests/src/Updates.cpp new file mode 100644 index 000000000..da4762517 --- /dev/null +++ b/tests/src/Updates.cpp @@ -0,0 +1,42 @@ +#include "singletons/Updates.hpp" + +#include "common/Version.hpp" + +#include +#include + +using namespace chatterino; + +TEST(Updates, MustBeDowngrade) +{ + EXPECT_TRUE(Updates::isDowngradeOf("1.0.0", "2.4.5")) + << "1.0.0 must be a downgrade of 2.4.5"; + EXPECT_TRUE(Updates::isDowngradeOf("2.0.0", "2.4.5")) + << "2.0.0 must be a downgrade of 2.4.5"; + EXPECT_TRUE(Updates::isDowngradeOf("2.4.0", "2.4.5")) + << "2.4.0 must be a downgrade of 2.4.5"; + EXPECT_TRUE(Updates::isDowngradeOf("2.4.4-beta", "2.4.5")) + << "2.4.4-beta must be a downgrade of 2.4.5"; + EXPECT_TRUE(Updates::isDowngradeOf("2.4.5-beta", "2.4.5")) + << "2.4.5-beta must be a downgrade of 2.4.5"; + EXPECT_TRUE(Updates::isDowngradeOf("2.4.5-beta.1", "2.4.5-beta.2")) + << "2.4.5-beta.1 must be a downgrade of 2.4.5-beta.2"; + EXPECT_TRUE(Updates::isDowngradeOf("2.4.5-beta", "2.4.5-beta.2")) + << "2.4.5-beta must be a downgrade of 2.4.5-beta.2"; + EXPECT_TRUE(Updates::isDowngradeOf("2.4.5-beta.2", "2.4.6-beta.1")) + << "2.4.5-beta.2 must be a downgrade of 2.4.6-beta.1"; +} + +TEST(Updates, MustNotBeDowngrade) +{ + EXPECT_FALSE(Updates::isDowngradeOf("2.4.5", "2.4.5")) + << "2.4.5 must not be a downgrade of 2.4.5"; + EXPECT_FALSE(Updates::isDowngradeOf("2.4.5", "2.4.5-beta")) + << "2.4.5 must not be a downgrade of 2.4.5-beta"; +} + +TEST(Updates, ValidateCurrentVersion) +{ + EXPECT_NO_THROW(auto v = semver::from_string(CHATTERINO_VERSION)) + << "Current version must be valid semver"; +}